diff --git a/contracts/ServiceNodeRewards.sol b/contracts/ServiceNodeRewards.sol index e802f15..3f5df21 100644 --- a/contracts/ServiceNodeRewards.sol +++ b/contracts/ServiceNodeRewards.sol @@ -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); @@ -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(); diff --git a/contracts/interfaces/IServiceNodeRewards.sol b/contracts/interfaces/IServiceNodeRewards.sol index 5161ab3..3cccc0e 100644 --- a/contracts/interfaces/IServiceNodeRewards.sol +++ b/contracts/interfaces/IServiceNodeRewards.sol @@ -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; @@ -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; diff --git a/test/unit-js/ServiceNodeRewardsTest.js b/test/unit-js/ServiceNodeRewardsTest.js index 30c699a..e7f8e61 100644 --- a/test/unit-js/ServiceNodeRewardsTest.js +++ b/test/unit-js/ServiceNodeRewardsTest.js @@ -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; @@ -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"), @@ -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; + }); }); });