Skip to content

Commit

Permalink
Add contributors to the seeding process
Browse files Browse the repository at this point in the history
  • Loading branch information
Doy-lee committed Jul 8, 2024
1 parent 25b9eba commit 6f797af
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 56 deletions.
45 changes: 27 additions & 18 deletions contracts/ServiceNodeRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
event SignatureExpiryUpdated(uint256 newExpiry);

// ERRORS
error ArrayLengthMismatch();
error DeleteSentinelNodeNotAllowed();
error BLSPubkeyAlreadyExists(uint64 serviceNodeID);
error BLSPubkeyDoesNotMatch(uint64 serviceNodeID, BN256G1.G1Point pubkey);
Expand Down Expand Up @@ -551,27 +550,37 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
/// Depending on the number of nodes that must be seeded, this function
/// may necessarily be called multiple times due to gas limits.
///
/// @param pkX Array of X-coordinates for the public keys.
/// @param pkY Array of Y-coordinates for the public keys.
/// @param amounts Array of amounts that the service node has staked,
/// associated with each public key.
function seedPublicKeyList(
uint256[] calldata pkX,
uint256[] calldata pkY,
uint256[] calldata amounts
) external onlyOwner {
/// @param nodes Array of service nodes to seed the smart contract with
function seedPublicKeyList(SeedServiceNode[] calldata nodes) external onlyOwner {
require(!isStarted, "The rewards list can only be seeded after "
"deployment and before `start` is invoked on the contract.");

if (pkX.length != pkY.length || pkX.length != amounts.length) {
revert ArrayLengthMismatch();
}
for (uint256 i = 0; i < nodes.length; i++) {
SeedServiceNode calldata node = nodes[i];

// NOTE: Basic sanity checks
require(node.deposit > 0, "Deposit must be non-zero");
require(node.pubkey.X != 0 && node.pubkey.Y != 0, "The zero public key is not permitted");
require(node.contributors.length > 0,
"There must be at-least one contributor in the node. The first contributor is defined to be the operator.");
require(node.contributors.length <= 10,
"Seeded service cannot have more than 10 contributors");

// NOTE: Add node to the smart contract
uint64 allocID = serviceNodeAdd(node.pubkey);
_serviceNodes[allocID].deposit = node.deposit;

uint256 stakedAmountSum = 0;
for (uint256 contributorIndex = 0; contributorIndex < node.contributors.length; contributorIndex++) {
Contributor calldata contributor = node.contributors[contributorIndex];
stakedAmountSum += contributor.stakedAmount;
require(contributor.addr != address(0), "Contributor address cannot be the nil address (zero)");
_serviceNodes[allocID].contributors.push(contributor);
}
require(stakedAmountSum == node.deposit,
"Sum of the contributor(s) staked amounts do not match the deposit of the node");

for (uint256 i = 0; i < pkX.length; i++) {
BN256G1.G1Point memory pubkey = BN256G1.G1Point(pkX[i], pkY[i]);
uint64 allocID = serviceNodeAdd(pubkey);
_serviceNodes[allocID].deposit = amounts[i];
emit NewSeededServiceNode(allocID, pubkey);
emit NewSeededServiceNode(allocID, node.pubkey);
}

updateBLSNonSignerThreshold();
Expand Down
10 changes: 8 additions & 2 deletions contracts/interfaces/IServiceNodeRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import "../libraries/BN256G1.sol";

interface IServiceNodeRewards {
struct Contributor {
address addr; // The address of the contributor
address addr; // The address of the contributor
uint256 stakedAmount; // The amount staked by the contributor
}

struct SeedServiceNode {
BN256G1.G1Point pubkey;
uint256 deposit;
Contributor[] contributors;
}

/// @notice Represents a service node in the network.
struct ServiceNode {
uint64 next;
Expand Down Expand Up @@ -90,7 +96,7 @@ interface IServiceNodeRewards {
BLSSignatureParams calldata blsSignature,
uint64[] memory ids
) external;
function seedPublicKeyList(uint256[] calldata pkX, uint256[] calldata pkY, uint256[] calldata amounts) external;
function seedPublicKeyList(SeedServiceNode[] calldata nodes) external;
function serviceNodesLength() external view returns (uint256 count);
function updateServiceNodesLength() external;
function start() external;
Expand Down
246 changes: 210 additions & 36 deletions test/unit-js/ServiceNodeRewardsTest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");

async function verifySeedData(contractSN, seedEntry) {
expect(contractSN.pubkey[0]).to.equal(BigInt(seedEntry.pubkey.X));
expect(contractSN.pubkey[1]).to.equal(BigInt(seedEntry.pubkey.Y));
expect(contractSN.deposit).to.equal(BigInt(seedEntry.deposit));
expect(contractSN.contributors.length).to.equal(seedEntry.contributors.length);
for (let contributorIndex = 0; contributorIndex < contractSN.contributors.length; contributorIndex++) {
expect(BigInt(contractSN.contributors[0].addr)).to.equal(BigInt(seedEntry.contributors[contributorIndex].addr));
expect(contractSN.contributors[0].stakedAmount).to.equal(seedEntry.contributors[contributorIndex].stakedAmount);
}
}

describe("ServiceNodeRewards Contract Tests", function () {
let MockERC20;
let mockERC20;
Expand Down Expand Up @@ -45,39 +56,61 @@ describe("ServiceNodeRewards Contract Tests", function () {
describe("Seeding the public key as owner", function () {

it("Should correctly seed public key list with a single item", async function () {
// Example values for BN256G1 X and Y coordinates (These are arbitrary 32-byte hexadecimal values)
let P = [
BigInt("0x0b5e634d0407c021e9e9dd9d03c4965810e236fef0955ab345e1d049a0438ec6"),
BigInt("0x1dbb7bf2b1f5340d4b5c466a0641b00cd3a9d9588c7bcad1c3158bdcc65c3332"),
const seedData = [
{
pubkey: {
X: "0x0b5e634d0407c021e9e9dd9d03c4965810e236fef0955ab345e1d049a0438ec6",
Y: "0x1dbb7bf2b1f5340d4b5c466a0641b00cd3a9d9588c7bcad1c3158bdcc65c3332",
},
deposit: 1000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 1000,
}
]
},
];

// Convert BigInt to hex strings
const pkX = [P[0]];
const pkY = [P[1]];
const amounts = [1000]; // Example token amounts

await serviceNodeRewards.connect(owner).seedPublicKeyList(pkX, pkY, amounts);

await serviceNodeRewards.connect(owner).seedPublicKeyList(seedData);
expect(await serviceNodeRewards.serviceNodesLength()).to.equal(1);
let aggregate_pubkey = await serviceNodeRewards.aggregatePubkey();
expect(aggregate_pubkey[0] == P[0])
expect(aggregate_pubkey[1] == P[1])
expect(aggregate_pubkey[0] == seedData[0].pubkey.X)
expect(aggregate_pubkey[1] == seedData[0].pubkey.Y)
verifySeedData(await serviceNodeRewards.serviceNodes(1), seedData[0]);
});

it("Should correctly seed public key list with multiple items", async function () {
// Example values for BN256G1 X and Y coordinates (These are arbitrary 32-byte hexadecimal values)
let P = [
BigInt("0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed"),
BigInt("0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab"),
BigInt("0x2ef6b73ab4486484de80681753a6a90c6a88a71f60aace9520fe6bb8bb8de34e"),
BigInt("0x29b8f2a87a758a89c394b121298b946dce9ada3226b5d008e54e54ddcd9e5227"),
const seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 2000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 2000,
}
]
},
{
pubkey: {
X: "0x2ef6b73ab4486484de80681753a6a90c6a88a71f60aace9520fe6bb8bb8de34e",
Y: "0x29b8f2a87a758a89c394b121298b946dce9ada3226b5d008e54e54ddcd9e5227",
},
deposit: 2000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 2000,
}
]
},
];

const pkX = [P[0], P[2]];
const pkY = [P[1], P[3]];
const amounts = [1000, 2000]; // Example token amounts

await serviceNodeRewards.connect(owner).seedPublicKeyList(pkX, pkY, amounts);
await serviceNodeRewards.connect(owner).seedPublicKeyList(seedData);
let expected_aggregate_pubkey = [
BigInt("0x040a638a13320ea807115f1e7865c89c70d2d3df83e2e8c3eaea519e18b6e6b0"),
BigInt("0x019081a4475388be53e1088f6ec0dd79f99fc794709b9cf8b1ad401a9c4d3413"),
Expand All @@ -86,26 +119,167 @@ describe("ServiceNodeRewards Contract Tests", function () {
expect(aggregate_pubkey[0] == expected_aggregate_pubkey[0])
expect(aggregate_pubkey[1] == expected_aggregate_pubkey[1])

// NOTE: We know that the sentinel node is reserved at the 0th ID.
// Hence the 2 service nodes we added are at ID 1 and 2.
expect(await serviceNodeRewards.serviceNodesLength()).to.equal(2);

verifySeedData(await serviceNodeRewards.serviceNodes(1), seedData[0]);
verifySeedData(await serviceNodeRewards.serviceNodes(2), seedData[1]);
});

it("Should fail to seed public key list with duplicate items", async function () {
// Example values for BN256G1 X and Y coordinates (These are arbitrary 32-byte hexadecimal values)
let P = [
BigInt("0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed"),
BigInt("0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab"),
BigInt("0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed"),
BigInt("0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab"),
const seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 1000,
}
]
},
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 1000,
}
]
},
];

const pkX = [P[0], P[2]];
const pkY = [P[1], P[3]];
const amounts = [1000, 2000]; // Example token amounts
await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(pkX, pkY, amounts))
await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData))
.to.be.revertedWithCustomError(serviceNodeRewards, "BLSPubkeyAlreadyExists")
});


