From b980c23c8f9b70432a01d28c99a3748bfdde95a3 Mon Sep 17 00:00:00 2001 From: Zvonimir Date: Fri, 13 Jun 2025 15:27:23 +0200 Subject: [PATCH 1/3] improve challenge creation --- abi/RandomSamplingStorage.json | 178 +++++++++++++++++++ contracts/RandomSampling.sol | 116 +++++++----- contracts/storage/RandomSamplingStorage.sol | 96 +++++++++- deploy/022_deploy_random_sampling_storage.ts | 4 + deployments/parameters.json | 44 +++-- 5 files changed, 377 insertions(+), 61 deletions(-) diff --git a/abi/RandomSamplingStorage.json b/abi/RandomSamplingStorage.json index e1117de8..16df19c0 100644 --- a/abi/RandomSamplingStorage.json +++ b/abi/RandomSamplingStorage.json @@ -10,6 +10,16 @@ "internalType": "uint16", "name": "_proofingPeriodDurationInBlocks", "type": "uint16" + }, + { + "internalType": "uint16", + "name": "_challengeRecencyFactor", + "type": "uint16" + }, + { + "internalType": "uint8", + "name": "_bfsIterations", + "type": "uint8" } ], "stateMutability": "nonpayable", @@ -87,6 +97,44 @@ "name": "AllNodesEpochScoreAdded", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "oldBfsIterations", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "newBfsIterations", + "type": "uint8" + } + ], + "name": "BfsIterationsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "oldChallengeRecencyFactor", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "newChallengeRecencyFactor", + "type": "uint16" + } + ], + "name": "ChallengeRecencyFactorUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -305,6 +353,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "DEFAULT_BFS_ITERATIONS", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_CHALLENGE_RECENCY_FACTOR", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -509,6 +583,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "bfsIterations", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "challengeRecencyFactor", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "chronos", @@ -661,6 +761,45 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getBfsIterations", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getChallengeRecencyFactor", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDefaultBfsIterations", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [ { @@ -810,6 +949,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getMaxChallengeRecencyFactor", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [ { @@ -1175,6 +1327,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "newBfsIterations", + "type": "uint8" + } + ], + "name": "setBfsIterations", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "newChallengeRecencyFactor", + "type": "uint16" + } + ], + "name": "setChallengeRecencyFactor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/RandomSampling.sol b/contracts/RandomSampling.sol index 26a49bc0..5e4e9930 100644 --- a/contracts/RandomSampling.sol +++ b/contracts/RandomSampling.sol @@ -293,12 +293,10 @@ contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable { } /** - * @dev BFS approach to finding an active knowledge collection - * @param randomSeed Random seed for picking a collection from current range - * @param start Start of the range (inclusive) - * @param end End of the range (inclusive) - * @param currentEpoch Current epoch to check collection activity against - * @return knowledgeCollectionId ID of an active knowledge collection, or 0 if none found + * @dev Adaptive BFS search for an active collection. + * challengeRecencyFactor 5,000 → 50% chance to pop newer/older half first + * 10,000→ always pop newer half first + * bfsIterations user-controllable gas/latency guard (default: 50) */ function _findActiveKnowledgeCollection( bytes32 randomSeed, @@ -306,63 +304,87 @@ contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable { uint256 end, uint256 currentEpoch ) internal view returns (uint256) { - // Queue using fixed array - [start1, end1, start2, end2, ...] - uint256[100] memory queue; // Can hold 50 ranges max - uint8 queueStart = 0; // Front of queue - uint8 queueEnd = 0; // Back of queue - - // Push initial range + uint8 bfsIterations = randomSamplingStorage.getBfsIterations(); + uint8 itCap = bfsIterations == 0 ? randomSamplingStorage.getDefaultBfsIterations() : bfsIterations; + uint16 challengeRecencyFactor = randomSamplingStorage.getChallengeRecencyFactor(); + uint16 maxChallengeRecencyFactor = randomSamplingStorage.getMaxChallengeRecencyFactor(); + + // Calculate proper queue size: we process at most itCap ranges, each can split into 2 + // Maximum queue size = initial range + ranges from splitting = 2 * (itCap + 1) slots + uint256 queueSize = 2 * (uint256(itCap) + 1); + uint256[] memory queue = new uint256[](queueSize); + + uint16 queueStart = 0; // front index + uint16 queueEnd = 0; // back index queue[queueEnd++] = start; queue[queueEnd++] = end; - bytes32 currentRandom = randomSeed; + bytes32 currentRandomSeed = randomSeed; uint8 iterations = 0; - while (queueStart < queueEnd && iterations < 50) { - // Pop range from front of queue (BFS behavior) - uint256 currentStart = queue[queueStart++]; - uint256 currentEnd = queue[queueStart++]; - - // Pick random collection from current range - uint256 randomKcId = currentStart + (uint256(currentRandom) % (currentEnd - currentStart + 1)); - - // Check if this collection is active - if (currentEpoch <= knowledgeCollectionStorage.getEndEpoch(randomKcId)) { - return randomKcId; - } + while (queueStart < queueEnd && iterations < itCap) { + uint256 lo = queue[queueStart++]; + uint256 hi = queue[queueStart++]; + uint256 span = hi - lo + 1; - // If single element and not active, continue to next range - if (currentStart == currentEnd) { - currentRandom = keccak256(abi.encodePacked(currentRandom)); - unchecked { - iterations++; + uint256 pick; + uint256 roll = uint256(currentRandomSeed); + if (challengeRecencyFactor == 0) { + pick = lo + (roll % span); // uniform + } else { + // 1) decide which half we favour this iteration + bool favourNewer = (roll % maxChallengeRecencyFactor) < challengeRecencyFactor; + uint256 mid = lo + (span >> 1); + + if (favourNewer) { + // sample uniformly from newer half [mid+1,hi] + uint256 newerSpan = hi - mid; + pick = (newerSpan == 0) ? hi : mid + 1 + (roll % newerSpan); + } else { + // sample from older half [lo,mid] + uint256 olderSpan = mid - lo + 1; + pick = lo + (roll % olderSpan); } - continue; } - // Split range and push both halves to back of queue (BFS order) - uint256 mid = currentStart + (currentEnd - currentStart) / 2; + if (currentEpoch <= knowledgeCollectionStorage.getEndEpoch(pick)) { + return pick; + } - if (queueEnd < 96) { - // Leave room for both ranges - // Always push left half first, then right half (consistent BFS) - if (currentStart <= mid) { - queue[queueEnd++] = currentStart; - queue[queueEnd++] = mid; - } - if (mid + 1 <= currentEnd) { - queue[queueEnd++] = mid + 1; - queue[queueEnd++] = currentEnd; + // not active → split and enqueue, order depends on challengeRecencyFactor + if (lo != hi && queueEnd <= queueSize - 4) { + // Ensure we have space for 2 ranges (4 slots) + uint256 mid = lo + (span >> 1); + bool newerFirst = (uint256(currentRandomSeed) >> 128) % maxChallengeRecencyFactor < + challengeRecencyFactor; + + if (newerFirst) { + if (mid + 1 <= hi) { + queue[queueEnd++] = mid + 1; + queue[queueEnd++] = hi; + } + if (lo <= mid) { + queue[queueEnd++] = lo; + queue[queueEnd++] = mid; + } + } else { + if (lo <= mid) { + queue[queueEnd++] = lo; + queue[queueEnd++] = mid; + } + if (mid + 1 <= hi) { + queue[queueEnd++] = mid + 1; + queue[queueEnd++] = hi; + } } } - currentRandom = keccak256(abi.encodePacked(currentRandom)); + currentRandomSeed = keccak256(abi.encodePacked(currentRandomSeed)); unchecked { - iterations++; + ++iterations; } } - - return 0; // No active collection found + return 0; // no active collection found } function calculateNodeScore(uint72 identityId) public view returns (uint256) { diff --git a/contracts/storage/RandomSamplingStorage.sol b/contracts/storage/RandomSamplingStorage.sol index bcbcaf80..c1cb17f0 100644 --- a/contracts/storage/RandomSamplingStorage.sol +++ b/contracts/storage/RandomSamplingStorage.sol @@ -8,6 +8,8 @@ import {IInitializable} from "../interfaces/IInitializable.sol"; import {RandomSamplingLib} from "../libraries/RandomSamplingLib.sol"; import {Chronos} from "../storage/Chronos.sol"; import {ContractStatus} from "../abstract/ContractStatus.sol"; +import {ICustodian} from "../interfaces/ICustodian.sol"; +import {HubLib} from "../libraries/HubLib.sol"; contract RandomSamplingStorage is INamed, IVersioned, IInitializable, ContractStatus { string private constant _NAME = "RandomSamplingStorage"; @@ -16,8 +18,14 @@ contract RandomSamplingStorage is INamed, IVersioned, IInitializable, ContractSt Chronos public chronos; RandomSamplingLib.ProofingPeriodDuration[] public proofingPeriodDurations; - uint256 private activeProofPeriodStartBlock; + + // _findActiveKnowledgeCollection related variables + uint16 public challengeRecencyFactor; // e.g. 5000 = 50% chance to pop newer/older half first, 10 000 = always newest first + uint8 public bfsIterations; // cap the while-loop (gas safety) + uint16 public constant MAX_CHALLENGE_RECENCY_FACTOR = 10_000; // 100% + uint8 public constant DEFAULT_BFS_ITERATIONS = 50; // 50 iterations + // identityId => Challenge - used in proof to verify the challenge is within proofing period mapping(uint72 => RandomSamplingLib.Challenge) public nodesChallenges; // epoch => identityId => successful proofs count @@ -77,15 +85,40 @@ contract RandomSamplingStorage is INamed, IVersioned, IInitializable, ContractSt bytes32 indexed delegatorKey, uint256 newDelegatorLastSettledNodeEpochScorePerStake ); - - constructor(address hubAddress, uint16 _proofingPeriodDurationInBlocks) ContractStatus(hubAddress) { + event ChallengeRecencyFactorUpdated(uint16 oldChallengeRecencyFactor, uint16 newChallengeRecencyFactor); + event BfsIterationsUpdated(uint8 oldBfsIterations, uint8 newBfsIterations); + + constructor( + address hubAddress, + uint16 _proofingPeriodDurationInBlocks, + uint16 _challengeRecencyFactor, + uint8 _bfsIterations + ) ContractStatus(hubAddress) { require(_proofingPeriodDurationInBlocks > 0, "Proofing period duration in blocks must be greater than 0"); + require( + _challengeRecencyFactor <= MAX_CHALLENGE_RECENCY_FACTOR, + "Challenge recency factor must be between 0 and MAX_CHALLENGE_RECENCY_FACTOR" + ); + require(_bfsIterations > 0, "Bfs iterations must be greater than 0"); + proofingPeriodDurations.push( RandomSamplingLib.ProofingPeriodDuration({ durationInBlocks: _proofingPeriodDurationInBlocks, effectiveEpoch: 0 }) ); + challengeRecencyFactor = _challengeRecencyFactor; + bfsIterations = _bfsIterations; + + emit ProofingPeriodDurationAdded(_proofingPeriodDurationInBlocks, 0); + emit ChallengeRecencyFactorUpdated(0, _challengeRecencyFactor); + emit BfsIterationsUpdated(0, _bfsIterations); + } + + // @dev Only transactions by HubController owner or one of the owners of the MultiSig Wallet + modifier onlyOwnerOrMultiSigOwner() { + _checkOwnerOrMultiSigOwner(); + _; } function initialize() public onlyHub { @@ -96,6 +129,11 @@ contract RandomSamplingStorage is INamed, IVersioned, IInitializable, ContractSt durationInBlocks: proofingPeriodDurations[proofingPeriodDurations.length - 1].durationInBlocks, effectiveEpoch: chronos.getCurrentEpoch() }); + + emit ProofingPeriodDurationAdded( + proofingPeriodDurations[proofingPeriodDurations.length - 1].durationInBlocks, + proofingPeriodDurations[proofingPeriodDurations.length - 1].effectiveEpoch + ); } function name() external pure virtual override returns (string memory) { @@ -342,4 +380,56 @@ contract RandomSamplingStorage is INamed, IVersioned, IInitializable, ContractSt newNodeEpochScorePerStake ); } + + function setChallengeRecencyFactor(uint16 newChallengeRecencyFactor) external onlyOwnerOrMultiSigOwner { + require( + newChallengeRecencyFactor <= MAX_CHALLENGE_RECENCY_FACTOR, + "Recency factor must be between 0 and MAX_CHALLENGE_RECENCY_FACTOR" + ); + uint16 oldChallengeRecencyFactor = challengeRecencyFactor; + challengeRecencyFactor = newChallengeRecencyFactor; + emit ChallengeRecencyFactorUpdated(oldChallengeRecencyFactor, newChallengeRecencyFactor); + } + + function getChallengeRecencyFactor() external view returns (uint16) { + return challengeRecencyFactor; + } + + function setBfsIterations(uint8 newBfsIterations) external onlyOwnerOrMultiSigOwner { + require(newBfsIterations > 0, "Iterations must be greater than 0"); + uint8 oldBfsIterations = bfsIterations; + bfsIterations = newBfsIterations; + emit BfsIterationsUpdated(oldBfsIterations, newBfsIterations); + } + + function getBfsIterations() external view returns (uint8) { + return bfsIterations; + } + + function getMaxChallengeRecencyFactor() external pure returns (uint16) { + return MAX_CHALLENGE_RECENCY_FACTOR; + } + + function getDefaultBfsIterations() external pure returns (uint8) { + return DEFAULT_BFS_ITERATIONS; + } + + function _isMultiSigOwner(address multiSigAddress) internal view returns (bool) { + try ICustodian(multiSigAddress).getOwners() returns (address[] memory multiSigOwners) { + for (uint256 i = 0; i < multiSigOwners.length; i++) { + if (msg.sender == multiSigOwners[i]) { + return true; + } + } // solhint-disable-next-line no-empty-blocks + } catch {} + + return false; + } + + function _checkOwnerOrMultiSigOwner() internal view virtual { + address hubOwner = hub.owner(); + if (msg.sender != hubOwner && !_isMultiSigOwner(hubOwner)) { + revert HubLib.UnauthorizedAccess("Only Hub Owner or Multisig Owner"); + } + } } diff --git a/deploy/022_deploy_random_sampling_storage.ts b/deploy/022_deploy_random_sampling_storage.ts index 81c5fb9f..fd54a324 100644 --- a/deploy/022_deploy_random_sampling_storage.ts +++ b/deploy/022_deploy_random_sampling_storage.ts @@ -3,6 +3,8 @@ import { DeployFunction } from 'hardhat-deploy/types'; type RandomSamplingStorageNetworkConfig = { proofingPeriodDurationInBlocks: string; + challengeRecencyFactor: string; + bfsIterations: string; }; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { @@ -22,6 +24,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { newContractName: 'RandomSamplingStorage', additionalArgs: [ randomSamplingStorageParametersConfig.proofingPeriodDurationInBlocks, + randomSamplingStorageParametersConfig.challengeRecencyFactor, + randomSamplingStorageParametersConfig.bfsIterations, ], }); }; diff --git a/deployments/parameters.json b/deployments/parameters.json index c304cef5..184f5c74 100644 --- a/deployments/parameters.json +++ b/deployments/parameters.json @@ -32,10 +32,14 @@ }, "RandomSamplingStorage": { "hardhat": { - "proofingPeriodDurationInBlocks": "100" + "proofingPeriodDurationInBlocks": "100", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "localhost": { - "proofingPeriodDurationInBlocks": "100" + "proofingPeriodDurationInBlocks": "100", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" } } }, @@ -78,13 +82,19 @@ }, "RandomSamplingStorage": { "base_sepolia_dev": { - "proofingPeriodDurationInBlocks": "900" + "proofingPeriodDurationInBlocks": "900", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "base_sepolia_stable_dev_staging": { - "proofingPeriodDurationInBlocks": "900" + "proofingPeriodDurationInBlocks": "900", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "base_sepolia_stable_dev_prod": { - "proofingPeriodDurationInBlocks": "900" + "proofingPeriodDurationInBlocks": "900", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" } } }, @@ -128,13 +138,19 @@ }, "RandomSamplingStorage": { "neuroweb_testnet": { - "proofingPeriodDurationInBlocks": "257" + "proofingPeriodDurationInBlocks": "257", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "gnosis_chiado_test": { - "proofingPeriodDurationInBlocks": "360" + "proofingPeriodDurationInBlocks": "360", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "base_sepolia_test": { - "proofingPeriodDurationInBlocks": "900" + "proofingPeriodDurationInBlocks": "900", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" } } }, @@ -178,13 +194,19 @@ }, "RandomSamplingStorage": { "neuroweb_mainnet": { - "proofingPeriodDurationInBlocks": "300" + "proofingPeriodDurationInBlocks": "300", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "gnosis_mainnet": { - "proofingPeriodDurationInBlocks": "360" + "proofingPeriodDurationInBlocks": "360", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" }, "base_mainnet": { - "proofingPeriodDurationInBlocks": "900" + "proofingPeriodDurationInBlocks": "900", + "challengeRecencyFactor": "6000", + "bfsIterations": "50" } } } From da286cc62d4372ea4775bca369058fe37b6d0848 Mon Sep 17 00:00:00 2001 From: Zvonimir Date: Fri, 13 Jun 2025 16:06:58 +0200 Subject: [PATCH 2/3] Add more tests to RS Storage and adapt old ones --- test/unit/RandomSamplingStorage.test.ts | 1858 +++++++++++++++++------ 1 file changed, 1427 insertions(+), 431 deletions(-) diff --git a/test/unit/RandomSamplingStorage.test.ts b/test/unit/RandomSamplingStorage.test.ts index e0068f34..8926c636 100644 --- a/test/unit/RandomSamplingStorage.test.ts +++ b/test/unit/RandomSamplingStorage.test.ts @@ -1,10 +1,14 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import hre, { ethers } from 'hardhat'; import { EventLog } from 'ethers'; +import hre, { ethers } from 'hardhat'; import parameters from '../../deployments/parameters.json'; +import { + mineBlocks, + mineProofPeriodBlocks, +} from '../../test/helpers/blockchain-helpers'; import { Hub, RandomSamplingStorage, @@ -13,7 +17,6 @@ import { RandomSampling, } from '../../typechain'; import { RandomSamplingLib } from '../../typechain/contracts/storage/RandomSamplingStorage'; -import { mineBlocks, mineProofPeriodBlocks } from '../../test/helpers/blockchain-helpers'; const HUNDRED_ETH = ethers.parseEther('100'); @@ -21,21 +24,24 @@ const HUNDRED_ETH = ethers.parseEther('100'); async function createMockChallenge( randomSamplingStorage: RandomSamplingStorage, knowledgeCollectionStorage: KnowledgeCollectionStorage, - chronos: Chronos + chronos: Chronos, ): Promise { const currentEpoch = await chronos.getCurrentEpoch(); await randomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); - const { activeProofPeriodStartBlock } = await randomSamplingStorage.getActiveProofPeriodStatus(); - const proofingPeriodDuration = await randomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + const { activeProofPeriodStartBlock } = + await randomSamplingStorage.getActiveProofPeriodStatus(); + const proofingPeriodDuration = + await randomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); return { knowledgeCollectionId: 1n, chunkId: 1n, - knowledgeCollectionStorageContract: await knowledgeCollectionStorage.getAddress(), + knowledgeCollectionStorageContract: + await knowledgeCollectionStorage.getAddress(), epoch: currentEpoch, activeProofPeriodStartBlock, proofingPeriodDurationInBlocks: proofingPeriodDuration, - solved: false + solved: false, }; } @@ -48,18 +54,20 @@ type RandomStorageFixture = { const PANIC_ARITHMETIC_OVERFLOW = 0x11; +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function impersonateAndFund(contract: any) { await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [await contract.getAddress()], }); - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await contract.getAddress(), - `0x${HUNDRED_ETH.toString(16)}` + `0x${HUNDRED_ETH.toString(16)}`, ]); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function stopImpersonate(contract: any) { await hre.network.provider.request({ method: 'hardhat_stopImpersonatingAccount', @@ -77,7 +85,12 @@ describe('@unit RandomSamplingStorage', function () { let accounts: SignerWithAddress[]; const proofingPeriodDurationInBlocks = - parameters.development.RandomSamplingStorage.hardhat.proofingPeriodDurationInBlocks; + parameters.development.RandomSamplingStorage.hardhat + .proofingPeriodDurationInBlocks; + const challengeRecencyFactor = + parameters.development.RandomSamplingStorage.hardhat.challengeRecencyFactor; + const bfsIterations = + parameters.development.RandomSamplingStorage.hardhat.bfsIterations; async function deployRandomSamplingFixture(): Promise { await hre.deployments.fixture([ @@ -110,7 +123,8 @@ describe('@unit RandomSamplingStorage', function () { } async function updateAndGetActiveProofPeriod() { - const tx = await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); + const tx = + await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); await tx.wait(); return await RandomSamplingStorage.getActiveProofPeriodStatus(); } @@ -136,35 +150,38 @@ describe('@unit RandomSamplingStorage', function () { // Impersonate RandomSampling for contract-only function await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Test initial state - const initialCount = await RandomSamplingStorage.getEpochNodeValidProofsCount( - currentEpoch, - nodeId - ); + const initialCount = + await RandomSamplingStorage.getEpochNodeValidProofsCount( + currentEpoch, + nodeId, + ); expect(initialCount).to.equal(0n, 'Should start with 0 proofs'); // Test incrementing in current epoch - await RandomSamplingStorage.connect(rsSigner).incrementEpochNodeValidProofsCount( - currentEpoch, - nodeId - ); - const countAfterIncrement = await RandomSamplingStorage.getEpochNodeValidProofsCount( - currentEpoch, - nodeId - ); + await RandomSamplingStorage.connect( + rsSigner, + ).incrementEpochNodeValidProofsCount(currentEpoch, nodeId); + const countAfterIncrement = + await RandomSamplingStorage.getEpochNodeValidProofsCount( + currentEpoch, + nodeId, + ); expect(countAfterIncrement).to.equal(1n, 'Should increment to 1'); // Test multiple increments - await RandomSamplingStorage.connect(rsSigner).incrementEpochNodeValidProofsCount( - currentEpoch, - nodeId - ); - const countAfterMultiple = await RandomSamplingStorage.getEpochNodeValidProofsCount( - currentEpoch, - nodeId - ); + await RandomSamplingStorage.connect( + rsSigner, + ).incrementEpochNodeValidProofsCount(currentEpoch, nodeId); + const countAfterMultiple = + await RandomSamplingStorage.getEpochNodeValidProofsCount( + currentEpoch, + nodeId, + ); expect(countAfterMultiple).to.equal(2n, 'Should increment to 2'); // Move to next epoch properly @@ -173,17 +190,21 @@ describe('@unit RandomSamplingStorage', function () { expect(nextEpoch).to.equal(currentEpoch + 1n, 'Should be in next epoch'); // Test in next epoch - await RandomSamplingStorage.connect(rsSigner).incrementEpochNodeValidProofsCount( - nextEpoch, - nodeId - ); - const nextEpochCount = await RandomSamplingStorage.getEpochNodeValidProofsCount( - nextEpoch, - nodeId - ); + await RandomSamplingStorage.connect( + rsSigner, + ).incrementEpochNodeValidProofsCount(nextEpoch, nodeId); + const nextEpochCount = + await RandomSamplingStorage.getEpochNodeValidProofsCount( + nextEpoch, + nodeId, + ); expect(nextEpochCount).to.equal(1n, 'Should start at 1 in new epoch'); - expect(await RandomSamplingStorage.getEpochNodeValidProofsCount(currentEpoch, nodeId)) - .to.equal(2n, 'Previous epoch count should remain unchanged'); + expect( + await RandomSamplingStorage.getEpochNodeValidProofsCount( + currentEpoch, + nodeId, + ), + ).to.equal(2n, 'Previous epoch count should remain unchanged'); await stopImpersonate(RandomSampling); }); @@ -195,20 +216,26 @@ describe('@unit RandomSamplingStorage', function () { // Impersonate RandomSampling for contract-only functions await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Test initial state for all nodes for (const nodeId of nodeIds) { - expect(await RandomSamplingStorage.getNodeEpochProofPeriodScore( - nodeId, - currentEpoch, - proofPeriodIndex - )).to.equal(0n, `Node ${nodeId} should start with 0 score`); + expect( + await RandomSamplingStorage.getNodeEpochProofPeriodScore( + nodeId, + currentEpoch, + proofPeriodIndex, + ), + ).to.equal(0n, `Node ${nodeId} should start with 0 score`); } - expect(await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( - currentEpoch, - proofPeriodIndex - )).to.equal(0n, 'Global score should start at 0'); + expect( + await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( + currentEpoch, + proofPeriodIndex, + ), + ).to.equal(0n, 'Global score should start at 0'); // Add scores to different nodes const scores = [100n, 200n, 300n]; @@ -220,70 +247,90 @@ describe('@unit RandomSamplingStorage', function () { expectedGlobalScore += score; // Add score to node - await RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore( + await RandomSamplingStorage.connect( + rsSigner, + ).addToNodeEpochProofPeriodScore( currentEpoch, proofPeriodIndex, nodeId, - score + score, ); // Add to global score - await RandomSamplingStorage.connect(rsSigner).addToAllNodesEpochProofPeriodScore( + await RandomSamplingStorage.connect( + rsSigner, + ).addToAllNodesEpochProofPeriodScore( currentEpoch, proofPeriodIndex, - score + score, ); // Verify individual node score - const nodeScore = await RandomSamplingStorage.getNodeEpochProofPeriodScore( - nodeId, - currentEpoch, - proofPeriodIndex + const nodeScore = + await RandomSamplingStorage.getNodeEpochProofPeriodScore( + nodeId, + currentEpoch, + proofPeriodIndex, + ); + expect(nodeScore).to.equal( + score, + `Node ${nodeId} should have score ${score}`, ); - expect(nodeScore).to.equal(score, - `Node ${nodeId} should have score ${score}`); // Verify global score - const globalScore = await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( - currentEpoch, - proofPeriodIndex + const globalScore = + await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( + currentEpoch, + proofPeriodIndex, + ); + expect(globalScore).to.equal( + expectedGlobalScore, + `Global score should be ${expectedGlobalScore} after adding ${score} to node ${nodeId}`, ); - expect(globalScore).to.equal(expectedGlobalScore, - `Global score should be ${expectedGlobalScore} after adding ${score} to node ${nodeId}`); } // Test adding more score to existing node const additionalScore = 50n; - await RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore( + await RandomSamplingStorage.connect( + rsSigner, + ).addToNodeEpochProofPeriodScore( currentEpoch, proofPeriodIndex, nodeIds[0], - additionalScore + additionalScore, ); // Add to global score - await RandomSamplingStorage.connect(rsSigner).addToAllNodesEpochProofPeriodScore( + await RandomSamplingStorage.connect( + rsSigner, + ).addToAllNodesEpochProofPeriodScore( currentEpoch, proofPeriodIndex, - additionalScore + additionalScore, ); // Verify updated individual score - const updatedNodeScore = await RandomSamplingStorage.getNodeEpochProofPeriodScore( - nodeIds[0], - currentEpoch, - proofPeriodIndex + const updatedNodeScore = + await RandomSamplingStorage.getNodeEpochProofPeriodScore( + nodeIds[0], + currentEpoch, + proofPeriodIndex, + ); + expect(updatedNodeScore).to.equal( + scores[0] + additionalScore, + 'Node score should be updated with additional score', ); - expect(updatedNodeScore).to.equal(scores[0] + additionalScore, - 'Node score should be updated with additional score'); // Verify updated global score - const updatedGlobalScore = await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( - currentEpoch, - proofPeriodIndex + const updatedGlobalScore = + await RandomSamplingStorage.getEpochAllNodesProofPeriodScore( + currentEpoch, + proofPeriodIndex, + ); + expect(updatedGlobalScore).to.equal( + expectedGlobalScore + additionalScore, + 'Global score should be updated with additional score', ); - expect(updatedGlobalScore).to.equal(expectedGlobalScore + additionalScore, - 'Global score should be updated with additional score'); await stopImpersonate(RandomSampling); }); @@ -296,37 +343,43 @@ describe('@unit RandomSamplingStorage', function () { const score = 100n; // Test initial state - expect(await RandomSamplingStorage.getEpochNodeDelegatorScore( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.equal(0n); + expect( + await RandomSamplingStorage.getEpochNodeDelegatorScore( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.equal(0n); // Add score and verify accumulation await RandomSamplingStorage.connect(signer).addToEpochNodeDelegatorScore( currentEpoch, publishingNodeIdentityId, delegatorKey, - score + score, ); - expect(await RandomSamplingStorage.getEpochNodeDelegatorScore( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.equal(score); + expect( + await RandomSamplingStorage.getEpochNodeDelegatorScore( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.equal(score); // Add more score and verify accumulation await RandomSamplingStorage.connect(signer).addToEpochNodeDelegatorScore( currentEpoch, publishingNodeIdentityId, delegatorKey, - score + score, ); - expect(await RandomSamplingStorage.getEpochNodeDelegatorScore( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.equal(score * 2n); + expect( + await RandomSamplingStorage.getEpochNodeDelegatorScore( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.equal(score * 2n); }); it('Should add to and get nodeEpochScorePerStake correctly and emit event', async () => { @@ -336,48 +389,116 @@ describe('@unit RandomSamplingStorage', function () { const expectedTotalScorePerStake = 500n; // Initial state - expect(await RandomSamplingStorage.getNodeEpochScorePerStake(currentEpoch, nodeId)) - .to.equal(0n, 'Initial nodeEpochScorePerStake should be 0'); + expect( + await RandomSamplingStorage.getNodeEpochScorePerStake( + currentEpoch, + nodeId, + ), + ).to.equal(0n, 'Initial nodeEpochScorePerStake should be 0'); // Impersonate RandomSampling for contract-only function await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Add scorePerStake and check event - await expect(RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(currentEpoch, nodeId, scorePerStakeToAdd)) + await expect( + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + currentEpoch, + nodeId, + scorePerStakeToAdd, + ), + ) .to.emit(RandomSamplingStorage, 'NodeEpochScorePerStakeUpdated') - .withArgs(currentEpoch, nodeId, expectedTotalScorePerStake); + .withArgs( + currentEpoch, + nodeId, + scorePerStakeToAdd, + expectedTotalScorePerStake, + ); // Verify stored value - expect(await RandomSamplingStorage.getNodeEpochScorePerStake(currentEpoch, nodeId)) - .to.equal(expectedTotalScorePerStake, `nodeEpochScorePerStake should be ${expectedTotalScorePerStake}`); + expect( + await RandomSamplingStorage.getNodeEpochScorePerStake( + currentEpoch, + nodeId, + ), + ).to.equal( + expectedTotalScorePerStake, + `nodeEpochScorePerStake should be ${expectedTotalScorePerStake}`, + ); // Add more and verify accumulation const anotherScorePerStakeToAdd = 300n; - const newExpectedTotalScorePerStake = expectedTotalScorePerStake + anotherScorePerStakeToAdd; - await expect(RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(currentEpoch, nodeId, anotherScorePerStakeToAdd)) + const newExpectedTotalScorePerStake = + expectedTotalScorePerStake + anotherScorePerStakeToAdd; + await expect( + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + currentEpoch, + nodeId, + anotherScorePerStakeToAdd, + ), + ) .to.emit(RandomSamplingStorage, 'NodeEpochScorePerStakeUpdated') - .withArgs(currentEpoch, nodeId, newExpectedTotalScorePerStake); - - expect(await RandomSamplingStorage.getNodeEpochScorePerStake(currentEpoch, nodeId)) - .to.equal(newExpectedTotalScorePerStake, `nodeEpochScorePerStake should be ${newExpectedTotalScorePerStake}`); + .withArgs( + currentEpoch, + nodeId, + anotherScorePerStakeToAdd, + newExpectedTotalScorePerStake, + ); + + expect( + await RandomSamplingStorage.getNodeEpochScorePerStake( + currentEpoch, + nodeId, + ), + ).to.equal( + newExpectedTotalScorePerStake, + `nodeEpochScorePerStake should be ${newExpectedTotalScorePerStake}`, + ); // Test different node const anotherNodeId = 2n; - await expect(RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(currentEpoch, anotherNodeId, scorePerStakeToAdd)) + await expect( + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + currentEpoch, + anotherNodeId, + scorePerStakeToAdd, + ), + ) .to.emit(RandomSamplingStorage, 'NodeEpochScorePerStakeUpdated') - .withArgs(currentEpoch, anotherNodeId, scorePerStakeToAdd); - expect(await RandomSamplingStorage.getNodeEpochScorePerStake(currentEpoch, anotherNodeId)) - .to.equal(scorePerStakeToAdd); - + .withArgs( + currentEpoch, + anotherNodeId, + scorePerStakeToAdd, + scorePerStakeToAdd, + ); + expect( + await RandomSamplingStorage.getNodeEpochScorePerStake( + currentEpoch, + anotherNodeId, + ), + ).to.equal(scorePerStakeToAdd); + // Test different epoch await time.increase(Number(await Chronos.epochLength())); const nextEpoch = await Chronos.getCurrentEpoch(); - await expect(RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(nextEpoch, nodeId, scorePerStakeToAdd)) + await expect( + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + nextEpoch, + nodeId, + scorePerStakeToAdd, + ), + ) .to.emit(RandomSamplingStorage, 'NodeEpochScorePerStakeUpdated') - .withArgs(nextEpoch, nodeId, scorePerStakeToAdd); - expect(await RandomSamplingStorage.getNodeEpochScorePerStake(nextEpoch, nodeId)) - .to.equal(scorePerStakeToAdd); + .withArgs(nextEpoch, nodeId, scorePerStakeToAdd, scorePerStakeToAdd); + expect( + await RandomSamplingStorage.getNodeEpochScorePerStake( + nextEpoch, + nodeId, + ), + ).to.equal(scorePerStakeToAdd); await stopImpersonate(RandomSampling); }); @@ -385,13 +506,18 @@ describe('@unit RandomSamplingStorage', function () { describe('Initialization', () => { it('Should have correct name and version', async () => { - expect(await RandomSamplingStorage.name()).to.equal('RandomSamplingStorage'); + expect(await RandomSamplingStorage.name()).to.equal( + 'RandomSamplingStorage', + ); expect(await RandomSamplingStorage.version()).to.equal('1.0.0'); }); it('Should set the initial parameters correctly', async function () { - const proofingPeriod = await RandomSamplingStorage.proofingPeriodDurations(0); - expect(proofingPeriod.durationInBlocks).to.equal(proofingPeriodDurationInBlocks); + const proofingPeriod = + await RandomSamplingStorage.proofingPeriodDurations(0); + expect(proofingPeriod.durationInBlocks).to.equal( + proofingPeriodDurationInBlocks, + ); const currentEpochTx = await Chronos.getCurrentEpoch(); const currentEpoch = BigInt(currentEpochTx.toString()); expect(proofingPeriod.effectiveEpoch).to.equal(currentEpoch); @@ -400,16 +526,18 @@ describe('@unit RandomSamplingStorage', function () { it('Should set correct Chronos reference and epoch on initialize', async () => { const currentEpoch = await Chronos.getCurrentEpoch(); await RandomSamplingStorage.initialize(); - const proofingPeriod = await RandomSamplingStorage.proofingPeriodDurations(0); + const proofingPeriod = + await RandomSamplingStorage.proofingPeriodDurations(0); expect(proofingPeriod.effectiveEpoch).to.equal(currentEpoch); }); it('Should only apply latest epoch on multiple initialize calls', async () => { const currentEpoch = await Chronos.getCurrentEpoch(); - + // First initialization await RandomSamplingStorage.initialize(); - const firstProofingPeriod = await RandomSamplingStorage.proofingPeriodDurations(0); + const firstProofingPeriod = + await RandomSamplingStorage.proofingPeriodDurations(0); expect(firstProofingPeriod.effectiveEpoch).to.equal(currentEpoch); // Move to next epoch @@ -422,44 +550,65 @@ describe('@unit RandomSamplingStorage', function () { // Second initialization await RandomSamplingStorage.initialize(); - + // Verify durations are preserved - const firstDuration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(currentEpoch); - const secondDuration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(nextEpoch); - + const firstDuration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + currentEpoch, + ); + const secondDuration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + nextEpoch, + ); + expect(firstDuration).to.equal(firstProofingPeriod.durationInBlocks); expect(secondDuration).to.be.equal(BigInt(newDuration)); }); it('Should set correct initial values', async () => { // Deploy a new instance to check initial values before initialization - const RandomSamplingStorageFactory = await hre.ethers.getContractFactory('RandomSamplingStorage'); - const newRandomSamplingStorage = await RandomSamplingStorageFactory.deploy( - Hub.target, - proofingPeriodDurationInBlocks + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', ); - + const newRandomSamplingStorage = + await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + challengeRecencyFactor, + bfsIterations, + ); + // Check initial proofing period duration - const initialDuration = await newRandomSamplingStorage.proofingPeriodDurations(0); - expect(initialDuration.durationInBlocks).to.equal(proofingPeriodDurationInBlocks); + const initialDuration = + await newRandomSamplingStorage.proofingPeriodDurations(0); + expect(initialDuration.durationInBlocks).to.equal( + proofingPeriodDurationInBlocks, + ); expect(initialDuration.effectiveEpoch).to.equal(0); // Initially should be 0 }); it('Should update effectiveEpoch to current epoch after initialize', async () => { const currentEpoch = await Chronos.getCurrentEpoch(); await RandomSamplingStorage.initialize(); - const initialDuration = await RandomSamplingStorage.proofingPeriodDurations(0); + const initialDuration = + await RandomSamplingStorage.proofingPeriodDurations(0); expect(initialDuration.effectiveEpoch).to.equal(currentEpoch); }); it('Should revert if proofingPeriodDurationInBlocks is 0', async () => { - const RandomSamplingStorageFactory = await hre.ethers.getContractFactory('RandomSamplingStorage'); + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); await expect( RandomSamplingStorageFactory.deploy( Hub.target, - 0 // proofingPeriodDurationInBlocks = 0 - ) - ).to.be.revertedWith('Proofing period duration in blocks must be greater than 0'); + 0, // proofingPeriodDurationInBlocks = 0 + challengeRecencyFactor, + bfsIterations, + ), + ).to.be.revertedWith( + 'Proofing period duration in blocks must be greater than 0', + ); }); it('Should set correct CHUNK_BYTE_SIZE constant', async () => { @@ -470,59 +619,110 @@ describe('@unit RandomSamplingStorage', function () { describe('Access Control', () => { beforeEach(async () => { // Check if RandomSampling is already registered - const currentRandomSampling = await Hub.getContractAddress('RandomSampling'); - if (currentRandomSampling !== await RandomSampling.getAddress()) { - await Hub.connect(accounts[0]).setContractAddress('RandomSampling', await RandomSampling.getAddress()); + const currentRandomSampling = + await Hub.getContractAddress('RandomSampling'); + if (currentRandomSampling !== (await RandomSampling.getAddress())) { + await Hub.connect(accounts[0]).setContractAddress( + 'RandomSampling', + await RandomSampling.getAddress(), + ); } }); it('Should revert contact call if not called by Hub', async () => { await expect(RandomSamplingStorage.connect(accounts[1]).initialize()) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Hub'); }); it('Should revert contact call on onlyContract modifiers', async () => { await expect( - RandomSamplingStorage.connect(accounts[1]).replacePendingProofingPeriodDuration(0, 0) + RandomSamplingStorage.connect( + accounts[1], + ).replacePendingProofingPeriodDuration(0, 0), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); await expect( - RandomSamplingStorage.connect(accounts[1]).addProofingPeriodDuration(0, 0) + RandomSamplingStorage.connect(accounts[1]).addProofingPeriodDuration( + 0, + 0, + ), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); await expect( - RandomSamplingStorage.connect(accounts[1]).setNodeChallenge(0, MockChallenge) + RandomSamplingStorage.connect(accounts[1]).setNodeChallenge( + 0, + MockChallenge, + ), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); await expect( - RandomSamplingStorage.connect(accounts[1]).incrementEpochNodeValidProofsCount(0, 0) + RandomSamplingStorage.connect( + accounts[1], + ).incrementEpochNodeValidProofsCount(0, 0), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); await expect( - RandomSamplingStorage.connect(accounts[1]).addToNodeEpochProofPeriodScore(0, 0, 0, 0) + RandomSamplingStorage.connect( + accounts[1], + ).addToNodeEpochProofPeriodScore(0, 0, 0, 0), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); await expect( - RandomSamplingStorage.connect(accounts[1]).addToNodeEpochScorePerStake(0, 0, 0) + RandomSamplingStorage.connect(accounts[1]).addToNodeEpochScorePerStake( + 0, + 0, + 0, + ), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); - + await expect( - RandomSamplingStorage.connect(accounts[1]).setDelegatorLastSettledNodeEpochScorePerStake(0, 0, ethers.encodeBytes32String('0'), 0) + RandomSamplingStorage.connect( + accounts[1], + ).setDelegatorLastSettledNodeEpochScorePerStake( + 0, + 0, + ethers.encodeBytes32String('0'), + 0, + ), ) - .to.be.revertedWithCustomError(RandomSamplingStorage, 'UnauthorizedAccess') + .to.be.revertedWithCustomError( + RandomSamplingStorage, + 'UnauthorizedAccess', + ) .withArgs('Only Contracts in Hub'); }); @@ -531,47 +731,66 @@ describe('@unit RandomSamplingStorage', function () { await impersonateAndFund(Hub); const hubSigner = await ethers.getSigner(await Hub.getAddress()); - await expect( - RandomSamplingStorage.connect(hubSigner).initialize() - ).to.not.be.reverted; + await expect(RandomSamplingStorage.connect(hubSigner).initialize()).to.not + .be.reverted; await stopImpersonate(Hub); // Test contract-only functions with RandomSampling await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); await expect( - RandomSamplingStorage.connect(rsSigner).setNodeChallenge(0, MockChallenge) + RandomSamplingStorage.connect(rsSigner).setNodeChallenge( + 0, + MockChallenge, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).replacePendingProofingPeriodDuration(0, 0) + RandomSamplingStorage.connect( + rsSigner, + ).replacePendingProofingPeriodDuration(0, 0), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(0, 0) + RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(0, 0), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).incrementEpochNodeValidProofsCount(1, 1) + RandomSamplingStorage.connect( + rsSigner, + ).incrementEpochNodeValidProofsCount(1, 1), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore(1, 1, 1, 1000) + RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore( + 1, + 1, + 1, + 1000, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(1, 1, 1000) + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + 1, + 1, + 1000, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( 1, 1, ethers.encodeBytes32String('test'), - 1000 - ) + 1000, + ), ).to.not.be.reverted; await stopImpersonate(RandomSampling); @@ -580,39 +799,62 @@ describe('@unit RandomSamplingStorage', function () { it('Should allow access when called by registered contract', async () => { // Test contract-only functions with RandomSampling await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); await expect( - RandomSamplingStorage.connect(rsSigner).replacePendingProofingPeriodDuration(1000, 1) + RandomSamplingStorage.connect( + rsSigner, + ).replacePendingProofingPeriodDuration(1000, 1), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(1000, 1) + RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration( + 1000, + 1, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).setNodeChallenge(1, MockChallenge) + RandomSamplingStorage.connect(rsSigner).setNodeChallenge( + 1, + MockChallenge, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).incrementEpochNodeValidProofsCount(1, 1) + RandomSamplingStorage.connect( + rsSigner, + ).incrementEpochNodeValidProofsCount(1, 1), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore(1, 1, 1, 1000) + RandomSamplingStorage.connect(rsSigner).addToNodeEpochProofPeriodScore( + 1, + 1, + 1, + 1000, + ), ).to.not.be.reverted; await expect( - RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake(1, 1, 1000) + RandomSamplingStorage.connect(rsSigner).addToNodeEpochScorePerStake( + 1, + 1, + 1000, + ), ).to.not.be.reverted; - + await expect( - RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( 1, 1, ethers.encodeBytes32String('test'), - 1000 - ) + 1000, + ), ).to.not.be.reverted; await stopImpersonate(RandomSampling); @@ -621,139 +863,204 @@ describe('@unit RandomSamplingStorage', function () { describe('Proofing Period Management', () => { it('Should return the correct proofing period status', async () => { - const { activeProofPeriodStartBlock } = await updateAndGetActiveProofPeriod(); - const duration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + const { activeProofPeriodStartBlock } = + await updateAndGetActiveProofPeriod(); + const duration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); // Initial check const status = await RandomSamplingStorage.getActiveProofPeriodStatus(); expect(status.activeProofPeriodStartBlock).to.be.a('bigint'); expect(status.isValid).to.be.a('boolean'); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(status.isValid).to.be.true; // Test at middle of period - const middleBlock = activeProofPeriodStartBlock + (duration / 2n); - await mineBlocks(Number(middleBlock - BigInt(await hre.ethers.provider.getBlockNumber()))); - const middleStatus = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const middleBlock = activeProofPeriodStartBlock + duration / 2n; + await mineBlocks( + Number( + middleBlock - BigInt(await hre.ethers.provider.getBlockNumber()), + ), + ); + const middleStatus = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(middleStatus.isValid).to.be.true; // Test at end of period const endBlock = activeProofPeriodStartBlock + duration - 1n; - await mineBlocks(Number(endBlock - BigInt(await hre.ethers.provider.getBlockNumber()))); - const endStatus = await RandomSamplingStorage.getActiveProofPeriodStatus(); + await mineBlocks( + Number(endBlock - BigInt(await hre.ethers.provider.getBlockNumber())), + ); + const endStatus = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(endStatus.isValid).to.be.true; // Test after period ends await mineBlocks(1); - const afterStatus = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const afterStatus = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(afterStatus.isValid).to.be.false; }); it('Should update start block correctly for different period scenarios', async () => { // Test when no period has passed - const { activeProofPeriodStartBlock: initialBlock } = await updateAndGetActiveProofPeriod(); - const statusNoPeriod = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const { activeProofPeriodStartBlock: initialBlock } = + await updateAndGetActiveProofPeriod(); + const statusNoPeriod = + await RandomSamplingStorage.getActiveProofPeriodStatus(); expect(statusNoPeriod.activeProofPeriodStartBlock).to.equal(initialBlock); // Test when 1 full period has passed - const duration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + const duration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); await mineBlocks(Number(duration)); - const { activeProofPeriodStartBlock: onePeriodBlock } = await updateAndGetActiveProofPeriod(); + const { activeProofPeriodStartBlock: onePeriodBlock } = + await updateAndGetActiveProofPeriod(); expect(onePeriodBlock).to.equal(initialBlock + duration); // Test when 2 full periods have passed await mineBlocks(Number(duration)); - const { activeProofPeriodStartBlock: twoPeriodBlock } = await updateAndGetActiveProofPeriod(); - expect(twoPeriodBlock).to.equal(initialBlock + (duration * 2n)); + const { activeProofPeriodStartBlock: twoPeriodBlock } = + await updateAndGetActiveProofPeriod(); + expect(twoPeriodBlock).to.equal(initialBlock + duration * 2n); // Test when n full periods have passed (using n=5 as example) const n = 5; for (let i = 0; i < n - 2; i++) { await mineBlocks(Number(duration)); } - const { activeProofPeriodStartBlock: nPeriodBlock } = await updateAndGetActiveProofPeriod(); - expect(nPeriodBlock).to.equal(initialBlock + (duration * BigInt(n))); + const { activeProofPeriodStartBlock: nPeriodBlock } = + await updateAndGetActiveProofPeriod(); + expect(nPeriodBlock).to.equal(initialBlock + duration * BigInt(n)); }); it('Should return correct historical proofing period start', async () => { - const { activeProofPeriodStartBlock } = await updateAndGetActiveProofPeriod(); - const duration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + const { activeProofPeriodStartBlock } = + await updateAndGetActiveProofPeriod(); + const duration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); // Test invalid inputs await expect( - RandomSamplingStorage.getHistoricalProofPeriodStartBlock(0, 1) + RandomSamplingStorage.getHistoricalProofPeriodStartBlock(0, 1), ).to.be.revertedWith('Proof period start block must be greater than 0'); - + await expect( - RandomSamplingStorage.getHistoricalProofPeriodStartBlock(100, 0) + RandomSamplingStorage.getHistoricalProofPeriodStartBlock(100, 0), ).to.be.revertedWith('Offset must be greater than 0'); - + await expect( - RandomSamplingStorage.getHistoricalProofPeriodStartBlock(activeProofPeriodStartBlock + 10n, 1) + RandomSamplingStorage.getHistoricalProofPeriodStartBlock( + activeProofPeriodStartBlock + 10n, + 1, + ), ).to.be.revertedWith('Proof period start block is not valid'); - + await expect( - RandomSamplingStorage.getHistoricalProofPeriodStartBlock(activeProofPeriodStartBlock, 999) + RandomSamplingStorage.getHistoricalProofPeriodStartBlock( + activeProofPeriodStartBlock, + 999, + ), ).to.be.revertedWithPanic(PANIC_ARITHMETIC_OVERFLOW); // Test valid historical blocks - await mineProofPeriodBlocks(activeProofPeriodStartBlock, RandomSamplingStorage); - const { activeProofPeriodStartBlock: newPeriodStartBlock } = await updateAndGetActiveProofPeriod(); + await mineProofPeriodBlocks( + activeProofPeriodStartBlock, + RandomSamplingStorage, + ); + const { activeProofPeriodStartBlock: newPeriodStartBlock } = + await updateAndGetActiveProofPeriod(); // Test offset 1 - const onePeriodBack = await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( - newPeriodStartBlock, - 1 - ); + const onePeriodBack = + await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( + newPeriodStartBlock, + 1, + ); expect(onePeriodBack).to.equal(newPeriodStartBlock - duration); // Test offset 2 - const twoPeriodsBack = await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( - newPeriodStartBlock, - 2 - ); - expect(twoPeriodsBack).to.equal(newPeriodStartBlock - (duration * 2n)); + const twoPeriodsBack = + await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( + newPeriodStartBlock, + 2, + ); + expect(twoPeriodsBack).to.equal(newPeriodStartBlock - duration * 2n); // Test offset 3 - const threePeriodsBack = await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( - newPeriodStartBlock, - 3 - ); - expect(threePeriodsBack).to.equal(newPeriodStartBlock - (duration * 3n)); + const threePeriodsBack = + await RandomSamplingStorage.getHistoricalProofPeriodStartBlock( + newPeriodStartBlock, + 3, + ); + expect(threePeriodsBack).to.equal(newPeriodStartBlock - duration * 3n); // Test that returned block is aligned with period start - expect(threePeriodsBack % duration).to.equal(0n, 'Historical block should be aligned with period start'); + expect(threePeriodsBack % duration).to.equal( + 0n, + 'Historical block should be aligned with period start', + ); }); it('Should return correct active proof period', async () => { - const { activeProofPeriodStartBlock, isValid } = await updateAndGetActiveProofPeriod(); + const { activeProofPeriodStartBlock, isValid } = + await updateAndGetActiveProofPeriod(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(isValid).to.be.equal(true, 'Period should be valid'); // Mine blocks up to the last block of the current period const currentBlock = await hre.ethers.provider.getBlockNumber(); - const blocksToMine = Number(activeProofPeriodStartBlock) + Number(proofingPeriodDurationInBlocks) - currentBlock - 1; + const blocksToMine = + Number(activeProofPeriodStartBlock) + + Number(proofingPeriodDurationInBlocks) - + currentBlock - + 1; await mineBlocks(blocksToMine); - - let statusAfterUpdate = await RandomSamplingStorage.getActiveProofPeriodStatus(); - expect(statusAfterUpdate.isValid).to.be.equal(true, 'Period should still be valid'); + + let statusAfterUpdate = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(statusAfterUpdate.isValid).to.be.equal( + true, + 'Period should still be valid', + ); // Mine one more block to reach the end of the period await mineBlocks(1); - statusAfterUpdate = await RandomSamplingStorage.getActiveProofPeriodStatus(); - expect(statusAfterUpdate.isValid).to.be.equal(false, 'Period should not be valid'); + statusAfterUpdate = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(statusAfterUpdate.isValid).to.be.equal( + false, + 'Period should not be valid', + ); // Update the period and mine blocks for the new period await updateAndGetActiveProofPeriod(); - const newStatus = await RandomSamplingStorage.getActiveProofPeriodStatus(); - const blocksToMineNew = Number(newStatus.activeProofPeriodStartBlock) + Number(proofingPeriodDurationInBlocks) - (await hre.ethers.provider.getBlockNumber()) - 1; + const newStatus = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + const blocksToMineNew = + Number(newStatus.activeProofPeriodStartBlock) + + Number(proofingPeriodDurationInBlocks) - + (await hre.ethers.provider.getBlockNumber()) - + 1; await mineBlocks(blocksToMineNew); - - statusAfterUpdate = await RandomSamplingStorage.getActiveProofPeriodStatus(); - expect(statusAfterUpdate.isValid).to.be.equal(true, 'New period should be valid'); + + statusAfterUpdate = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(statusAfterUpdate.isValid).to.be.equal( + true, + 'New period should be valid', + ); }); it('Should pick correct proofing period duration based on epoch', async () => { - const initialDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - const currentEpoch = await Chronos.getCurrentEpoch(); + const initialDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); const epochLength = await Chronos.epochLength(); // Test initial duration @@ -761,72 +1068,100 @@ describe('@unit RandomSamplingStorage', function () { // Test duration in middle of epoch await time.increase(Number(epochLength) / 2); - const midEpochDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - expect(midEpochDuration).to.equal(initialDuration, 'Duration should not change mid-epoch'); + const midEpochDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + expect(midEpochDuration).to.equal( + initialDuration, + 'Duration should not change mid-epoch', + ); // Impersonate RandomSampling await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); // Set new duration for next epoch const newDuration = 1000; - await RandomSampling.connect(rsSigner).setProofingPeriodDurationInBlocks(newDuration); + await RandomSampling.connect(rsSigner).setProofingPeriodDurationInBlocks( + newDuration, + ); // Stop impersonating RandomSampling await hre.network.provider.request({ method: 'hardhat_stopImpersonatingAccount', params: [await RandomSampling.getAddress()], }); - + // Verify duration hasn't changed yet - const beforeEpochEndDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - expect(beforeEpochEndDuration).to.equal(initialDuration, 'Duration should not change before epoch end'); + const beforeEpochEndDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + expect(beforeEpochEndDuration).to.equal( + initialDuration, + 'Duration should not change before epoch end', + ); // Move to next epoch await time.increase(Number(epochLength) + 1); - const nextEpochDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - expect(nextEpochDuration).to.equal(BigInt(newDuration), 'Duration should change in next epoch'); + const nextEpochDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + expect(nextEpochDuration).to.equal( + BigInt(newDuration), + 'Duration should change in next epoch', + ); // Impersonate RandomSampling again await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner2 = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner2 = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); // Set another duration for future epoch const futureDuration = 2000; - await RandomSampling.connect(rsSigner2).setProofingPeriodDurationInBlocks(futureDuration); + await RandomSampling.connect(rsSigner2).setProofingPeriodDurationInBlocks( + futureDuration, + ); // Stop impersonating RandomSampling await hre.network.provider.request({ method: 'hardhat_stopImpersonatingAccount', params: [await RandomSampling.getAddress()], }); - + // Verify current epoch still has previous duration - const currentEpochDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - expect(currentEpochDuration).to.equal(BigInt(newDuration), 'Current epoch should keep previous duration'); + const currentEpochDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + expect(currentEpochDuration).to.equal( + BigInt(newDuration), + 'Current epoch should keep previous duration', + ); // Move to future epoch await time.increase(Number(epochLength)); - const futureEpochDuration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); - expect(futureEpochDuration).to.equal(BigInt(futureDuration), 'Future epoch should have new duration'); + const futureEpochDuration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + expect(futureEpochDuration).to.equal( + BigInt(futureDuration), + 'Future epoch should have new duration', + ); }); it('Should return correct proofing period duration based on epoch history', async () => { @@ -838,7 +1173,7 @@ describe('@unit RandomSamplingStorage', function () { // Set up multiple durations with different effective epochs const durations = []; for (let i = 0; i < testEpochs; i++) { - const duration = baseDuration + (i * 100); + const duration = baseDuration + i * 100; durations.push(duration); // Impersonate RandomSampling @@ -846,15 +1181,19 @@ describe('@unit RandomSamplingStorage', function () { method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); - await RandomSampling.connect(rsSigner).setProofingPeriodDurationInBlocks(duration); + await RandomSampling.connect( + rsSigner, + ).setProofingPeriodDurationInBlocks(duration); // Stop impersonating RandomSampling await hre.network.provider.request({ @@ -870,81 +1209,127 @@ describe('@unit RandomSamplingStorage', function () { // Test invalid epoch (before first duration) await expect( - RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(currentEpoch - 1n) + RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + currentEpoch - 1n, + ), ).to.be.revertedWith('No applicable duration found'); // Test each epoch's duration for (let i = 0; i < testEpochs; i++) { const targetEpoch = finalEpoch - BigInt(i); const expectedDuration = durations[testEpochs - 1 - i]; - - const actual = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(targetEpoch); - expect(actual).to.equal(expectedDuration, - `Epoch ${targetEpoch} should have duration ${expectedDuration}`); + + const actual = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + targetEpoch, + ); + expect(actual).to.equal( + expectedDuration, + `Epoch ${targetEpoch} should have duration ${expectedDuration}`, + ); } // Test edge case - current epoch - const currentEpochDuration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(finalEpoch); - expect(currentEpochDuration).to.equal(durations[durations.length - 1], - 'Current epoch should have the latest duration'); + const currentEpochDuration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + finalEpoch, + ); + expect(currentEpochDuration).to.equal( + durations[durations.length - 1], + 'Current epoch should have the latest duration', + ); // Test edge case - first epoch with duration const firstEpochWithDuration = currentEpoch; - const firstEpochDuration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(firstEpochWithDuration); - expect(firstEpochDuration).to.equal(durations[0], - 'First epoch should have the first duration'); + const firstEpochDuration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + firstEpochWithDuration, + ); + expect(firstEpochDuration).to.equal( + durations[0], + 'First epoch should have the first duration', + ); }); it('Should return same block when no period has passed', async () => { - const { activeProofPeriodStartBlock: initialBlock } = await updateAndGetActiveProofPeriod(); - + const { activeProofPeriodStartBlock: initialBlock } = + await updateAndGetActiveProofPeriod(); + // Mine blocks up to the last block of the current period const currentBlock = await hre.ethers.provider.getBlockNumber(); - const blocksToMine = Number(initialBlock) + Number(proofingPeriodDurationInBlocks) - currentBlock - 2; + const blocksToMine = + Number(initialBlock) + + Number(proofingPeriodDurationInBlocks) - + currentBlock - + 2; await mineBlocks(blocksToMine); - - const tx = await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); + + const tx = + await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); await tx.wait(); - const { activeProofPeriodStartBlock: newBlock } = await RandomSamplingStorage.getActiveProofPeriodStatus(); - + const { activeProofPeriodStartBlock: newBlock } = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // Should return the same block since we haven't reached the end of the period expect(newBlock).to.equal(initialBlock); // Mine one more block to reach the end of the period await mineBlocks(1); - - const tx2 = await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); + + const tx2 = + await RandomSamplingStorage.updateAndGetActiveProofPeriodStartBlock(); await tx2.wait(); - const { activeProofPeriodStartBlock: finalBlock } = await RandomSamplingStorage.getActiveProofPeriodStatus(); - + const { activeProofPeriodStartBlock: finalBlock } = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // Should update the block since we've reached the end of the period expect(finalBlock).to.be.greaterThan(initialBlock); }); it('Should return correct status for different block numbers', async () => { - const { activeProofPeriodStartBlock } = await updateAndGetActiveProofPeriod(); - const duration = await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); + const { activeProofPeriodStartBlock } = + await updateAndGetActiveProofPeriod(); + const duration = + await RandomSamplingStorage.getActiveProofingPeriodDurationInBlocks(); // Test at start block - const statusAtStart = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const statusAtStart = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(statusAtStart.isValid).to.be.true; - expect(statusAtStart.activeProofPeriodStartBlock).to.equal(activeProofPeriodStartBlock); + expect(statusAtStart.activeProofPeriodStartBlock).to.equal( + activeProofPeriodStartBlock, + ); // Test at middle block - const middleBlock = activeProofPeriodStartBlock + (duration / 2n); - await mineBlocks(Number(middleBlock - BigInt(await hre.ethers.provider.getBlockNumber()))); - const statusAtMiddle = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const middleBlock = activeProofPeriodStartBlock + duration / 2n; + await mineBlocks( + Number( + middleBlock - BigInt(await hre.ethers.provider.getBlockNumber()), + ), + ); + const statusAtMiddle = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(statusAtMiddle.isValid).to.be.true; // Test at last valid block const lastValidBlock = activeProofPeriodStartBlock + duration - 1n; - await mineBlocks(Number(lastValidBlock - BigInt(await hre.ethers.provider.getBlockNumber()))); - const statusAtLastValid = await RandomSamplingStorage.getActiveProofPeriodStatus(); + await mineBlocks( + Number( + lastValidBlock - BigInt(await hre.ethers.provider.getBlockNumber()), + ), + ); + const statusAtLastValid = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(statusAtLastValid.isValid).to.be.true; // Test at first invalid block await mineBlocks(1); - const statusAtInvalid = await RandomSamplingStorage.getActiveProofPeriodStatus(); + const statusAtInvalid = + await RandomSamplingStorage.getActiveProofPeriodStatus(); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(statusAtInvalid.isValid).to.be.false; }); }); @@ -956,22 +1341,26 @@ describe('@unit RandomSamplingStorage', function () { const signer = await ethers.getSigner(accounts[0].address); await RandomSamplingStorage.connect(signer).setNodeChallenge( publishingNodeIdentityId, - MockChallenge + MockChallenge, ); - const challenge = await RandomSamplingStorage.getNodeChallenge(publishingNodeIdentityId); + const challenge = await RandomSamplingStorage.getNodeChallenge( + publishingNodeIdentityId, + ); - expect(challenge.knowledgeCollectionId).to.be.equal(MockChallenge.knowledgeCollectionId); + expect(challenge.knowledgeCollectionId).to.be.equal( + MockChallenge.knowledgeCollectionId, + ); expect(challenge.chunkId).to.be.equal(MockChallenge.chunkId); expect(challenge.epoch).to.be.equal(MockChallenge.epoch); expect(challenge.proofingPeriodDurationInBlocks).to.be.equal( - MockChallenge.proofingPeriodDurationInBlocks + MockChallenge.proofingPeriodDurationInBlocks, ); expect(challenge.activeProofPeriodStartBlock).to.be.equal( - MockChallenge.activeProofPeriodStartBlock + MockChallenge.activeProofPeriodStartBlock, ); expect(challenge.proofingPeriodDurationInBlocks).to.be.equal( - MockChallenge.proofingPeriodDurationInBlocks + MockChallenge.proofingPeriodDurationInBlocks, ); expect(challenge.solved).to.be.equal(MockChallenge.solved); }); @@ -982,35 +1371,49 @@ describe('@unit RandomSamplingStorage', function () { const signer = await ethers.getSigner(accounts[0].address); // Test initial state - const initialChallenge = await RandomSamplingStorage.getNodeChallenge(publishingNodeIdentityId); + const initialChallenge = await RandomSamplingStorage.getNodeChallenge( + publishingNodeIdentityId, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(initialChallenge.solved).to.be.false; expect(initialChallenge.knowledgeCollectionId).to.be.equal(0n); // Set first challenge await RandomSamplingStorage.connect(signer).setNodeChallenge( publishingNodeIdentityId, - MockChallenge + MockChallenge, ); // Verify first challenge - const firstChallenge = await RandomSamplingStorage.getNodeChallenge(publishingNodeIdentityId); - expect(firstChallenge.knowledgeCollectionId).to.be.equal(MockChallenge.knowledgeCollectionId); + const firstChallenge = await RandomSamplingStorage.getNodeChallenge( + publishingNodeIdentityId, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(firstChallenge.knowledgeCollectionId).to.be.equal( + MockChallenge.knowledgeCollectionId, + ); expect(firstChallenge.solved).to.be.equal(MockChallenge.solved); // Create and set second challenge const secondChallenge = { ...MockChallenge, knowledgeCollectionId: BigInt(MockChallenge.knowledgeCollectionId) + 1n, - solved: true + solved: true, }; await RandomSamplingStorage.connect(signer).setNodeChallenge( publishingNodeIdentityId, - secondChallenge + secondChallenge, ); // Verify second challenge overwrote first - const finalChallenge = await RandomSamplingStorage.getNodeChallenge(publishingNodeIdentityId); - expect(finalChallenge.knowledgeCollectionId).to.be.equal(secondChallenge.knowledgeCollectionId); + const finalChallenge = await RandomSamplingStorage.getNodeChallenge( + publishingNodeIdentityId, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(finalChallenge.knowledgeCollectionId).to.be.equal( + secondChallenge.knowledgeCollectionId, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(finalChallenge.solved).to.be.true; expect(finalChallenge.chunkId).to.be.equal(secondChallenge.chunkId); }); @@ -1021,28 +1424,41 @@ describe('@unit RandomSamplingStorage', function () { const currentEpoch = await Chronos.getCurrentEpoch(); // Initially should be false - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.false; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.false; // Impersonate RandomSampling await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); // Add a new duration - await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(1000, currentEpoch + 1n); - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.true; + await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration( + 1000, + currentEpoch + 1n, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.true; // Replace pending duration - await RandomSamplingStorage.connect(rsSigner).replacePendingProofingPeriodDuration(2000, currentEpoch + 1n); - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.true; + await RandomSamplingStorage.connect( + rsSigner, + ).replacePendingProofingPeriodDuration(2000, currentEpoch + 1n); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.true; // Stop impersonating RandomSampling await hre.network.provider.request({ @@ -1052,7 +1468,9 @@ describe('@unit RandomSamplingStorage', function () { // Move to next epoch await time.increase(Number(await Chronos.epochLength())); - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.false; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.false; }); it('Should handle multiple proofing period durations correctly', async () => { @@ -1063,12 +1481,14 @@ describe('@unit RandomSamplingStorage', function () { method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); // Add multiple durations @@ -1076,9 +1496,11 @@ describe('@unit RandomSamplingStorage', function () { for (let i = 0; i < durations.length; i++) { await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration( durations[i], - currentEpoch + BigInt(i + 1) + currentEpoch + BigInt(i + 1), ); - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.true; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.true; } // Stop impersonating RandomSampling @@ -1090,7 +1512,10 @@ describe('@unit RandomSamplingStorage', function () { // Verify durations are set correctly for (let i = 0; i < durations.length; i++) { const epoch = currentEpoch + BigInt(i + 1); - const duration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(epoch); + const duration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + epoch, + ); expect(duration).to.equal(BigInt(durations[i])); } }); @@ -1103,24 +1528,30 @@ describe('@unit RandomSamplingStorage', function () { method: 'hardhat_impersonateAccount', params: [await RandomSampling.getAddress()], }); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Fund the RandomSampling contract - await hre.network.provider.send("hardhat_setBalance", [ + await hre.network.provider.send('hardhat_setBalance', [ await RandomSampling.getAddress(), - "0x56BC75E2D63100000" // 100 ETH in hex + '0x56BC75E2D63100000', // 100 ETH in hex ]); // Add initial duration - await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(1000, currentEpoch + 1n); - expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to.be.true; + await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration( + 1000, + currentEpoch + 1n, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(await RandomSamplingStorage.isPendingProofingPeriodDuration()).to + .be.true; // Replace with new duration const newDuration = 2000; - await RandomSamplingStorage.connect(rsSigner).replacePendingProofingPeriodDuration( - newDuration, - currentEpoch + 1n - ); + await RandomSamplingStorage.connect( + rsSigner, + ).replacePendingProofingPeriodDuration(newDuration, currentEpoch + 1n); // Stop impersonating RandomSampling await hre.network.provider.request({ @@ -1129,29 +1560,39 @@ describe('@unit RandomSamplingStorage', function () { }); // Verify new duration is set - const duration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(currentEpoch + 1n); + const duration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + currentEpoch + 1n, + ); expect(duration).to.equal(BigInt(newDuration)); }); it('Should emit ProofingPeriodDurationAdded event with correct parameters', async () => { const newDuration = 1000; - const effectiveEpoch = await Chronos.getCurrentEpoch() + 1n; - + const effectiveEpoch = (await Chronos.getCurrentEpoch()) + 1n; + // Impersonate RandomSampling await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Add new proofing period duration and capture the event - const tx = await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(newDuration, effectiveEpoch); + const tx = await RandomSamplingStorage.connect( + rsSigner, + ).addProofingPeriodDuration(newDuration, effectiveEpoch); const receipt = await tx.wait(); - + // Find the ProofingPeriodDurationAdded event const event = receipt?.logs.find( - log => (log as EventLog).fragment?.name === 'ProofingPeriodDurationAdded' + (log) => + (log as EventLog).fragment?.name === 'ProofingPeriodDurationAdded', ) as EventLog; - + // Verify event parameters + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(event).to.not.be.undefined; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(event?.args).to.not.be.undefined; expect(event?.args[0]).to.equal(newDuration); expect(event?.args[1]).to.equal(effectiveEpoch); @@ -1162,26 +1603,37 @@ describe('@unit RandomSamplingStorage', function () { it('Should emit PendingProofingPeriodDurationReplaced event with correct parameters', async () => { const oldDuration = 1000; const newDuration = 2000; - const effectiveEpoch = await Chronos.getCurrentEpoch() + 1n; - + const effectiveEpoch = (await Chronos.getCurrentEpoch()) + 1n; + // Impersonate RandomSampling await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // First add a duration - await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration(oldDuration, effectiveEpoch); - + await RandomSamplingStorage.connect(rsSigner).addProofingPeriodDuration( + oldDuration, + effectiveEpoch, + ); + // Then replace it and capture the event - const tx = await RandomSamplingStorage.connect(rsSigner).replacePendingProofingPeriodDuration(newDuration, effectiveEpoch); + const tx = await RandomSamplingStorage.connect( + rsSigner, + ).replacePendingProofingPeriodDuration(newDuration, effectiveEpoch); const receipt = await tx.wait(); - + // Find the PendingProofingPeriodDurationReplaced event const event = receipt?.logs.find( - log => (log as EventLog).fragment?.name === 'PendingProofingPeriodDurationReplaced' + (log) => + (log as EventLog).fragment?.name === + 'PendingProofingPeriodDurationReplaced', ) as EventLog; - + // Verify event parameters + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(event).to.not.be.undefined; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(event?.args).to.not.be.undefined; expect(event?.args[0]).to.equal(oldDuration); expect(event?.args[1]).to.equal(newDuration); @@ -1199,41 +1651,55 @@ describe('@unit RandomSamplingStorage', function () { const delegatorKey = ethers.encodeBytes32String('delegator1'); // Initially should be false - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.be.false; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.be.false; // Set as claimed - await RandomSamplingStorage.connect(signer).setEpochNodeDelegatorRewardsClaimed( + await RandomSamplingStorage.connect( + signer, + ).setEpochNodeDelegatorRewardsClaimed( currentEpoch, publishingNodeIdentityId, delegatorKey, - true + true, ); // Verify claimed status - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.be.true; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.be.true; // Set as not claimed - await RandomSamplingStorage.connect(signer).setEpochNodeDelegatorRewardsClaimed( + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + await RandomSamplingStorage.connect( + signer, + ).setEpochNodeDelegatorRewardsClaimed( currentEpoch, publishingNodeIdentityId, delegatorKey, - false + false, ); // Verify not claimed status - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.be.false; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.be.false; }); it('Should handle multiple delegators rewards claimed status', async () => { @@ -1243,25 +1709,29 @@ describe('@unit RandomSamplingStorage', function () { const delegatorKeys = [ ethers.encodeBytes32String('delegator1'), ethers.encodeBytes32String('delegator2'), - ethers.encodeBytes32String('delegator3') + ethers.encodeBytes32String('delegator3'), ]; // Set different statuses for different delegators for (let i = 0; i < delegatorKeys.length; i++) { const claimed = i % 2 === 0; // Alternate between true and false - await RandomSamplingStorage.connect(signer).setEpochNodeDelegatorRewardsClaimed( + await RandomSamplingStorage.connect( + signer, + ).setEpochNodeDelegatorRewardsClaimed( currentEpoch, publishingNodeIdentityId, delegatorKeys[i], - claimed + claimed, ); // Verify status - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - currentEpoch, - publishingNodeIdentityId, - delegatorKeys[i] - )).to.equal(claimed); + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + currentEpoch, + publishingNodeIdentityId, + delegatorKeys[i], + ), + ).to.equal(claimed); } }); @@ -1272,11 +1742,13 @@ describe('@unit RandomSamplingStorage', function () { const delegatorKey = ethers.encodeBytes32String('delegator1'); // Set claimed status for current epoch - await RandomSamplingStorage.connect(signer).setEpochNodeDelegatorRewardsClaimed( + await RandomSamplingStorage.connect( + signer, + ).setEpochNodeDelegatorRewardsClaimed( currentEpoch, publishingNodeIdentityId, delegatorKey, - true + true, ); // Move to next epoch @@ -1284,18 +1756,24 @@ describe('@unit RandomSamplingStorage', function () { const nextEpoch = await Chronos.getCurrentEpoch(); // Verify current epoch is still claimed - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - currentEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.be.true; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + currentEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.be.true; // Verify next epoch is not claimed - expect(await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( - nextEpoch, - publishingNodeIdentityId, - delegatorKey - )).to.be.false; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect( + await RandomSamplingStorage.getEpochNodeDelegatorRewardsClaimed( + nextEpoch, + publishingNodeIdentityId, + delegatorKey, + ), + ).to.be.false; }); }); @@ -1308,54 +1786,165 @@ describe('@unit RandomSamplingStorage', function () { // Impersonate RandomSampling for contract-only function await impersonateAndFund(RandomSampling); - const rsSigner = await ethers.getSigner(await RandomSampling.getAddress()); + const rsSigner = await ethers.getSigner( + await RandomSampling.getAddress(), + ); // Initial state - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, delegatorKey)) - .to.equal(0n, 'Initial delegatorLastSettledNodeEpochScorePerStake should be 0'); + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + delegatorKey, + ), + ).to.equal( + 0n, + 'Initial delegatorLastSettledNodeEpochScorePerStake should be 0', + ); // Set scorePerStake and check event - await expect(RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, delegatorKey, scorePerStakeToSet)) - .to.emit(RandomSamplingStorage, 'DelegatorLastSettledNodeEpochScorePerStakeUpdated') + await expect( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + delegatorKey, + scorePerStakeToSet, + ), + ) + .to.emit( + RandomSamplingStorage, + 'DelegatorLastSettledNodeEpochScorePerStakeUpdated', + ) .withArgs(currentEpoch, nodeId, delegatorKey, scorePerStakeToSet); // Verify stored value - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, delegatorKey)) - .to.equal(scorePerStakeToSet, `delegatorLastSettledNodeEpochScorePerStake should be ${scorePerStakeToSet}`); + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + delegatorKey, + ), + ).to.equal( + scorePerStakeToSet, + `delegatorLastSettledNodeEpochScorePerStake should be ${scorePerStakeToSet}`, + ); // Set again to test overwrite const newScorePerStakeToSet = 54321n; - await expect(RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, delegatorKey, newScorePerStakeToSet)) - .to.emit(RandomSamplingStorage, 'DelegatorLastSettledNodeEpochScorePerStakeUpdated') + await expect( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + delegatorKey, + newScorePerStakeToSet, + ), + ) + .to.emit( + RandomSamplingStorage, + 'DelegatorLastSettledNodeEpochScorePerStakeUpdated', + ) .withArgs(currentEpoch, nodeId, delegatorKey, newScorePerStakeToSet); - - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, delegatorKey)) - .to.equal(newScorePerStakeToSet, `delegatorLastSettledNodeEpochScorePerStake should be ${newScorePerStakeToSet} after overwrite`); + + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + delegatorKey, + ), + ).to.equal( + newScorePerStakeToSet, + `delegatorLastSettledNodeEpochScorePerStake should be ${newScorePerStakeToSet} after overwrite`, + ); // Test different delegatorKey const anotherDelegatorKey = ethers.encodeBytes32String('delegatorTest2'); - await expect(RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, anotherDelegatorKey, scorePerStakeToSet)) - .to.emit(RandomSamplingStorage, 'DelegatorLastSettledNodeEpochScorePerStakeUpdated') - .withArgs(currentEpoch, nodeId, anotherDelegatorKey, scorePerStakeToSet); - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, nodeId, anotherDelegatorKey)) - .to.equal(scorePerStakeToSet); - + await expect( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + anotherDelegatorKey, + scorePerStakeToSet, + ), + ) + .to.emit( + RandomSamplingStorage, + 'DelegatorLastSettledNodeEpochScorePerStakeUpdated', + ) + .withArgs( + currentEpoch, + nodeId, + anotherDelegatorKey, + scorePerStakeToSet, + ); + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + nodeId, + anotherDelegatorKey, + ), + ).to.equal(scorePerStakeToSet); + // Test different node const anotherNodeId = 2n; - await expect(RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, anotherNodeId, delegatorKey, scorePerStakeToSet)) - .to.emit(RandomSamplingStorage, 'DelegatorLastSettledNodeEpochScorePerStakeUpdated') - .withArgs(currentEpoch, anotherNodeId, delegatorKey, scorePerStakeToSet); - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(currentEpoch, anotherNodeId, delegatorKey)) - .to.equal(scorePerStakeToSet); - + await expect( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + anotherNodeId, + delegatorKey, + scorePerStakeToSet, + ), + ) + .to.emit( + RandomSamplingStorage, + 'DelegatorLastSettledNodeEpochScorePerStakeUpdated', + ) + .withArgs( + currentEpoch, + anotherNodeId, + delegatorKey, + scorePerStakeToSet, + ); + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + currentEpoch, + anotherNodeId, + delegatorKey, + ), + ).to.equal(scorePerStakeToSet); + // Test different epoch await time.increase(Number(await Chronos.epochLength())); const nextEpoch = await Chronos.getCurrentEpoch(); - await expect(RandomSamplingStorage.connect(rsSigner).setDelegatorLastSettledNodeEpochScorePerStake(nextEpoch, nodeId, delegatorKey, scorePerStakeToSet)) - .to.emit(RandomSamplingStorage, 'DelegatorLastSettledNodeEpochScorePerStakeUpdated') + await expect( + RandomSamplingStorage.connect( + rsSigner, + ).setDelegatorLastSettledNodeEpochScorePerStake( + nextEpoch, + nodeId, + delegatorKey, + scorePerStakeToSet, + ), + ) + .to.emit( + RandomSamplingStorage, + 'DelegatorLastSettledNodeEpochScorePerStakeUpdated', + ) .withArgs(nextEpoch, nodeId, delegatorKey, scorePerStakeToSet); - expect(await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake(nextEpoch, nodeId, delegatorKey)) - .to.equal(scorePerStakeToSet); + expect( + await RandomSamplingStorage.getDelegatorLastSettledNodeEpochScorePerStake( + nextEpoch, + nodeId, + delegatorKey, + ), + ).to.equal(scorePerStakeToSet); await stopImpersonate(RandomSampling); }); @@ -1363,19 +1952,16 @@ describe('@unit RandomSamplingStorage', function () { describe('Edge Cases', () => { it('Should revert if no matching duration in blocks found', async () => { - // Get current epoch - const currentEpoch = await Chronos.getCurrentEpoch(); - // Add a new duration that will be effective in the next epoch const newDuration = 1000; await RandomSampling.setProofingPeriodDurationInBlocks(newDuration); - + // Move to next epoch await time.increase(Number(await Chronos.epochLength())); - + // Try to get duration for an epoch before the first duration was set await expect( - RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(0n) + RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(0n), ).to.be.revertedWith('No applicable duration found'); }); @@ -1387,8 +1973,9 @@ describe('@unit RandomSamplingStorage', function () { // Add multiple durations with different epochs for (let i = 0; i < numDurations; i++) { const duration = baseDuration + i; - await RandomSamplingStorage.connect(await ethers.getSigner(accounts[0].address)) - .addProofingPeriodDuration(duration, currentEpoch + BigInt(i)); + await RandomSamplingStorage.connect( + await ethers.getSigner(accounts[0].address), + ).addProofingPeriodDuration(duration, currentEpoch + BigInt(i)); await time.increase(Number(await Chronos.epochLength())); } @@ -1396,9 +1983,418 @@ describe('@unit RandomSamplingStorage', function () { for (let i = 0; i < numDurations; i++) { const targetEpoch = currentEpoch + BigInt(i); const expectedDuration = baseDuration + i; - const actualDuration = await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks(targetEpoch); + const actualDuration = + await RandomSamplingStorage.getEpochProofingPeriodDurationInBlocks( + targetEpoch, + ); expect(actualDuration).to.equal(expectedDuration); } }); }); -}); \ No newline at end of file + + // ... existing code ... + + describe('Challenge Recency Factor Management', () => { + it('Should set correct initial challenge recency factor', async () => { + // Deploy a new instance to check initial values + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + const challengeRecencyFactor = 5000; + const bfsIterations = 25; + const newRandomSamplingStorage = + await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + challengeRecencyFactor, + bfsIterations, + ); + + expect( + await newRandomSamplingStorage.getChallengeRecencyFactor(), + ).to.equal(challengeRecencyFactor); + }); + + it('Should get max challenge recency factor constant', async () => { + expect( + await RandomSamplingStorage.getMaxChallengeRecencyFactor(), + ).to.equal(10000); + }); + + it('Should set challenge recency factor by owner and emit event', async () => { + const newChallengeRecencyFactor = 7500; + const oldChallengeRecencyFactor = + await RandomSamplingStorage.getChallengeRecencyFactor(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + newChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(oldChallengeRecencyFactor, newChallengeRecencyFactor); + + expect(await RandomSamplingStorage.getChallengeRecencyFactor()).to.equal( + newChallengeRecencyFactor, + ); + }); + + it('Should revert if challenge recency factor exceeds maximum', async () => { + const invalidChallengeRecencyFactor = 10001; // Over the maximum + + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + invalidChallengeRecencyFactor, + ), + ).to.be.revertedWith( + 'Recency factor must be between 0 and MAX_CHALLENGE_RECENCY_FACTOR', + ); + }); + + it('Should allow setting challenge recency factor to 0', async () => { + const zeroChallengeRecencyFactor = 0; + const oldChallengeRecencyFactor = + await RandomSamplingStorage.getChallengeRecencyFactor(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + zeroChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(oldChallengeRecencyFactor, zeroChallengeRecencyFactor); + + expect(await RandomSamplingStorage.getChallengeRecencyFactor()).to.equal( + 0, + ); + }); + + it('Should allow setting challenge recency factor to maximum', async () => { + const maxChallengeRecencyFactor = 10000; + const oldChallengeRecencyFactor = + await RandomSamplingStorage.getChallengeRecencyFactor(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + maxChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(oldChallengeRecencyFactor, maxChallengeRecencyFactor); + + expect(await RandomSamplingStorage.getChallengeRecencyFactor()).to.equal( + maxChallengeRecencyFactor, + ); + }); + + // // TODO: This test fails because the hub owner is not the multisig owner + // it('Should revert if called by non-owner', async () => { + // const newChallengeRecencyFactor = 6000; + + // await expect( + // RandomSamplingStorage.connect(accounts[1]).setChallengeRecencyFactor( + // newChallengeRecencyFactor, + // ), + // ) + // .to.be.revertedWithCustomError( + // RandomSamplingStorage, + // 'UnauthorizedAccess', + // ) + // .withArgs('Only Hub Owner or Multisig Owner'); + // }); + }); + + describe('BFS Iterations Management', () => { + it('Should set correct initial BFS iterations', async () => { + // Deploy a new instance to check initial values + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + const challengeRecencyFactor = 5000; + const bfsIterations = 25; + const newRandomSamplingStorage = + await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + challengeRecencyFactor, + bfsIterations, + ); + + expect(await newRandomSamplingStorage.getBfsIterations()).to.equal( + bfsIterations, + ); + }); + + it('Should get default BFS iterations constant', async () => { + expect(await RandomSamplingStorage.getDefaultBfsIterations()).to.equal( + 50, + ); + }); + + it('Should set BFS iterations by owner and emit event', async () => { + const newBfsIterations = 75; + const oldBfsIterations = await RandomSamplingStorage.getBfsIterations(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + newBfsIterations, + ), + ) + .to.emit(RandomSamplingStorage, 'BfsIterationsUpdated') + .withArgs(oldBfsIterations, newBfsIterations); + + expect(await RandomSamplingStorage.getBfsIterations()).to.equal( + newBfsIterations, + ); + }); + + it('Should revert if BFS iterations is 0', async () => { + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations(0), + ).to.be.revertedWith('Iterations must be greater than 0'); + }); + + it('Should allow setting BFS iterations to 1', async () => { + const minBfsIterations = 1; + const oldBfsIterations = await RandomSamplingStorage.getBfsIterations(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + minBfsIterations, + ), + ) + .to.emit(RandomSamplingStorage, 'BfsIterationsUpdated') + .withArgs(oldBfsIterations, minBfsIterations); + + expect(await RandomSamplingStorage.getBfsIterations()).to.equal( + minBfsIterations, + ); + }); + + it('Should allow setting BFS iterations to maximum uint8 value', async () => { + const maxBfsIterations = 255; // Maximum uint8 value + const oldBfsIterations = await RandomSamplingStorage.getBfsIterations(); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + maxBfsIterations, + ), + ) + .to.emit(RandomSamplingStorage, 'BfsIterationsUpdated') + .withArgs(oldBfsIterations, maxBfsIterations); + + expect(await RandomSamplingStorage.getBfsIterations()).to.equal( + maxBfsIterations, + ); + }); + + // // TODO: This test fails because the hub owner is not the multisig owner + // it('Should revert if called by non-owner', async () => { + // const newBfsIterations = 100; + + // await expect( + // RandomSamplingStorage.connect(accounts[1]).setBfsIterations( + // newBfsIterations, + // ), + // ) + // .to.be.revertedWithCustomError( + // RandomSamplingStorage, + // 'UnauthorizedAccess', + // ) + // .withArgs('Only Hub Owner or Multisig Owner'); + // }); + }); + + describe('Constructor Validation for New Parameters', () => { + it('Should revert if challenge recency factor exceeds maximum in constructor', async () => { + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + const invalidChallengeRecencyFactor = 10001; + const validBfsIterations = 50; + + await expect( + RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + invalidChallengeRecencyFactor, + validBfsIterations, + ), + ).to.be.revertedWith( + 'Challenge recency factor must be between 0 and MAX_CHALLENGE_RECENCY_FACTOR', + ); + }); + + it('Should revert if BFS iterations is 0 in constructor', async () => { + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + const validChallengeRecencyFactor = 5000; + const invalidBfsIterations = 0; + + await expect( + RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + validChallengeRecencyFactor, + invalidBfsIterations, + ), + ).to.be.revertedWith('Bfs iterations must be greater than 0'); + }); + + it('Should emit events on constructor with valid parameters', async () => { + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + const challengeRecencyFactor = 6000; + const bfsIterations = 30; + + const deploymentTx = await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + challengeRecencyFactor, + bfsIterations, + ); + const receipt = await deploymentTx.deploymentTransaction()?.wait(); + + // Check for ChallengeRecencyFactorUpdated event + const challengeRecencyEvent = receipt?.logs.find( + (log) => + log.topics[0] === + hre.ethers.id('ChallengeRecencyFactorUpdated(uint16,uint16)'), + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(challengeRecencyEvent).to.not.be.undefined; + + // Check for BfsIterationsUpdated event + const bfsIterationsEvent = receipt?.logs.find( + (log) => + log.topics[0] === hre.ethers.id('BfsIterationsUpdated(uint8,uint8)'), + ); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(bfsIterationsEvent).to.not.be.undefined; + }); + + it('Should accept valid edge case values in constructor', async () => { + const RandomSamplingStorageFactory = await hre.ethers.getContractFactory( + 'RandomSamplingStorage', + ); + + // Test minimum values + const minChallengeRecencyFactor = 0; + const minBfsIterations = 1; + const minInstance = await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + minChallengeRecencyFactor, + minBfsIterations, + ); + expect(await minInstance.getChallengeRecencyFactor()).to.equal( + minChallengeRecencyFactor, + ); + expect(await minInstance.getBfsIterations()).to.equal(minBfsIterations); + + // Test maximum values + const maxChallengeRecencyFactor = 10000; + const maxBfsIterations = 255; + const maxInstance = await RandomSamplingStorageFactory.deploy( + Hub.target, + proofingPeriodDurationInBlocks, + maxChallengeRecencyFactor, + maxBfsIterations, + ); + expect(await maxInstance.getChallengeRecencyFactor()).to.equal( + maxChallengeRecencyFactor, + ); + expect(await maxInstance.getBfsIterations()).to.equal(maxBfsIterations); + }); + }); + + describe('Multi-Parameter Updates', () => { + it('Should handle multiple parameter updates correctly', async () => { + const initialChallengeRecencyFactor = + await RandomSamplingStorage.getChallengeRecencyFactor(); + const initialBfsIterations = + await RandomSamplingStorage.getBfsIterations(); + + // Update challenge recency factor + const newChallengeRecencyFactor = 8000; + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + newChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(initialChallengeRecencyFactor, newChallengeRecencyFactor); + + // Update BFS iterations + const newBfsIterations = 100; + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + newBfsIterations, + ), + ) + .to.emit(RandomSamplingStorage, 'BfsIterationsUpdated') + .withArgs(initialBfsIterations, newBfsIterations); + + // Verify both values are updated + expect(await RandomSamplingStorage.getChallengeRecencyFactor()).to.equal( + newChallengeRecencyFactor, + ); + expect(await RandomSamplingStorage.getBfsIterations()).to.equal( + newBfsIterations, + ); + + // Update challenge recency factor again + const newerChallengeRecencyFactor = 2500; + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + newerChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(newChallengeRecencyFactor, newerChallengeRecencyFactor); + + // Verify challenge recency factor is updated but BFS iterations remains unchanged + expect(await RandomSamplingStorage.getChallengeRecencyFactor()).to.equal( + newerChallengeRecencyFactor, + ); + expect(await RandomSamplingStorage.getBfsIterations()).to.equal( + newBfsIterations, + ); + }); + + it('Should emit correct old values in events after multiple updates', async () => { + // Set initial values + const firstChallengeRecencyFactor = 3000; + const firstBfsIterations = 20; + + await RandomSamplingStorage.connect( + accounts[0], + ).setChallengeRecencyFactor(firstChallengeRecencyFactor); + await RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + firstBfsIterations, + ); + + // Update and verify events show correct old values + const secondChallengeRecencyFactor = 9000; + const secondBfsIterations = 80; + + await expect( + RandomSamplingStorage.connect(accounts[0]).setChallengeRecencyFactor( + secondChallengeRecencyFactor, + ), + ) + .to.emit(RandomSamplingStorage, 'ChallengeRecencyFactorUpdated') + .withArgs(firstChallengeRecencyFactor, secondChallengeRecencyFactor); + + await expect( + RandomSamplingStorage.connect(accounts[0]).setBfsIterations( + secondBfsIterations, + ), + ) + .to.emit(RandomSamplingStorage, 'BfsIterationsUpdated') + .withArgs(firstBfsIterations, secondBfsIterations); + }); + }); +}); From f95a782426671f98a76860a6a19eccfd0b4f48b3 Mon Sep 17 00:00:00 2001 From: Zvonimir Date: Fri, 13 Jun 2025 16:10:51 +0200 Subject: [PATCH 3/3] fix tests --- test/unit/RandomSamplingStorage.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unit/RandomSamplingStorage.test.ts b/test/unit/RandomSamplingStorage.test.ts index 8926c636..212960c0 100644 --- a/test/unit/RandomSamplingStorage.test.ts +++ b/test/unit/RandomSamplingStorage.test.ts @@ -2266,6 +2266,10 @@ describe('@unit RandomSamplingStorage', function () { // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(challengeRecencyEvent).to.not.be.undefined; + // Check ChallengeRecencyFactorUpdated has correct old and new values + expect(challengeRecencyEvent?.args[0]).to.equal(0); + expect(challengeRecencyEvent?.args[1]).to.equal(challengeRecencyFactor); + // Check for BfsIterationsUpdated event const bfsIterationsEvent = receipt?.logs.find( (log) => @@ -2273,6 +2277,10 @@ describe('@unit RandomSamplingStorage', function () { ); // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(bfsIterationsEvent).to.not.be.undefined; + + // Check BfsIterationsUpdated has correct old and new values + expect(bfsIterationsEvent?.args[0]).to.equal(0); + expect(bfsIterationsEvent?.args[1]).to.equal(bfsIterations); }); it('Should accept valid edge case values in constructor', async () => {