it("Fails when sum of contributor stakes do not add up the deposit amount", async function () {
const seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 500,
}
]
},
];

await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData)).to.be.reverted;
});

it("Fails if deposit is 0", async function () {
const seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 0,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 1000,
}
]
},
];

await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData)).to.be.reverted;
});

it("Fails if the BLS pubkey is the zero key", async function () {
const seedData = [
{
pubkey: {
X: "0x0000000000000000000000000000000000000000000000000000000000000000",
Y: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
deposit: 1000,
contributors: [
{
addr: "0x66d801a70615979d82c304b7db374d11c232db66",
stakedAmount: 1000,
}
]
},
];

await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData)).to.be.reverted;
});

it("Fails if there are no contributors", async function () {
const seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1000,
contributors: []
},
];

await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData)).to.be.reverted;
});

it("Supports 10 contributors", async function () {
seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1000,
contributors: []
},
];

const contributorCount = 10;
const ethAddr = "0x66d801a70615979d82c304b7db374d11c232db66";
const stakePerContributor = seedData[0].deposit / contributorCount;
for (let index = 0; index < contributorCount; index++) {
seedData[0].contributors.push({addr: ethAddr, stakedAmount: stakePerContributor});
}

await serviceNodeRewards.connect(owner).seedPublicKeyList(seedData);
expect(await serviceNodeRewards.serviceNodesLength()).to.equal(1);
verifySeedData(await serviceNodeRewards.serviceNodes(1), seedData[0]);
});

it("Fails if there are 11 contributors (pre-migration Oxen has a 10 contributor limit)", async function () {
seedData = [
{
pubkey: {
X: "0x12c59fb45c483177873406e5b74a2e6914fe25a591185f30d2788e737da6f2ed",
Y: "0x016e56f330d11faaf90ec281b1c4184e98a52d4043075fcbe45a976de0f795ab",
},
deposit: 1100,
contributors: []
},
];

const contributorCount = 11;
const ethAddr = "0x66d801a70615979d82c304b7db374d11c232db66";
const stakePerContributor = seedData[0].deposit / contributorCount;
for (let index = 0; index < contributorCount; index++) {
seedData[0].contributors.push({addr: ethAddr, stakedAmount: stakePerContributor});
}

await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(seedData)).to.be.reverted;
});
});
});

0 comments on commit 6f797af

Please sign in to comment.