diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index 351ccdfba..84fd481e7 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -21,10 +21,43 @@ permissions: jobs: tests: runs-on: ubuntu-latest - + env: + FORGE_REV: v0.3.0 steps: - uses: actions/checkout@v3 + # TODO: Remove after upgrade to CSM v2 on Mainnet. + - name: Checkout CSM repo + uses: actions/checkout@v4 + with: + repository: 'lidofinance/community-staking-module' + ref: 'develop' + path: 'testruns/community-staking-module' + persist-credentials: false + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: ${{ env.FORGE_REV }} + + - name: Install node + uses: actions/setup-node@v4 + with: + node-version-file: "testruns/community-staking-module/.nvmrc" + cache: 'yarn' + cache-dependency-path: "testruns/community-staking-module/yarn.lock" + + - name: Install Just + run: cargo install "just@1.24.0" + + - name: Install dependencies + working-directory: testruns/community-staking-module + run: just deps + + - name: Build contracts + working-directory: testruns/community-staking-module + run: just build + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: @@ -42,9 +75,6 @@ jobs: run: | poetry install --no-interaction --with=dev - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - name: Mainnet Fork Tests run: poetry run pytest -m 'fork' -n auto tests env: diff --git a/.gitignore b/.gitignore index a12e4a1a0..e8866b716 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +testruns/ # Translations *.mo diff --git a/assets/CSAccounting.json b/assets/CSAccounting.json index 348937605..271d922d5 100644 --- a/assets/CSAccounting.json +++ b/assets/CSAccounting.json @@ -1 +1 @@ -[{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargeRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockRetentionPeriod","inputs":[],"outputs":[{"name":"retention","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"retentionUntil","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"}] +[{"type":"constructor","inputs":[{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"module","type":"address","internalType":"address"},{"name":"_feeDistributor","type":"address","internalType":"address"},{"name":"maxCurveLength","type":"uint256","internalType":"uint256"},{"name":"minBondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"maxBondLockPeriod","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_BOND_CURVE_ID","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"LIDO","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILido"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_BOND_CURVES_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MAX_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MAX_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"WITHDRAWAL_QUEUE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWithdrawalQueue"}],"stateMutability":"view"},{"type":"function","name":"WSTETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWstETH"}],"stateMutability":"view"},{"type":"function","name":"addBondCurve","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"id","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargePenaltyRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETH","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"bondCurvesInputs","type":"tuple[][]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[][]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockPeriod","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummaryShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableRewardsAndBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimableShares","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getCurvesCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"until","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]},{"name":"admin","type":"address","internalType":"address"},{"name":"bondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pullFeeRewards","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renewBurnerAllowance","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondLockPeriod","inputs":[{"name":"period","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setChargePenaltyRecipient","inputs":[{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"applied","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"updateBondCurve","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BondBurned","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amountToBurn","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"burnedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCharged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toChargeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"chargedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"requestId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveAdded","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondCurveSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"curveId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveUpdated","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondDepositedETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"newAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"until","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockPeriodChanged","inputs":[{"name":"period","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ChargePenaltyRecipientSet","inputs":[{"name":"chargePenaltyRecipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"ElRewardsVaultReceiveFailed","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidBondCurveId","inputs":[]},{"type":"error","name":"InvalidBondCurveLength","inputs":[]},{"type":"error","name":"InvalidBondCurveMaxLength","inputs":[]},{"type":"error","name":"InvalidBondCurveValues","inputs":[]},{"type":"error","name":"InvalidBondCurvesLength","inputs":[]},{"type":"error","name":"InvalidBondLockAmount","inputs":[]},{"type":"error","name":"InvalidBondLockPeriod","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInitializationCurveId","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NothingToClaim","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotModule","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroChargePenaltyRecipientAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] diff --git a/assets/CSFeeDistributor.json b/assets/CSFeeDistributor.json index da1cfa3ed..3682abbb8 100644 --- a/assets/CSFeeDistributor.json +++ b/assets/CSFeeDistributor.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"FeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCID","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAccounting","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"distributed","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"distributionDataHistoryCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getHistoricalDistributionData","inputs":[{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSFeeDistributor.DistributionData","components":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"logCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"_logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"rebateRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRebateRecipient","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"DistributionLogUpdated","inputs":[{"name":"logCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"ModuleFeeDistributed","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"OperatorFeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RebateRecipientSet","inputs":[{"name":"recipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RebateTransferred","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidLogCID","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCid","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SenderIsNotAccounting","inputs":[]},{"type":"error","name":"SenderIsNotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroRebateRecipientAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index 4c0e68d3d..6d13c0c2c 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"CONTRACT_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"avgPerfLeewayBP","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"_avgPerfLeewayBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeeway","inputs":[{"name":"valueBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct CSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerfLeewaySet","inputs":[{"name":"valueBP","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSettled","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerfLeeway","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"feeDistributor","type":"address","internalType":"address"},{"name":"strikes","type":"address","internalType":"address"},{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"STRIKES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] diff --git a/assets/CSModule.json b/assets/CSModule.json index 02b75373e..4521e2966 100644 --- a/assets/CSModule.json +++ b/assets/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"minSlashingPenaltyQuotient","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingFine","type":"uint256","internalType":"uint256"},{"name":"maxKeysPerOperatorEA","type":"uint256","internalType":"uint256"},{"name":"lidoLocator","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EL_REWARDS_STEALING_FINE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"INITIAL_SLASHING_PENALTY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"activatePublicRelease","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addNodeOperatorStETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"length","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"earlyAdoption","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEarlyAdoption"}],"stateMutability":"view"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"_earlyAdoption","type":"address","internalType":"address"},{"name":"_keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"keyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"normalizeQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitInitialSlashing","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"InitialSlashingSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PublicRelease","inputs":[],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChangedByRequest","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadySet","inputs":[]},{"type":"error","name":"AlreadySubmitted","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MaxSigningKeysCountExceeded","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToJoinYet","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"},{"name":"_accounting","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEPOSIT_SIZE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"},{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositQueuePointers","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"exitDeadlineThreshold","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"},{"name":"usedPriorityQueue","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorManagementProperties","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorOwner","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorTotalDepositedKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalDepositedKeys","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorExitDelayPenaltyApplicable","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"migrateToPriorityQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorExitTriggered","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"withdrawalRequestPaidFee","type":"uint256","internalType":"uint256"},{"name":"exitType","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportValidatorExitDelay","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawals","inputs":[{"name":"withdrawalsInfo","type":"tuple[]","internalType":"struct ValidatorWithdrawalInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateDepositableValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"queuePriority","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DelayedValidatorExitPenalized","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penaltyValue","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"CannotAddKeys","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"KeysLimitExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"PriorityQueueAlreadyUsed","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json new file mode 100644 index 000000000..43f093fac --- /dev/null +++ b/assets/CSParametersRegistry.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidAllowedExitDelay","inputs":[]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyNumberValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]},{"type":"error","name":"ZeroQueueLowestPriority","inputs":[]}] diff --git a/assets/CSStrikes.json b/assets/CSStrikes.json new file mode 100644 index 000000000..fdd452171 --- /dev/null +++ b/assets/CSStrikes.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"ejector","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEjector"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"keyStrikes","type":"tuple","internalType":"struct ICSStrikes.KeyStrikes","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkey","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.KeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"},{"name":"refundRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setEjector","inputs":[{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.KeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkeys","type":"bytes[]","internalType":"bytes[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"EjectorSet","inputs":[{"name":"ejector","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"MerkleProofInvalidMultiproof","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SenderIsNotOracle","inputs":[]},{"type":"error","name":"ValueNotEvenlyDivisible","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroEjectorAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]}] diff --git a/src/constants.py b/src/constants.py index 2ba467222..5e73ce9ba 100644 --- a/src/constants.py +++ b/src/constants.py @@ -32,7 +32,9 @@ MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA = Gwei(2**7 * 10**9) MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT = Gwei(2**8 * 10**9) # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters -SLOTS_PER_HISTORICAL_ROOT = 2**13 # 8192 +SLOTS_PER_HISTORICAL_ROOT = 2**13 # 8192 +# https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#sync-committee +EPOCHS_PER_SYNC_COMMITTEE_PERIOD = 256 # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#withdrawals-processing MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 2**3 @@ -45,9 +47,15 @@ SHARE_RATE_PRECISION_E27 = 10**27 TOTAL_BASIS_POINTS = 10000 +# Lido CSM constants for network performance calculation +ATTESTATIONS_WEIGHT = 54 +BLOCKS_WEIGHT = 8 +SYNC_WEIGHT = 2 + # Local constants GWEI_TO_WEI = 10**9 MAX_BLOCK_GAS_LIMIT = 30_000_000 UINT64_MAX = 2**64 - 1 +UINT256_MAX = 2**256 - 1 ALLOWED_KAPI_VERSION = Version('1.5.0') diff --git a/src/modules/checks/suites/consensus_node.py b/src/modules/checks/suites/consensus_node.py index 39ed6f0db..a4994cda8 100644 --- a/src/modules/checks/suites/consensus_node.py +++ b/src/modules/checks/suites/consensus_node.py @@ -36,6 +36,6 @@ def check_attestation_committees(web3: Web3, blockstamp): assert web3.cc.get_attestation_committees(blockstamp, epoch), "consensus-client provide no attestation committees" -def check_block_attestations(web3: Web3, blockstamp): +def check_block_attestations_and_sync(web3: Web3, blockstamp): """Check that consensus-client able to provide block attestations""" - assert web3.cc.get_block_attestations(blockstamp.slot_number), "consensus-client provide no block attestations" + assert web3.cc.get_block_attestations_and_sync(blockstamp.slot_number), "consensus-client provide no block attestations and sync" diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 35d926ada..021e71e18 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -1,4 +1,5 @@ import logging +from collections import UserDict from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from itertools import batched @@ -6,13 +7,16 @@ from typing import Iterable, Sequence from src import variables -from src.constants import SLOTS_PER_HISTORICAL_ROOT -from src.metrics.prometheus.csm import CSM_MIN_UNPROCESSED_EPOCH, CSM_UNPROCESSED_EPOCHS_COUNT +from src.constants import SLOTS_PER_HISTORICAL_ROOT, EPOCHS_PER_SYNC_COMMITTEE_PERIOD +from src.metrics.prometheus.csm import CSM_UNPROCESSED_EPOCHS_COUNT, CSM_MIN_UNPROCESSED_EPOCH from src.modules.csm.state import State from src.providers.consensus.client import ConsensusClient +from src.providers.consensus.types import SyncCommittee, SyncAggregate +from src.utils.blockstamp import build_blockstamp from src.providers.consensus.types import BlockAttestation from src.types import BlockRoot, BlockStamp, CommitteeIndex, EpochNumber, SlotNumber, ValidatorIndex from src.utils.range import sequence +from src.utils.slot import get_prev_non_missed_slot from src.utils.timeit import timeit from src.utils.types import hex_str_to_bytes from src.utils.web3converter import Web3Converter @@ -22,6 +26,7 @@ class MinStepIsNotReached(Exception): ... +class SlotOutOfRootsRange(Exception): ... @dataclass @@ -32,7 +37,7 @@ class FrameCheckpoint: @dataclass class ValidatorDuty: - index: ValidatorIndex + validator_index: ValidatorIndex included: bool @@ -103,7 +108,22 @@ def _is_min_step_reached(self): return False -type Committees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] +type SlotBlockRoot = tuple[SlotNumber, BlockRoot | None] +type SyncCommittees = dict[SlotNumber, list[ValidatorDuty]] +type AttestationCommittees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] + + +class SyncCommitteesCache(UserDict): + + max_size = max(2, variables.CSM_ORACLE_MAX_CONCURRENCY) + + def __setitem__(self, sync_committee_period: int, value: SyncCommittee): + if len(self) >= self.max_size: + self.pop(min(self)) + super().__setitem__(sync_committee_period, value) + + +SYNC_COMMITTEES_CACHE = SyncCommitteesCache() class FrameCheckpointProcessor: @@ -135,10 +155,11 @@ def exec(self, checkpoint: FrameCheckpoint) -> int: return 0 block_roots = self._get_block_roots(checkpoint.slot) duty_epochs_roots = { - duty_epoch: self._select_block_roots(duty_epoch, block_roots, checkpoint.slot) + duty_epoch: self._select_block_roots(block_roots, duty_epoch, checkpoint.slot) for duty_epoch in unprocessed_epochs } - self._process(unprocessed_epochs, duty_epochs_roots) + self._process(block_roots, checkpoint.slot, unprocessed_epochs, duty_epochs_roots) + self.state.commit() return len(unprocessed_epochs) def _get_block_roots(self, checkpoint_slot: SlotNumber): @@ -153,8 +174,8 @@ def _get_block_roots(self, checkpoint_slot: SlotNumber): return [br[i] if i == pivot_index or br[i] != br[i - 1] else None for i in range(len(br))] def _select_block_roots( - self, duty_epoch: EpochNumber, block_roots: list[BlockRoot | None], checkpoint_slot: SlotNumber - ) -> list[BlockRoot]: + self, block_roots: list[BlockRoot | None], duty_epoch: EpochNumber, checkpoint_slot: SlotNumber + ) -> tuple[list[SlotBlockRoot], list[SlotBlockRoot]]: roots_to_check = [] # To check duties in the current epoch you need to # have 32 slots of the current epoch and 32 slots of the next epoch @@ -163,20 +184,39 @@ def _select_block_roots( self.converter.get_epoch_last_slot(EpochNumber(duty_epoch + 1)), ) for slot_to_check in slots: - # From spec - # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_block_root_at_slot - if not slot_to_check < checkpoint_slot <= slot_to_check + SLOTS_PER_HISTORICAL_ROOT: - raise ValueError("Slot is out of the state block roots range") - if br := block_roots[slot_to_check % SLOTS_PER_HISTORICAL_ROOT]: - roots_to_check.append(br) + block_root = self._select_block_root_by_slot(block_roots, checkpoint_slot, slot_to_check) + roots_to_check.append((slot_to_check, block_root)) + + slots_per_epoch = self.converter.chain_config.slots_per_epoch + duty_epoch_roots, next_epoch_roots = roots_to_check[:slots_per_epoch], roots_to_check[slots_per_epoch:] - return roots_to_check + return duty_epoch_roots, next_epoch_roots - def _process(self, unprocessed_epochs: list[EpochNumber], duty_epochs_roots: dict[EpochNumber, list[BlockRoot]]): + @staticmethod + def _select_block_root_by_slot(block_roots: list[BlockRoot | None], checkpoint_slot: SlotNumber, root_slot: SlotNumber) -> BlockRoot | None: + # From spec + # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_block_root_at_slot + if not root_slot < checkpoint_slot <= root_slot + SLOTS_PER_HISTORICAL_ROOT: + raise SlotOutOfRootsRange("Slot is out of the state block roots range") + return block_roots[root_slot % SLOTS_PER_HISTORICAL_ROOT] + + def _process( + self, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber, + unprocessed_epochs: list[EpochNumber], + epochs_roots_to_check: dict[EpochNumber, tuple[list[SlotBlockRoot], list[SlotBlockRoot]]] + ): executor = ThreadPoolExecutor(max_workers=variables.CSM_ORACLE_MAX_CONCURRENCY) try: futures = { - executor.submit(self._check_duty, duty_epoch, duty_epochs_roots[duty_epoch]) + executor.submit( + self._check_duties, + checkpoint_block_roots, + checkpoint_slot, + duty_epoch, + *epochs_roots_to_check[duty_epoch] + ) for duty_epoch in unprocessed_epochs } for future in as_completed(futures): @@ -190,28 +230,54 @@ def _process(self, unprocessed_epochs: list[EpochNumber], duty_epochs_roots: dic logger.info({"msg": "The executor was shut down"}) @timeit(lambda args, duration: logger.info({"msg": f"Epoch {args.duty_epoch} processed in {duration:.2f} seconds"})) - def _check_duty( + def _check_duties( self, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber, duty_epoch: EpochNumber, - block_roots: list[BlockRoot], + duty_epoch_roots: list[SlotBlockRoot], + next_epoch_roots: list[SlotBlockRoot], ): logger.info({"msg": f"Processing epoch {duty_epoch}"}) - committees = self._prepare_committees(duty_epoch) - for root in block_roots: - attestations = self.cc.get_block_attestations(root) - process_attestations(attestations, committees) + + att_committees = self._prepare_attestation_duties(duty_epoch) + propose_duties = self._prepare_propose_duties(duty_epoch, checkpoint_block_roots, checkpoint_slot) + sync_committees = self._prepare_sync_committee_duties(duty_epoch, duty_epoch_roots) + + for slot, root in [*duty_epoch_roots, *next_epoch_roots]: + missed_slot = root is None + if missed_slot: + continue + attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(root) + if (slot, root) in duty_epoch_roots: + propose_duties[slot].included = True + process_sync(slot, sync_aggregate, sync_committees) + process_attestations(attestations, att_committees) with lock: - for committee in committees.values(): - for validator_duty in committee: - self.state.inc( - validator_duty.index, - included=validator_duty.included, - ) if duty_epoch not in self.state.unprocessed_epochs: raise ValueError(f"Epoch {duty_epoch} is not in epochs that should be processed") + for att_committee in att_committees.values(): + for att_duty in att_committee: + self.state.save_att_duty( + duty_epoch, + att_duty.validator_index, + included=att_duty.included, + ) + for sync_committee in sync_committees.values(): + for sync_duty in sync_committee: + self.state.save_sync_duty( + duty_epoch, + sync_duty.validator_index, + included=sync_duty.included, + ) + for proposer_duty in propose_duties.values(): + self.state.save_prop_duty( + duty_epoch, + proposer_duty.validator_index, + included=proposer_duty.included + ) self.state.add_processed_epoch(duty_epoch) - self.state.commit() self.state.log_progress() unprocessed_epochs = self.state.unprocessed_epochs CSM_UNPROCESSED_EPOCHS_COUNT.set(len(unprocessed_epochs)) @@ -219,23 +285,128 @@ def _check_duty( @timeit( lambda args, duration: logger.info( - {"msg": f"Committees for epoch {args.epoch} processed in {duration:.2f} seconds"} + {"msg": f"Attestation Committees for epoch {args.epoch} prepared in {duration:.2f} seconds"} ) ) - def _prepare_committees(self, epoch: EpochNumber) -> Committees: + def _prepare_attestation_duties(self, epoch: EpochNumber) -> AttestationCommittees: committees = {} for committee in self.cc.get_attestation_committees(self.finalized_blockstamp, epoch): validators = [] # Order of insertion is used to track the positions in the committees. - for validator in committee.validators: - validators.append(ValidatorDuty(index=validator, included=False)) + for validator_index in committee.validators: + validators.append(ValidatorDuty(validator_index, included=False)) committees[(committee.slot, committee.index)] = validators return committees + @timeit( + lambda args, duration: logger.info( + {"msg": f"Sync Committee for epoch {args.epoch} prepared in {duration:.2f} seconds"} + ) + ) + def _prepare_sync_committee_duties( + self, epoch: EpochNumber, epoch_block_roots: list[SlotBlockRoot] + ) -> dict[SlotNumber, list[ValidatorDuty]]: + + with lock: + sync_committee = self._get_sync_committee(epoch) + + duties = {} + for slot, root in epoch_block_roots: + missed_slot = root is None + if missed_slot: + continue + duties[slot] = [ + ValidatorDuty(validator_index=validator_index, included=False) + for validator_index in sync_committee.validators + ] + + return duties + + def _get_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if cached_sync_committee := SYNC_COMMITTEES_CACHE.get(sync_committee_period): + return cached_sync_committee + from_epoch = EpochNumber(epoch - epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD) + to_epoch = EpochNumber(from_epoch + EPOCHS_PER_SYNC_COMMITTEE_PERIOD - 1) + logger.info({"msg": f"Preparing cached Sync Committee for [{from_epoch};{to_epoch}] chain epochs"}) + state_blockstamp = build_blockstamp( + get_prev_non_missed_slot( + self.cc, + self.converter.get_epoch_first_slot(epoch), + self.finalized_blockstamp.slot_number + ) + ) + sync_committee = self.cc.get_sync_committee(state_blockstamp, epoch) + SYNC_COMMITTEES_CACHE[sync_committee_period] = sync_committee + return sync_committee + + @timeit( + lambda args, duration: logger.info( + {"msg": f"Propose Duties for epoch {args.epoch} prepared in {duration:.2f} seconds"} + ) + ) + def _prepare_propose_duties( + self, + epoch: EpochNumber, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber + ) -> dict[SlotNumber, ValidatorDuty]: + duties = {} + dependent_root = self._get_dependent_root_for_proposer_duties(epoch, checkpoint_block_roots, checkpoint_slot) + proposer_duties = self.cc.get_proposer_duties(epoch, dependent_root) + for duty in proposer_duties: + duties[duty.slot] = ValidatorDuty(validator_index=duty.validator_index, included=False) + return duties + + def _get_dependent_root_for_proposer_duties( + self, + epoch: EpochNumber, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber + ) -> BlockRoot: + dependent_root = None + dependent_slot = self.converter.get_epoch_last_slot(EpochNumber(epoch - 1)) + try: + while not dependent_root: + dependent_root = self._select_block_root_by_slot( + checkpoint_block_roots, checkpoint_slot, dependent_slot + ) + if dependent_root: + logger.debug( + { + "msg": f"Got dependent root from state block roots for epoch {epoch}. " + f"{dependent_slot=} {dependent_root=}" + } + ) + break + dependent_slot = SlotNumber(int(dependent_slot - 1)) + except SlotOutOfRootsRange: + dependent_non_missed_slot = get_prev_non_missed_slot( + self.cc, + dependent_slot, + self.finalized_blockstamp.slot_number + ).message.slot + dependent_root = self.cc.get_block_root(dependent_non_missed_slot).root + logger.debug( + { + "msg": f"Got dependent root from CL for epoch {epoch}. " + f"{dependent_non_missed_slot=} {dependent_root=}" + } + ) + return dependent_root + + +def process_sync(slot: SlotNumber, sync_aggregate: SyncAggregate, committees: SyncCommittees) -> None: + committee = committees[slot] + # Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#syncaggregate + sync_bits = hex_bitvector_to_list(sync_aggregate.sync_committee_bits) + for index_in_committee in get_set_indices(sync_bits): + committee[index_in_committee].included = True + def process_attestations( attestations: Iterable[BlockAttestation], - committees: Committees, + committees: AttestationCommittees, ) -> None: for attestation in attestations: committee_offset = 0 diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 5e212d8a0..f1183dcce 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,10 +1,8 @@ import logging -from collections import defaultdict -from typing import Iterator from hexbytes import HexBytes -from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX +from src.constants import UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE from src.metrics.prometheus.csm import ( CSM_CURRENT_FRAME_RANGE_L_EPOCH, @@ -12,10 +10,12 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached +from src.modules.csm.distribution import Distribution, DistributionResult, StrikesValidator +from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog from src.modules.csm.state import State -from src.modules.csm.tree import Tree -from src.modules.csm.types import ReportData, Shares +from src.modules.csm.tree import RewardsTree, StrikesTree, Tree +from src.modules.csm.types import ReportData, RewardsShares, StrikesList from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH @@ -27,14 +27,10 @@ EpochNumber, ReferenceBlockStamp, SlotNumber, - StakingModuleAddress, - StakingModuleId, ) -from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache -from src.utils.slot import get_next_non_missed_slot from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator +from src.web3py.extensions.lido_validators import NodeOperatorId from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -59,23 +55,24 @@ class CSOracle(BaseModule, ConsensusModule): 3. Calculate the share of each CSM node operator excluding underperforming validators. """ - COMPATIBLE_CONTRACT_VERSION = 1 - COMPATIBLE_CONSENSUS_VERSION = 2 + COMPATIBLE_CONTRACT_VERSION = 2 + COMPATIBLE_CONSENSUS_VERSION = 3 report_contract: CSFeeOracleContract - module_id: StakingModuleId def __init__(self, w3: Web3): self.report_contract = w3.csm.oracle self.state = State.load() super().__init__(w3) - self.module_id = self._get_module_id() def refresh_contracts(self): self.report_contract = self.w3.csm.oracle # type: ignore self.state.clear() def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecuteDelay: + if not self._check_compatability(last_finalized_blockstamp): + return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + collected = self.collect_data(last_finalized_blockstamp) if not collected: logger.info( @@ -83,9 +80,8 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute ) return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH - # pylint:disable=duplicate-code report_blockstamp = self.get_blockstamp_for_report(last_finalized_blockstamp) - if not report_blockstamp or not self._check_compatability(report_blockstamp): + if not report_blockstamp: return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH self.process_report(report_blockstamp) @@ -96,49 +92,50 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: self.validate_state(blockstamp) - prev_root = self.w3.csm.get_csm_tree_root(blockstamp) - prev_cid = self.w3.csm.get_csm_tree_cid(blockstamp) - - if (prev_cid is None) != (prev_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") + last_report = self._get_last_report(blockstamp) + rewards_tree_root, rewards_cid = last_report.rewards_tree_root, last_report.rewards_tree_cid - distributed, shares, log = self.calculate_distribution(blockstamp) + distribution = self.calculate_distribution(blockstamp, last_report) - if distributed != sum(shares.values()): - raise InconsistentData(f"Invalid distribution: {sum(shares.values())=} != {distributed=}") + if distribution.total_rewards: + rewards_tree = self.make_rewards_tree(distribution.total_rewards_map) + rewards_tree_root = rewards_tree.root + rewards_cid = self.publish_tree(rewards_tree) - log_cid = self.publish_log(log) - - if not distributed and not shares: - logger.info({"msg": "No shares distributed in the current frame"}) - return ReportData( - consensus_version=self.get_consensus_version(blockstamp), - ref_slot=blockstamp.ref_slot, - tree_root=prev_root, - tree_cid=prev_cid or "", - log_cid=log_cid, - distributed=0, - ).as_tuple() - - if prev_cid and prev_root != ZERO_HASH: - # Update cumulative amount of shares for all operators. - for no_id, acc_shares in self.get_accumulated_shares(prev_cid, prev_root): - shares[no_id] += acc_shares + if distribution.strikes: + strikes_tree = self.make_strikes_tree(distribution.strikes) + strikes_tree_root = strikes_tree.root + strikes_cid = self.publish_tree(strikes_tree) + if strikes_tree_root == last_report.strikes_tree_root: + logger.info({"msg": "Strikes tree is the same as the previous one"}) + if (strikes_cid == last_report.strikes_tree_cid) != (strikes_tree_root == last_report.strikes_tree_root): + raise ValueError(f"Invalid strikes tree built: {strikes_cid=}, {strikes_tree_root=}") else: - logger.info({"msg": "No previous distribution. Nothing to accumulate"}) + strikes_tree_root = HexBytes(ZERO_HASH) + strikes_cid = None - tree = self.make_tree(shares) - tree_cid = self.publish_tree(tree) + logs_cid = self.publish_log(distribution.logs) return ReportData( consensus_version=self.get_consensus_version(blockstamp), ref_slot=blockstamp.ref_slot, - tree_root=tree.root, - tree_cid=tree_cid, - log_cid=log_cid, - distributed=distributed, + tree_root=rewards_tree_root, + tree_cid=rewards_cid or "", + log_cid=logs_cid, + distributed=distribution.total_rewards, + rebate=distribution.total_rebate, + strikes_tree_root=strikes_tree_root, + strikes_tree_cid=strikes_cid or "", ).as_tuple() + def _get_last_report(self, blockstamp: BlockStamp) -> LastReport: + return LastReport.load(self.w3, blockstamp) + + def calculate_distribution(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> DistributionResult: + distribution = Distribution(self.w3, self.converter(blockstamp), self.state) + result = distribution.calculate(blockstamp, last_report) + return result + def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: last_ref_slot = self.w3.csm.get_csm_last_processing_ref_slot(blockstamp) ref_slot = self.get_initial_or_current_frame(blockstamp).ref_slot @@ -152,16 +149,10 @@ def is_reporting_allowed(self, blockstamp: ReferenceBlockStamp) -> bool: CONTRACT_ON_PAUSE.labels("csm").set(on_pause) return not on_pause - @lru_cache(maxsize=1) - def module_validators_by_node_operators(self, blockstamp: BlockStamp) -> ValidatorsByNodeOperator: - return self.w3.lido_validators.get_module_validators_by_node_operators( - StakingModuleAddress(self.w3.csm.module.address), blockstamp - ) - def validate_state(self, blockstamp: ReferenceBlockStamp) -> None: # NOTE: We cannot use `r_epoch` from the `current_frame_range` call because the `blockstamp` is a # `ReferenceBlockStamp`, hence it's a block the frame ends at. We use `ref_epoch` instead. - l_epoch, _ = self.current_frame_range(blockstamp) + l_epoch, _ = self.get_epochs_range_to_process(blockstamp) r_epoch = blockstamp.ref_epoch self.state.validate(l_epoch, r_epoch) @@ -175,8 +166,8 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: converter = self.converter(blockstamp) - l_epoch, r_epoch = self.current_frame_range(blockstamp) - logger.info({"msg": f"Frame for performance data collect: epochs [{l_epoch};{r_epoch}]"}) + l_epoch, r_epoch = self.get_epochs_range_to_process(blockstamp) + logger.info({"msg": f"Epochs range for performance data collect: [{l_epoch};{r_epoch}]"}) # NOTE: Finalized slot is the first slot of justifying epoch, so we need to take the previous. But if the first # slot of the justifying epoch is empty, blockstamp.slot_number will point to the slot where the last finalized @@ -192,16 +183,16 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: if report_blockstamp and report_blockstamp.ref_epoch != r_epoch: logger.warning( { - "msg": f"Frame has been changed, but the change is not yet observed on finalized epoch {finalized_epoch}" + "msg": f"Epochs range has been changed, but the change is not yet observed on finalized epoch {finalized_epoch}" } ) return False if l_epoch > finalized_epoch: - logger.info({"msg": "The starting epoch of the frame is not finalized yet"}) + logger.info({"msg": "The starting epoch of the epochs range is not finalized yet"}) return False - self.state.migrate(l_epoch, r_epoch, consensus_version) + self.state.migrate(l_epoch, r_epoch, converter.frame_config.epochs_per_frame, consensus_version) self.state.log_progress() if self.state.is_fulfilled: @@ -210,7 +201,10 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: try: checkpoints = FrameCheckpointsIterator( - converter, min(self.state.unprocessed_epochs) or l_epoch, r_epoch, finalized_epoch + converter, + min(self.state.unprocessed_epochs), + r_epoch, + finalized_epoch, ) except MinStepIsNotReached: return False @@ -218,114 +212,15 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: processor = FrameCheckpointProcessor(self.w3.cc, self.state, converter, blockstamp) for checkpoint in checkpoints: - if self.current_frame_range(self._receive_last_finalized_slot()) != (l_epoch, r_epoch): - logger.info({"msg": "Checkpoints were prepared for an outdated frame, stop processing"}) + if self.get_epochs_range_to_process(self._receive_last_finalized_slot()) != (l_epoch, r_epoch): + logger.info({"msg": "Checkpoints were prepared for an outdated epochs range, stop processing"}) raise ValueError("Outdated checkpoint") processor.exec(checkpoint) - + # Reset BaseOracle cycle timeout to avoid timeout errors during long checkpoints processing + self._reset_cycle_timeout() return self.state.is_fulfilled - def calculate_distribution( - self, blockstamp: ReferenceBlockStamp - ) -> tuple[int, defaultdict[NodeOperatorId, int], FramePerfLog]: - """Computes distribution of fee shares at the given timestamp""" - - network_avg_perf = self.state.get_network_aggr().perf - threshold = network_avg_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS - operators_to_validators = self.module_validators_by_node_operators(blockstamp) - - # Build the map of the current distribution operators. - distribution: dict[NodeOperatorId, int] = defaultdict(int) - stuck_operators = self.stuck_operators(blockstamp) - log = FramePerfLog(blockstamp, self.state.frame, threshold) - - for (_, no_id), validators in operators_to_validators.items(): - if no_id in stuck_operators: - log.operators[no_id].stuck = True - continue - - for v in validators: - aggr = self.state.data.get(v.index) - - if aggr is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - continue - - if v.validator.slashed is True: - # It means that validator was active during the frame and got slashed and didn't meet the exit - # epoch, so we should not count such validator for operator's share. - log.operators[no_id].validators[v.index].slashed = True - continue - - if aggr.perf > threshold: - # Count of assigned attestations used as a metrics of time - # the validator was active in the current frame. - distribution[no_id] += aggr.assigned - - log.operators[no_id].validators[v.index].perf = aggr - - # Calculate share of each CSM node operator. - shares = defaultdict[NodeOperatorId, int](int) - total = sum(p for p in distribution.values()) - - if not total: - return 0, shares, log - - to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - log.distributable = to_distribute - - for no_id, no_share in distribution.items(): - if no_share: - shares[no_id] = to_distribute * no_share // total - log.operators[no_id].distributed = shares[no_id] - - distributed = sum(s for s in shares.values()) - if distributed > to_distribute: - raise CSMError(f"Invalid distribution: {distributed=} > {to_distribute=}") - return distributed, shares, log - - def get_accumulated_shares(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: - logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) - tree = Tree.decode(self.w3.ipfs.fetch(cid)) - - logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) - - if tree.root != root: - raise ValueError("Unexpected tree root got from IPFS dump") - - for v in tree.tree.values: - yield v["value"] - - def stuck_operators(self, blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: - stuck: set[NodeOperatorId] = set() - l_epoch, _ = self.current_frame_range(blockstamp) - l_ref_slot = self.converter(blockstamp).get_epoch_first_slot(l_epoch) - # NOTE: r_block is guaranteed to be <= ref_slot, and the check - # in the inner frames assures the l_block <= r_block. - l_blockstamp = build_blockstamp( - get_next_non_missed_slot( - self.w3.cc, - l_ref_slot, - blockstamp.slot_number, - ) - ) - - nos_by_module = self.w3.lido_validators.get_lido_node_operators_by_modules(l_blockstamp) - if self.module_id in nos_by_module: - stuck.update(no.id for no in nos_by_module[self.module_id] if no.stuck_validators_count > 0) - else: - logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) - - stuck.update( - self.w3.csm.get_operators_with_stucks_in_range( - l_blockstamp.block_hash, - blockstamp.block_hash, - ) - ) - return stuck - - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: + def make_rewards_tree(self, shares: dict[NodeOperatorId, RewardsShares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") @@ -339,8 +234,26 @@ def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: if stone in shares and len(shares) > 2: shares.pop(stone) - tree = Tree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) - logger.info({"msg": "New tree built for the report", "root": repr(tree.root)}) + tree = RewardsTree.new(tuple(shares.items())) + logger.info({"msg": "New rewards tree built for the report", "root": repr(tree.root)}) + return tree + + def make_strikes_tree(self, strikes: dict[StrikesValidator, StrikesList]) -> StrikesTree: + if not strikes: + raise ValueError("No strikes to build a tree") + + # XXX: We put a stone here to make sure, that even with only 1 validator in the tree, it's + # still possible to report strikes. The CSStrikes contract reverts if the proof's length + # is zero, which is the case when the tree has only one leaf. + stone = (NodeOperatorId(self.w3.csm.module.MAX_OPERATORS_COUNT), HexBytes(b"")) + strikes[stone] = StrikesList() + + # XXX: Remove the stone as soon as we have enough leafs to build a suitable tree. + if stone in strikes and len(strikes) > 2: + strikes.pop(stone) + + tree = StrikesTree.new(tuple((no_id, pubkey, strikes) for ((no_id, pubkey), strikes) in strikes.items())) + logger.info({"msg": "New strikes tree built for the report", "root": repr(tree.root)}) return tree def publish_tree(self, tree: Tree) -> CID: @@ -348,13 +261,13 @@ def publish_tree(self, tree: Tree) -> CID: logger.info({"msg": "Tree dump uploaded to IPFS", "cid": repr(tree_cid)}) return tree_cid - def publish_log(self, log: FramePerfLog) -> CID: - log_cid = self.w3.ipfs.publish(log.encode()) - logger.info({"msg": "Frame log uploaded to IPFS", "cid": repr(log_cid)}) + def publish_log(self, logs: list[FramePerfLog]) -> CID: + log_cid = self.w3.ipfs.publish(FramePerfLog.encode(logs)) + logger.info({"msg": "Frame(s) log uploaded to IPFS", "cid": repr(log_cid)}) return log_cid @lru_cache(maxsize=1) - def current_frame_range(self, blockstamp: BlockStamp) -> tuple[EpochNumber, EpochNumber]: + def get_epochs_range_to_process(self, blockstamp: BlockStamp) -> tuple[EpochNumber, EpochNumber]: converter = self.converter(blockstamp) far_future_initial_epoch = converter.get_epoch_by_timestamp(UINT64_MAX) @@ -385,9 +298,9 @@ def current_frame_range(self, blockstamp: BlockStamp) -> tuple[EpochNumber, Epoc ) if l_ref_slot < last_processing_ref_slot: - raise CSMError(f"Got invalid frame range: {l_ref_slot=} < {last_processing_ref_slot=}") + raise CSMError(f"Got invalid epochs range: {l_ref_slot=} < {last_processing_ref_slot=}") if l_ref_slot >= r_ref_slot: - raise CSMError(f"Got invalid frame range {r_ref_slot=}, {l_ref_slot=}") + raise CSMError(f"Got invalid epochs range {r_ref_slot=}, {l_ref_slot=}") l_epoch = converter.get_epoch_by_slot(SlotNumber(l_ref_slot + 1)) r_epoch = converter.get_epoch_by_slot(r_ref_slot) @@ -400,12 +313,3 @@ def current_frame_range(self, blockstamp: BlockStamp) -> tuple[EpochNumber, Epoc def converter(self, blockstamp: BlockStamp) -> Web3Converter: return Web3Converter(self.get_chain_config(blockstamp), self.get_frame_config(blockstamp)) - - def _get_module_id(self) -> StakingModuleId: - modules: list[StakingModule] = self.w3.lido_contracts.staking_router.get_staking_modules() - - for mod in modules: - if mod.staking_module_address == self.w3.csm.module.address: - return mod.id - - raise NoModuleFound diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py new file mode 100644 index 000000000..9fb6771e1 --- /dev/null +++ b/src/modules/csm/distribution.py @@ -0,0 +1,316 @@ +import logging +import math +from collections import defaultdict +from copy import deepcopy +from dataclasses import dataclass, field + +from src.modules.csm.helpers.last_report import LastReport +from src.modules.csm.log import FramePerfLog, OperatorFrameSummary +from src.modules.csm.state import Frame, State, ValidatorDuties +from src.modules.csm.types import RewardsShares, StrikesList, StrikesValidator, ParticipationShares +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients +from src.providers.execution.exceptions import InconsistentData +from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, StakingModuleAddress, ValidatorIndex +from src.utils.slot import get_reference_blockstamp +from src.utils.web3converter import Web3Converter +from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator +from src.web3py.types import Web3 + +logger = logging.getLogger(__name__) + + +@dataclass +class ValidatorDutiesOutcome: + participation_share: ParticipationShares + rebate_share: ParticipationShares + strikes: int + + +@dataclass +class DistributionResult: + total_rewards: RewardsShares = 0 + total_rebate: RewardsShares = 0 + total_rewards_map: dict[NodeOperatorId, RewardsShares] = field(default_factory=lambda: defaultdict(RewardsShares)) + strikes: dict[StrikesValidator, StrikesList] = field(default_factory=lambda: defaultdict(StrikesList)) + logs: list[FramePerfLog] = field(default_factory=list) + + +class Distribution: + w3: Web3 + converter: Web3Converter + state: State + + def __init__(self, w3: Web3, converter: Web3Converter, state: State): + self.w3 = w3 + self.converter = converter + self.state = state + + def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> DistributionResult: + """Computes distribution of fee shares at the given timestamp""" + result = DistributionResult() + result.strikes.update(last_report.strikes.items()) + + distributed_so_far = 0 + for frame in self.state.frames: + from_epoch, to_epoch = frame + logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) + + frame_blockstamp = self._get_frame_blockstamp(blockstamp, to_epoch) + frame_module_validators = self._get_module_validators(frame_blockstamp) + + total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) + rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far + + frame_log = FramePerfLog(frame_blockstamp, frame) + (rewards_map_in_frame, distributed_rewards_in_frame, rebate_to_protocol_in_frame, strikes_in_frame) = ( + self._calculate_distribution_in_frame( + frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators, frame_log + ) + ) + if not distributed_rewards_in_frame: + logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) + + result.strikes = self._process_strikes(result.strikes, strikes_in_frame, frame_blockstamp) + if not strikes_in_frame: + logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]. Just shifting current strikes."}) + + result.total_rewards += distributed_rewards_in_frame + result.total_rebate += rebate_to_protocol_in_frame + + self.validate_distribution(result.total_rewards, result.total_rebate, total_rewards_to_distribute) + distributed_so_far = result.total_rewards + result.total_rebate + + for no_id, rewards in rewards_map_in_frame.items(): + result.total_rewards_map[no_id] += rewards + + result.logs.append(frame_log) + + if result.total_rewards != sum(result.total_rewards_map.values()): + raise InconsistentData( + f"Invalid distribution: {sum(result.total_rewards_map.values())=} != {result.total_rewards=}" + ) + + for no_id, last_report_rewards in last_report.rewards: + result.total_rewards_map[no_id] += last_report_rewards + + return result + + def _get_frame_blockstamp(self, blockstamp: ReferenceBlockStamp, to_epoch: EpochNumber) -> ReferenceBlockStamp: + if to_epoch != blockstamp.ref_epoch: + return self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) + return blockstamp + + def _get_ref_blockstamp_for_frame( + self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber + ) -> ReferenceBlockStamp: + return get_reference_blockstamp( + cc=self.w3.cc, + ref_slot=self.converter.get_epoch_last_slot(frame_ref_epoch), + ref_epoch=frame_ref_epoch, + last_finalized_slot_number=blockstamp.slot_number, + ) + + def _get_module_validators(self, blockstamp: ReferenceBlockStamp) -> ValidatorsByNodeOperator: + return self.w3.lido_validators.get_module_validators_by_node_operators( + StakingModuleAddress(self.w3.csm.module.address), blockstamp + ) + + def _calculate_distribution_in_frame( + self, + frame: Frame, + blockstamp: ReferenceBlockStamp, + rewards_to_distribute: RewardsShares, + operators_to_validators: ValidatorsByNodeOperator, + log: FramePerfLog, + ) -> tuple[dict[NodeOperatorId, RewardsShares], RewardsShares, RewardsShares, dict[StrikesValidator, int]]: + total_rebate_share = 0 + participation_shares: dict[NodeOperatorId, dict[ValidatorIndex, ParticipationShares]] = {} + frame_strikes: dict[StrikesValidator, int] = {} + + network_perf = self._get_network_performance(frame) + + for (_, no_id), validators in operators_to_validators.items(): + active_validators = [v for v in validators if self.state.data[frame].attestations[v.index].assigned > 0] + if not active_validators: + logger.info({"msg": f"No active validators for {no_id=} in the frame. Skipping"}) + continue + + logger.info({"msg": f"Calculating distribution for {no_id=}"}) + log_operator = log.operators[no_id] + + curve_params = self.w3.csm.get_curve_params(no_id, blockstamp) + log_operator.performance_coefficients = curve_params.perf_coeffs + + active_validators.sort(key=lambda v: v.index) + numbered_validators = enumerate(active_validators, 1) + for key_number, validator in numbered_validators: + key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) + key_reward_share = curve_params.reward_share_data.get_for(key_number) + + duties = self.state.get_validator_duties(frame, validator.index) + + validator_duties_outcome = self.get_validator_duties_outcome( + validator, + duties, + key_threshold, + key_reward_share, + curve_params.perf_coeffs, + log_operator, + ) + if validator_duties_outcome.strikes: + frame_strikes[(no_id, validator.pubkey)] = validator_duties_outcome.strikes + log_operator.validators[validator.index].strikes = validator_duties_outcome.strikes + if not participation_shares.get(no_id): + participation_shares[no_id] = {} + participation_shares[no_id][validator.index] = validator_duties_outcome.participation_share + + total_rebate_share += validator_duties_outcome.rebate_share + + rewards_distribution_map = self.calc_rewards_distribution_in_frame( + participation_shares, total_rebate_share, rewards_to_distribute, log + ) + distributed_rewards = sum(rewards_distribution_map.values()) + # All rewards to distribute should not be rebated if no duties were assigned to validators or + # all validators were below threshold. + rebate_to_protocol = 0 if not distributed_rewards else rewards_to_distribute - distributed_rewards + + for no_id, no_rewards in rewards_distribution_map.items(): + log.operators[no_id].distributed_rewards = no_rewards + log.distributable = rewards_to_distribute + log.distributed_rewards = distributed_rewards + log.rebate_to_protocol = rebate_to_protocol + + return rewards_distribution_map, distributed_rewards, rebate_to_protocol, frame_strikes + + def _get_network_performance(self, frame: Frame) -> float: + att_aggr = self.state.get_att_network_aggr(frame) + prop_aggr = self.state.get_prop_network_aggr(frame) + sync_aggr = self.state.get_sync_network_aggr(frame) + network_perf = PerformanceCoefficients().calc_performance(ValidatorDuties(att_aggr, prop_aggr, sync_aggr)) + return network_perf + + @staticmethod + def get_validator_duties_outcome( + validator: LidoValidator, + duties: ValidatorDuties, + threshold: float, + reward_share: float, + perf_coeffs: PerformanceCoefficients, + log_operator: OperatorFrameSummary, + ) -> ValidatorDutiesOutcome: + if duties.attestation is None or duties.attestation.assigned == 0: + # It's possible that the validator is not assigned to any duty, hence it's performance + # is not presented in the aggregates (e.g. exited, pending for activation etc). + # + # There is a case when validator is exited and still in sync committee. But we can't count his + # `participation_share` because there is no `assigned` attestations for him. + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=0) + + log_validator = log_operator.validators[validator.index] + + if validator.validator.slashed: + # It means that validator was active during the frame and got slashed and didn't meet the exit + # epoch, so we should not count such validator for operator's share. + log_validator.slashed = True + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1) + + performance = perf_coeffs.calc_performance(duties) + + log_validator.threshold = threshold + log_validator.rewards_share = reward_share + log_validator.performance = performance + log_validator.attestation_duty = duties.attestation + if duties.proposal: + log_validator.proposal_duty = duties.proposal + if duties.sync: + log_validator.sync_duty = duties.sync + + if performance > threshold: + # + # - Count of assigned attestations used as a metrics of time the validator was active in the current frame. + # - Reward share is a share of the operator's reward the validator should get, and + # it can be less than 1 due to the value from `CSParametersRegistry`. + # In case of decimal value, the reward should be rounded up in favour of the operator. + # + # Example: + # - Validator was 103 epochs active in the frame (assigned 103 attestations) + # - Reward share for this Operator's key is 0.85 + # 87.55 ≈ 88 of 103 participation shares should be counted for the operator key's reward. + # The rest 15 participation shares should be counted for the protocol's rebate. + # + participation_share = math.ceil(duties.attestation.assigned * reward_share) + rebate_share = duties.attestation.assigned - participation_share + if rebate_share < 0: + raise ValueError(f"Invalid rebate share: {rebate_share=}") + return ValidatorDutiesOutcome(participation_share, rebate_share, strikes=0) + + # In case of bad performance the validator should be striked and assigned attestations are not counted for + # the operator's reward and rebate, so rewards will be socialized between CSM operators. + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1) + + @staticmethod + def calc_rewards_distribution_in_frame( + participation_shares: dict[NodeOperatorId, dict[ValidatorIndex, ParticipationShares]], + rebate_share: ParticipationShares, + rewards_to_distribute: RewardsShares, + log: FramePerfLog, + ) -> dict[NodeOperatorId, RewardsShares]: + if rewards_to_distribute < 0: + raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute=}") + + rewards_distribution: dict[NodeOperatorId, RewardsShares] = defaultdict(RewardsShares) + + node_operators_participation_shares_sum = 0 + per_node_operator_participation_shares: dict[NodeOperatorId, ParticipationShares] = {} + for no_id, per_validator_participation_shares in participation_shares.items(): + no_participation_share = sum(per_validator_participation_shares.values()) + if no_participation_share == 0: + # Skip operators with zero participation + continue + per_node_operator_participation_shares[no_id] = no_participation_share + node_operators_participation_shares_sum += no_participation_share + + total_shares = rebate_share + node_operators_participation_shares_sum + + for no_id, no_participation_share in per_node_operator_participation_shares.items(): + rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_shares + + # Just for logging purpose. We don't expect here any accurate values. + for val_index, val_participation_share in participation_shares[no_id].items(): + log.operators[no_id].validators[val_index].distributed_rewards = ( + rewards_to_distribute * val_participation_share // total_shares + ) + + return rewards_distribution + + @staticmethod + def validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + if (total_distributed_rewards + total_rebate) > total_rewards_to_distribute: + raise ValueError( + f"Invalid distribution: {total_distributed_rewards} + {total_rebate} > {total_rewards_to_distribute}" + ) + + def _process_strikes( + self, + acc: dict[StrikesValidator, StrikesList], + strikes_in_frame: dict[StrikesValidator, int], + frame_blockstamp: ReferenceBlockStamp, + ) -> dict[StrikesValidator, StrikesList]: + merged = deepcopy(acc) + + for key in strikes_in_frame: + if key not in merged: + merged[key] = StrikesList() + merged[key].push(strikes_in_frame[key]) + + for key in list(merged.keys()): + no_id, _ = key + if key not in strikes_in_frame: + merged[key].push(StrikesList.SENTINEL) # Just shifting... + maxlen = self.w3.csm.get_curve_params(no_id, frame_blockstamp).strikes_params.lifetime + merged[key].resize(maxlen) + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + if not sum(merged[key]): + del merged[key] + + return merged diff --git a/src/modules/csm/helpers/__init__.py b/src/modules/csm/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/modules/csm/helpers/last_report.py b/src/modules/csm/helpers/last_report.py new file mode 100644 index 000000000..c8500a0fc --- /dev/null +++ b/src/modules/csm/helpers/last_report.py @@ -0,0 +1,82 @@ +import logging +from dataclasses import dataclass +from functools import cached_property +from typing import Self, Iterable + +from hexbytes import HexBytes + +from src.modules.csm.tree import RewardsTree, StrikesTree +from src.modules.submodules.types import ZERO_HASH +from src.providers.execution.exceptions import InconsistentData +from src.providers.ipfs import CID +from src.types import BlockStamp +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesValidator +from src.web3py.types import Web3 + +logger = logging.getLogger(__name__) + + +@dataclass +class LastReport: + w3: Web3 + blockstamp: BlockStamp + + rewards_tree_root: HexBytes + strikes_tree_root: HexBytes + rewards_tree_cid: CID | None + strikes_tree_cid: CID | None + + @classmethod + def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: + rewards_tree_root = w3.csm.get_rewards_tree_root(blockstamp) + rewards_tree_cid = w3.csm.get_rewards_tree_cid(blockstamp) + + if (rewards_tree_cid is None) != (rewards_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {rewards_tree_root=} {rewards_tree_cid=}") + + strikes_tree_root = w3.csm.get_strikes_tree_root(blockstamp) + strikes_tree_cid = w3.csm.get_strikes_tree_cid(blockstamp) + + if (strikes_tree_cid is None) != (strikes_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {strikes_tree_root=} {strikes_tree_cid=}") + + return cls( + w3, + blockstamp, + rewards_tree_root, + strikes_tree_root, + rewards_tree_cid, + strikes_tree_cid, + ) + + @cached_property + def rewards(self) -> Iterable[RewardsTreeLeaf]: + if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: + logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) + return [] + + logger.info({"msg": "Fetching rewards tree by CID from IPFS", "cid": repr(self.rewards_tree_cid)}) + tree = RewardsTree.decode(self.w3.ipfs.fetch(self.rewards_tree_cid)) + + logger.info({"msg": "Restored rewards tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.rewards_tree_root: + raise ValueError("Unexpected rewards tree root got from IPFS dump") + + return tree.values + + @cached_property + def strikes(self) -> dict[StrikesValidator, StrikesList]: + if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: + logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) + return {} + + logger.info({"msg": "Fetching strikes tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) + tree = StrikesTree.decode(self.w3.ipfs.fetch(self.strikes_tree_cid)) + + logger.info({"msg": "Restored strikes tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.strikes_tree_root: + raise ValueError("Unexpected strikes tree root got from IPFS dump") + + return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index f89f4ef58..3dfef5be3 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -2,8 +2,9 @@ from collections import defaultdict from dataclasses import asdict, dataclass, field -from src.modules.csm.state import AttestationsAccumulator -from src.modules.csm.types import Shares +from src.modules.csm.state import DutyAccumulator +from src.modules.csm.types import RewardsShares +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, ValidatorIndex @@ -12,36 +13,44 @@ class LogJSONEncoder(json.JSONEncoder): ... @dataclass class ValidatorFrameSummary: - perf: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) + distributed_rewards: RewardsShares = 0 + performance: float = 0.0 + threshold: float = 0.0 + rewards_share: float = 0.0 slashed: bool = False + strikes: int = 0 + attestation_duty: DutyAccumulator = field(default_factory=DutyAccumulator) + proposal_duty: DutyAccumulator = field(default_factory=DutyAccumulator) + sync_duty: DutyAccumulator = field(default_factory=DutyAccumulator) @dataclass class OperatorFrameSummary: - distributed: int = 0 + distributed_rewards: RewardsShares = 0 + performance_coefficients: PerformanceCoefficients = field(default_factory=PerformanceCoefficients) validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) - stuck: bool = False @dataclass class FramePerfLog: """A log of performance assessed per operator in the given frame""" - blockstamp: ReferenceBlockStamp frame: tuple[EpochNumber, EpochNumber] - threshold: float = 0.0 - distributable: Shares = 0 + distributable: RewardsShares = 0 + distributed_rewards: RewardsShares = 0 + rebate_to_protocol: RewardsShares = 0 operators: dict[NodeOperatorId, OperatorFrameSummary] = field( default_factory=lambda: defaultdict(OperatorFrameSummary) ) - def encode(self) -> bytes: + @staticmethod + def encode(logs: list['FramePerfLog']) -> bytes: return ( LogJSONEncoder( indent=None, separators=(',', ':'), sort_keys=True, ) - .encode(asdict(self)) + .encode([asdict(log) for log in logs]) .encode() ) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 20ddb42f8..6076b3b15 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -2,7 +2,9 @@ import os import pickle from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field +from functools import lru_cache +from itertools import batched from pathlib import Path from typing import Self @@ -18,8 +20,8 @@ class InvalidState(ValueError): @dataclass -class AttestationsAccumulator: - """Accumulator of attestations duties observed for a validator""" +class DutyAccumulator: + """Accumulator of duties observed for a validator""" assigned: int = 0 included: int = 0 @@ -32,8 +34,41 @@ def add_duty(self, included: bool) -> None: self.assigned += 1 self.included += 1 if included else 0 + def merge(self, other: Self) -> None: + self.assigned += other.assigned + self.included += other.included + + +@dataclass +class ValidatorDuties: + attestation: DutyAccumulator | None + proposal: DutyAccumulator | None + sync: DutyAccumulator | None + + +@dataclass +class NetworkDuties: + # fmt: off + attestations: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) + proposals: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) + syncs: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) + + def merge(self, other: Self) -> None: + for val, duty in other.attestations.items(): + self.attestations[val].merge(duty) + for val, duty in other.proposals.items(): + self.proposals[val].merge(duty) + for val, duty in other.syncs.items(): + self.syncs[val].merge(duty) + + +type Frame = tuple[EpochNumber, EpochNumber] +type StateData = dict[Frame, NetworkDuties] + class State: + # pylint: disable=too-many-public-methods + """ Processing state of a CSM performance oracle frame. @@ -44,17 +79,18 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ - data: defaultdict[ValidatorIndex, AttestationsAccumulator] + data: StateData _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] - _consensus_version: int = 2 + _consensus_version: int - def __init__(self, data: dict[ValidatorIndex, AttestationsAccumulator] | None = None) -> None: - self.data = defaultdict(AttestationsAccumulator, data or {}) + def __init__(self) -> None: + self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() + self._consensus_version = 0 EXTENSION = ".pkl" @@ -67,6 +103,7 @@ def load(cls) -> Self: try: with file.open(mode="rb") as f: obj = pickle.load(f) + print({"msg": "Read object from pickle file"}) if not obj: raise ValueError("Got empty object") except Exception as e: # pylint: disable=broad-exception-caught @@ -89,14 +126,58 @@ def file(cls) -> Path: def buffer(self) -> Path: return self.file().with_suffix(".buf") + @property + def is_empty(self) -> bool: + return not self.data and not self._epochs_to_process and not self._processed_epochs + + @property + def frames(self) -> list[Frame]: + return list(self.data.keys()) + + @property + def unprocessed_epochs(self) -> set[EpochNumber]: + if not self._epochs_to_process: + raise ValueError("Epochs to process are not set") + diff = set(self._epochs_to_process) - self._processed_epochs + return diff + + @property + def is_fulfilled(self) -> bool: + return not self.unprocessed_epochs + + @staticmethod + def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: + """Split epochs to process into frames of `epochs_per_frame` length""" + if len(epochs_to_process) % epochs_per_frame != 0: + raise ValueError("Insufficient epochs to form a frame") + return [(frame[0], frame[-1]) for frame in batched(sorted(epochs_to_process), epochs_per_frame)] + def clear(self) -> None: - self.data = defaultdict(AttestationsAccumulator) + self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() + self._consensus_version = 0 assert self.is_empty - def inc(self, key: ValidatorIndex, included: bool) -> None: - self.data[key].add_duty(included) + @lru_cache(variables.CSM_ORACLE_MAX_CONCURRENCY) + def find_frame(self, epoch: EpochNumber) -> Frame: + for epoch_range in self.frames: + from_epoch, to_epoch = epoch_range + if from_epoch <= epoch <= to_epoch: + return epoch_range + raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") + + def save_att_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + frame = self.find_frame(epoch) + self.data[frame].attestations[val_index].add_duty(included) + + def save_prop_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + frame = self.find_frame(epoch) + self.data[frame].proposals[val_index].add_duty(included) + + def save_sync_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + frame = self.find_frame(epoch) + self.data[frame].syncs[val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: self._processed_epochs.add(epoch) @@ -104,8 +185,10 @@ def add_processed_epoch(self, epoch: EpochNumber) -> None: def log_progress(self) -> None: logger.info({"msg": f"Processed {len(self._processed_epochs)} of {len(self._epochs_to_process)} epochs"}) - def migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, consensus_version: int): - if consensus_version != self._consensus_version: + def migrate( + self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int + ) -> None: + if self._consensus_version and consensus_version != self._consensus_version: logger.warning( { "msg": f"Cache was built for consensus version {self._consensus_version}. " @@ -114,17 +197,40 @@ def migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, consensus_version: ) self.clear() - for state_epochs in (self._epochs_to_process, self._processed_epochs): - for epoch in state_epochs: - if epoch < l_epoch or epoch > r_epoch: - logger.warning({"msg": "Discarding invalidated state cache"}) - self.clear() - break + new_frames = self._calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + if self.frames == new_frames: + logger.info({"msg": "No need to migrate duties data cache"}) + return + self._migrate_frames_data(new_frames) + self.find_frame.cache_clear() self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.commit() + def _migrate_frames_data(self, new_frames: list[Frame]): + logger.info({"msg": f"Migrating duties data cache: {self.frames=} -> {new_frames=}"}) + new_data: StateData = {} + for frame in new_frames: + new_data[frame] = NetworkDuties() + + def overlaps(a: Frame, b: Frame): + return a[0] <= b[0] and a[1] >= b[1] + + consumed = [] + for new_frame in new_frames: + for frame_to_consume in self.frames: + if overlaps(new_frame, frame_to_consume): + assert frame_to_consume not in consumed + consumed.append(frame_to_consume) + new_data[new_frame].merge(self.data[frame_to_consume]) + for frame in self.frames: + if frame in consumed: + continue + logger.warning({"msg": f"Invalidating frame duties data cache: {frame}"}) + self._processed_epochs -= set(sequence(*frame)) + self.data = new_data + def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: raise InvalidState(f"State is not fulfilled. {self.unprocessed_epochs=}") @@ -135,41 +241,54 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: for epoch in sequence(l_epoch, r_epoch): if epoch not in self._processed_epochs: - raise InvalidState(f"Epoch {epoch} should be processed") + raise InvalidState(f"Epoch {epoch} missing in processed epochs") - @property - def is_empty(self) -> bool: - return not self.data and not self._epochs_to_process and not self._processed_epochs + def get_validator_duties(self, frame: Frame, validator_index: ValidatorIndex) -> ValidatorDuties: + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") - @property - def unprocessed_epochs(self) -> set[EpochNumber]: - if not self._epochs_to_process: - raise ValueError("Epochs to process are not set") - diff = set(self._epochs_to_process) - self._processed_epochs - return diff + att_duty = frame_data.attestations.get(validator_index) + prop_duty = frame_data.proposals.get(validator_index) + sync_duty = frame_data.syncs.get(validator_index) - @property - def is_fulfilled(self) -> bool: - return not self.unprocessed_epochs + return ValidatorDuties(att_duty, prop_duty, sync_duty) - @property - def frame(self) -> tuple[EpochNumber, EpochNumber]: - if not self._epochs_to_process: - raise ValueError("Epochs to process are not set") - return min(self._epochs_to_process), max(self._epochs_to_process) + def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: + # TODO: exclude `active_slashed` validators from the calculation + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self._get_duty_network_aggr(frame_data.attestations) + logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr + + def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self._get_duty_network_aggr(frame_data.proposals) + logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr - def get_network_aggr(self) -> AttestationsAccumulator: - """Return `AttestationsAccumulator` over duties of all the network validators""" + def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self._get_duty_network_aggr(frame_data.syncs) + logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr + @staticmethod + def _get_duty_network_aggr(duty_frame_data: defaultdict[ValidatorIndex, DutyAccumulator]) -> DutyAccumulator: included = assigned = 0 - for validator, acc in self.data.items(): + for validator, acc in duty_frame_data.items(): if acc.included > acc.assigned: raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") included += acc.included assigned += acc.assigned - aggr = AttestationsAccumulator( + aggr = DutyAccumulator( included=included, assigned=assigned, ) - logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index e38c0e36a..92abb5098 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -1,47 +1,60 @@ import json -from dataclasses import dataclass -from typing import Self, Sequence +from abc import ABC, abstractmethod +from json import JSONDecodeError, JSONDecoder, JSONEncoder +from typing import Any, ClassVar, Iterable, Self, Sequence from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree -from src.modules.csm.types import RewardTreeLeaf -from src.providers.ipfs.cid import CID +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf +from src.utils.types import hex_str_to_bytes -class TreeJSONEncoder(json.JSONEncoder): +class TreeJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, bytes): - return f"0x{o.hex()}" - if isinstance(o, CID): - return str(o) + return HexBytes(o).to_0x_hex() return super().default(o) -@dataclass -class Tree: +class TreeJSONDecoder(JSONDecoder): ... + + +class Tree[LeafType: Iterable](ABC): """A wrapper around StandardMerkleTree to cover use cases of the CSM oracle""" - tree: StandardMerkleTree[RewardTreeLeaf] + encoder: ClassVar[type[JSONEncoder]] = TreeJSONEncoder + decoder: ClassVar[type[JSONDecoder]] = TreeJSONDecoder + + tree: StandardMerkleTree[LeafType] + + def __init__(self, tree: StandardMerkleTree[LeafType]) -> None: + self.tree = tree @property def root(self) -> HexBytes: return HexBytes(self.tree.root) + @property + def values(self) -> list[LeafType]: + return [v["value"] for v in self.tree.values] + @classmethod def decode(cls, content: bytes) -> Self: """Restore a tree from a supported binary representation""" try: - return cls(StandardMerkleTree.load(json.loads(content))) - except json.JSONDecodeError as e: - raise ValueError("Unsupported tree format") from e + return cls(StandardMerkleTree.load(json.loads(content, cls=cls.decoder))) + except JSONDecodeError as e: + raise ValueError("Invalid tree's JSON") from e + except Exception as e: + raise ValueError("Unable to load tree") from e def encode(self) -> bytes: """Convert the underlying StandardMerkleTree to a binary representation""" return ( - TreeJSONEncoder( + self.encoder( indent=None, separators=(',', ':'), sort_keys=True, @@ -50,10 +63,84 @@ def encode(self) -> bytes: .encode() ) - def dump(self) -> Dump[RewardTreeLeaf]: + def dump(self) -> Dump[LeafType]: return self.tree.dump() @classmethod - def new(cls, values: Sequence[RewardTreeLeaf]) -> Self: + @abstractmethod + def new(cls, values: Sequence[LeafType]) -> Self: + raise NotImplementedError + + +class RewardsTreeJSONDecoder(TreeJSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_decode_value(key: str, obj: Any): + if key != "value": + return obj + if not isinstance(obj, list) or not len(obj) == 2: + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + no_id, shares = obj + if not isinstance(no_id, int): + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + if not isinstance(shares, int): + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + return no_id, shares + + return {k: try_decode_value(k, v) for k, v in items} + + +class RewardsTree(Tree[RewardsTreeLeaf]): + decoder = RewardsTreeJSONDecoder + + @classmethod + def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" return cls(StandardMerkleTree(values, ("uint256", "uint256"))) + + +class StrikesTreeJSONEncoder(TreeJSONEncoder): + def default(self, o): + if isinstance(o, StrikesList): + return list(o) + return super().default(o) + + +class StrikesTreeJSONDecoder(TreeJSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_decode_value(key: str, obj: Any): + if key != "value": + return obj + if not isinstance(obj, list) or not len(obj) == 3: + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + no_id, pubkey, strikes = obj + if not isinstance(no_id, int): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + if not isinstance(pubkey, str) or not pubkey.startswith("0x"): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + if not isinstance(strikes, list): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + return no_id, HexBytes(hex_str_to_bytes(pubkey)), StrikesList(strikes) + + return {k: try_decode_value(k, v) for k, v in items} + + +class StrikesTree(Tree[StrikesTreeLeaf]): + encoder = StrikesTreeJSONEncoder + decoder = StrikesTreeJSONDecoder + + @classmethod + def new(cls, values) -> Self: + """Create new instance around the wrapped tree out of the given values""" + return cls(StandardMerkleTree(values, ("uint256", "bytes", "uint256[]"))) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index d305a6e0b..2ef3bad1d 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import TypeAlias, Literal +from typing import Final, Iterable, Literal, Sequence, TypeAlias from hexbytes import HexBytes @@ -9,8 +9,48 @@ logger = logging.getLogger(__name__) -Shares: TypeAlias = int -RewardTreeLeaf: TypeAlias = tuple[NodeOperatorId, Shares] +type StrikesValidator = tuple[NodeOperatorId, HexBytes] + + +class StrikesList(Sequence[int]): + """Deque-like structure to store strikes""" + + SENTINEL: Final = 0 + + data: list[int] + + def __init__(self, data: Iterable[int] | None = None, maxlen: int | None = None) -> None: + self.data = list(data or []) + if maxlen: + self.resize(maxlen) + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, index): + return self.data[index] + + def __eq__(self, value: object, /) -> bool: + if isinstance(value, StrikesList): + return self.data == value.data + return self.data == value + + def __repr__(self) -> str: + return repr(self.data) + + def resize(self, maxlen: int) -> None: + """Update maximum length of the list""" + self.data = self.data[:maxlen] + [self.SENTINEL] * (maxlen - len(self.data)) + + def push(self, item: int) -> None: + """Push element at the beginning of the list resizing the list to keep one more item""" + self.data.insert(0, item) + + +ParticipationShares: TypeAlias = int +RewardsShares: TypeAlias = int +type RewardsTreeLeaf = tuple[NodeOperatorId, RewardsShares] +type StrikesTreeLeaf = tuple[NodeOperatorId, HexBytes, StrikesList] @dataclass @@ -21,6 +61,9 @@ class ReportData: tree_cid: CID | Literal[""] log_cid: CID distributed: int + rebate: int + strikes_tree_root: HexBytes + strikes_tree_cid: CID | Literal[""] def as_tuple(self): # Tuple with report in correct order @@ -31,4 +74,7 @@ def as_tuple(self): str(self.tree_cid), str(self.log_cid), self.distributed, + self.rebate, + self.strikes_tree_root, + str(self.strikes_tree_cid), ) diff --git a/src/modules/submodules/oracle_module.py b/src/modules/submodules/oracle_module.py index d2bd42241..ad284da2a 100644 --- a/src/modules/submodules/oracle_module.py +++ b/src/modules/submodules/oracle_module.py @@ -1,4 +1,5 @@ import logging +import signal import time import traceback from abc import abstractmethod, ABC @@ -112,6 +113,12 @@ def _cycle(self): except ValueError as error: logger.error({'msg': 'Unexpected error.', 'error': str(error)}) + @staticmethod + def _reset_cycle_timeout(): + """Reset the timeout timer for the current cycle.""" + logger.info({'msg': f'Reset running cycle timeout to {variables.MAX_CYCLE_LIFETIME_IN_SECONDS} seconds'}) + signal.setitimer(signal.ITIMER_REAL, variables.MAX_CYCLE_LIFETIME_IN_SECONDS) + @staticmethod def _sleep_cycle(): """Handles sleeping between cycles based on the configured cycle sleep time.""" diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index d6d5f9aa3..b2dc66b79 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -1,11 +1,11 @@ from http import HTTPStatus -from typing import Literal, cast - -from json_stream.base import TransientStreamingJSONObject # type: ignore +from typing import Any, Literal, cast +from src import variables from src.metrics.logging import logging from src.metrics.prometheus.basic import CL_REQUESTS_DURATION from src.providers.consensus.types import ( + BeaconSpecResponse, BeaconStateView, BlockAttestation, BlockAttestationResponse, @@ -13,13 +13,21 @@ BlockHeaderFullResponse, BlockHeaderResponseData, BlockRootResponse, - Validator, - BeaconSpecResponse, GenesisResponse, + ProposerDuties, SlotAttestationCommittee, + SyncAggregate, + SyncCommittee, + Validator, +) +from src.providers.http_provider import ( + HTTPProvider, + NotOkResponse, + data_is_dict, + data_is_list, + data_is_transient_dict, ) -from src.providers.http_provider import HTTPProvider, NotOkResponse -from src.types import BlockRoot, BlockStamp, SlotNumber, EpochNumber, StateRoot +from src.types import BlockRoot, BlockStamp, EpochNumber, SlotNumber, StateRoot from src.utils.cache import global_lru_cache as lru_cache from src.utils.dataclass import list_of_dataclasses @@ -48,6 +56,8 @@ class ConsensusClient(HTTPProvider): API_GET_BLOCK_HEADER = 'eth/v1/beacon/headers/{}' API_GET_BLOCK_DETAILS = 'eth/v2/beacon/blocks/{}' API_GET_ATTESTATION_COMMITTEES = 'eth/v1/beacon/states/{}/committees' + API_GET_SYNC_COMMITTEE = 'eth/v1/beacon/states/{}/sync_committees' + API_GET_PROPOSER_DUTIES = 'eth/v1/validator/duties/proposer/{}' API_GET_STATE = 'eth/v2/debug/beacon/states/{}' API_GET_VALIDATORS = 'eth/v1/beacon/states/{}/validators' API_GET_SPEC = 'eth/v1/config/spec' @@ -55,18 +65,14 @@ class ConsensusClient(HTTPProvider): def get_config_spec(self) -> BeaconSpecResponse: """Spec: https://ethereum.github.io/beacon-APIs/#/Config/getSpec""" - data, _ = self._get(self.API_GET_SPEC) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getSpec") + data, _ = self._get(self.API_GET_SPEC, retval_validator=data_is_dict) return BeaconSpecResponse.from_response(**data) def get_genesis(self): """ Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis """ - data, _ = self._get(self.API_GET_GENESIS) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getGenesis") + data, _ = self._get(self.API_GET_GENESIS, retval_validator=data_is_dict) return GenesisResponse.from_response(**data) def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> BlockRootResponse: @@ -79,21 +85,19 @@ def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> Blo self.API_GET_BLOCK_ROOT, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + retval_validator=data_is_dict, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockRoot") return BlockRootResponse.from_response(**data) @lru_cache(maxsize=1) def get_block_header(self, state_id: SlotNumber | BlockRoot) -> BlockHeaderFullResponse: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeader""" - data, meta_data = cast(tuple[dict, dict], self._get( + data, meta_data = self._get( self.API_GET_BLOCK_HEADER, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - )) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockHeader") + retval_validator=data_is_dict, + ) resp = BlockHeaderFullResponse.from_response(data=BlockHeaderResponseData.from_response(**data), **meta_data) return resp @@ -104,25 +108,29 @@ def get_block_details(self, state_id: SlotNumber | BlockRoot) -> BlockDetailsRes self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + retval_validator=data_is_dict, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockV2") return BlockDetailsResponse.from_response(**data) - @lru_cache(maxsize=256) - def get_block_attestations( - self, - state_id: SlotNumber | BlockRoot, - ) -> list[BlockAttestation]: + @lru_cache(maxsize=variables.CSM_ORACLE_MAX_CONCURRENCY * 32 * 2) # threads count * blocks * epochs to check duties + def get_block_attestations_and_sync( + self, state_id: SlotNumber | BlockRoot + ) -> tuple[list[BlockAttestation], SyncAggregate]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2""" data, _ = self._get( self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + retval_validator=data_is_dict, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockV2") - return [BlockAttestationResponse.from_response(**att) for att in data["message"]["body"]["attestations"]] + + attestations = [ + cast(BlockAttestation, BlockAttestationResponse.from_response(**att)) + for att in data["message"]["body"]["attestations"] + ] + sync = SyncAggregate.from_response(**data["message"]["body"]["sync_aggregate"]) + + return attestations, sync @list_of_dataclasses(SlotAttestationCommittee.from_response) def get_attestation_committees( @@ -130,7 +138,7 @@ def get_attestation_committees( blockstamp: BlockStamp, epoch: EpochNumber | None = None, committee_index: int | None = None, - slot: SlotNumber | None = None + slot: SlotNumber | None = None, ) -> list[SlotAttestationCommittee]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees""" try: @@ -138,10 +146,9 @@ def get_attestation_committees( self.API_GET_ATTESTATION_COMMITTEES, path_params=(blockstamp.state_root,), query_params={'epoch': epoch, 'index': committee_index, 'slot': slot}, - force_raise=self.__raise_on_prysm_error + force_raise=self.__raise_on_prysm_error, + retval_validator=data_is_list, ) - if not isinstance(data, list): - raise ValueError("Expected list response from getEpochCommittees") except NotOkResponse as error: if self.PRYSM_STATE_NOT_FOUND_ERROR in error.text: data = self._get_attestation_committees_with_prysm( @@ -154,14 +161,47 @@ def get_attestation_committees( raise error return cast(list[SlotAttestationCommittee], data) + def get_sync_committee(self, blockstamp: BlockStamp, epoch: EpochNumber) -> SyncCommittee: + """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochSyncCommittees""" + data, _ = self._get( + self.API_GET_SYNC_COMMITTEE, + path_params=(blockstamp.state_root,), + query_params={'epoch': epoch}, + force_raise=self.__raise_on_prysm_error, + retval_validator=data_is_dict, + ) + return SyncCommittee.from_response(**data) + + @list_of_dataclasses(ProposerDuties.from_response) + def get_proposer_duties(self, epoch: EpochNumber, expected_dependent_root: BlockRoot) -> list[ProposerDuties]: + """Spec: https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties""" + + def data_is_list_and_dependent_root_matches(data: Any, meta: dict, endpoint: str): + data_is_list(data, meta, endpoint=endpoint) + # It is recommended by spec to use the dependent root to ensure the epoch is correct + if meta["dependent_root"] != expected_dependent_root: + raise ValueError( + "Dependent root for proposer duties request mismatch: " + f"{meta['dependent_root']=} is not {expected_dependent_root=}. " + "Probably, CL node is not fully synced." + ) + + data, _ = self._get( + self.API_GET_PROPOSER_DUTIES, + path_params=(epoch,), + retval_validator=data_is_list_and_dependent_root_matches, + ) + return data + @lru_cache(maxsize=1) def get_state_block_roots(self, state_id: SlotNumber) -> list[BlockRoot]: - streamed_json = cast(TransientStreamingJSONObject, self._get( + data, _ = self._get( self.API_GET_STATE, path_params=(state_id,), stream=True, - )) - return list(streamed_json['data']['block_roots']) + retval_validator=data_is_transient_dict, + ) + return list(data["block_roots"]) def get_validators(self, blockstamp: BlockStamp) -> list[Validator]: return self.get_state_view(blockstamp).indexed_validators @@ -201,11 +241,9 @@ def _get_state_by_state_id(self, state_id: StateRoot | SlotNumber) -> dict: data, _ = self._get( self.API_GET_STATE, path_params=(state_id,), - stream=False, force_raise=self.__raise_on_prysm_error, + retval_validator=data_is_dict, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getStateV2") return data def __raise_on_prysm_error(self, errors: list[Exception]) -> Exception | None: @@ -224,7 +262,7 @@ def _get_attestation_committees_with_prysm( blockstamp: BlockStamp, epoch: EpochNumber | None = None, index: int | None = None, - slot: SlotNumber | None = None + slot: SlotNumber | None = None, ) -> list[dict]: # Avoid Prysm issue with state root - https://github.com/prysmaticlabs/prysm/issues/12053 # Trying to get committees by slot number @@ -232,9 +270,8 @@ def _get_attestation_committees_with_prysm( self.API_GET_ATTESTATION_COMMITTEES, path_params=(blockstamp.slot_number,), query_params={'epoch': epoch, 'index': index, 'slot': slot}, + retval_validator=data_is_dict, ) - if not isinstance(data, list): - raise ValueError("Expected list response from getEpochCommittees") return data def __raise_last_missed_slot_error(self, errors: list[Exception]) -> Exception | None: @@ -250,7 +287,9 @@ def __raise_last_missed_slot_error(self, errors: list[Exception]) -> Exception | return None def _get_chain_id_with_provider(self, provider_index: int) -> int: - data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.API_GET_SPEC) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getSpec") + data, _ = self._get_without_fallbacks( + self.hosts[provider_index], + self.API_GET_SPEC, + retval_validator=data_is_dict, + ) return BeaconSpecResponse.from_response(**data).DEPOSIT_CHAIN_ID diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index adaa6826b..5b6fb42ef 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -3,10 +3,12 @@ from typing import Protocol from eth_typing import BlockNumber +from hexbytes import HexBytes from web3.types import Timestamp from src.types import BlockHash, BlockRoot, CommitteeIndex, EpochNumber, Gwei, SlotNumber, StateRoot, ValidatorIndex from src.utils.dataclass import FromResponse, Nested +from src.utils.types import hex_str_to_bytes @dataclass @@ -96,10 +98,16 @@ class BlockAttestation(Protocol): data: AttestationData +@dataclass +class SyncAggregate(FromResponse): + sync_committee_bits: str + + @dataclass class BeaconBlockBody(Nested, FromResponse): execution_payload: ExecutionPayload attestations: list[BlockAttestationResponse] + sync_aggregate: SyncAggregate @dataclass @@ -129,6 +137,10 @@ class Validator(Nested, FromResponse): balance: Gwei validator: ValidatorState + @property + def pubkey(self) -> HexBytes: + return HexBytes(hex_str_to_bytes(self.validator.pubkey)) + @dataclass class BlockDetailsResponse(Nested, FromResponse): @@ -178,3 +190,15 @@ def indexed_validators(self) -> list[Validator]: ) for (i, v) in enumerate(self.validators) ] + + +@dataclass +class SyncCommittee(Nested, FromResponse): + validators: list[ValidatorIndex] + + +@dataclass +class ProposerDuties(Nested, FromResponse): + pubkey: str + validator_index: ValidatorIndex + slot: SlotNumber diff --git a/src/providers/execution/contracts/cs_accounting.py b/src/providers/execution/contracts/cs_accounting.py index 786ea7f27..7fc73f942 100644 --- a/src/providers/execution/contracts/cs_accounting.py +++ b/src/providers/execution/contracts/cs_accounting.py @@ -4,6 +4,9 @@ from web3 import Web3 from web3.types import BlockIdentifier +from src.types import NodeOperatorId +from src.utils.cache import global_lru_cache as lru_cache + from ..base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -24,3 +27,17 @@ def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> Check } ) return Web3.to_checksum_address(resp) + + @lru_cache() + def get_bond_curve_id(self, node_operator_id: NodeOperatorId, block_identifier: BlockIdentifier = "latest") -> int: + """Returns the curve ID""" + + resp = self.functions.getBondCurveId(node_operator_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getBondCurveId({node_operator_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return resp diff --git a/src/providers/execution/contracts/cs_fee_distributor.py b/src/providers/execution/contracts/cs_fee_distributor.py index 937c7dc49..8a556b250 100644 --- a/src/providers/execution/contracts/cs_fee_distributor.py +++ b/src/providers/execution/contracts/cs_fee_distributor.py @@ -3,7 +3,7 @@ from eth_typing import ChecksumAddress from hexbytes import HexBytes from web3 import Web3 -from web3.types import BlockIdentifier +from web3.types import BlockIdentifier, Wei from ..base_interface import ContractInterface @@ -26,7 +26,7 @@ def oracle(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddres ) return Web3.to_checksum_address(resp) - def shares_to_distribute(self, block_identifier: BlockIdentifier = "latest") -> int: + def shares_to_distribute(self, block_identifier: BlockIdentifier = "latest") -> Wei: """Returns the amount of shares that are pending to be distributed""" resp = self.functions.pendingSharesToDistribute().call(block_identifier=block_identifier) diff --git a/src/providers/execution/contracts/cs_fee_oracle.py b/src/providers/execution/contracts/cs_fee_oracle.py index 05d78d687..15d0c042f 100644 --- a/src/providers/execution/contracts/cs_fee_oracle.py +++ b/src/providers/execution/contracts/cs_fee_oracle.py @@ -1,5 +1,7 @@ import logging +from eth_typing import ChecksumAddress +from web3 import Web3 from web3.types import BlockIdentifier from src.providers.execution.contracts.base_oracle import BaseOracleContract @@ -23,15 +25,15 @@ def is_paused(self, block_identifier: BlockIdentifier = "latest") -> bool: ) return resp - def perf_leeway_bp(self, block_identifier: BlockIdentifier = "latest") -> int: - """Performance threshold leeway used to determine underperforming validators""" + def strikes(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: + """Return the address of the CSStrikes contract""" - resp = self.functions.avgPerfLeewayBP().call(block_identifier=block_identifier) + resp = self.functions.STRIKES().call(block_identifier=block_identifier) logger.info( { - "msg": "Call `avgPerfLeewayBP()`.", + "msg": "Call `STRIKES()`.", "value": resp, "block_identifier": repr(block_identifier), } ) - return resp + return Web3.to_checksum_address(resp) diff --git a/src/providers/execution/contracts/cs_module.py b/src/providers/execution/contracts/cs_module.py index 2b0b37e29..2ac59f55e 100644 --- a/src/providers/execution/contracts/cs_module.py +++ b/src/providers/execution/contracts/cs_module.py @@ -1,5 +1,4 @@ import logging -from typing import NamedTuple from eth_typing import ChecksumAddress from web3 import Web3 @@ -12,19 +11,6 @@ logger = logging.getLogger(__name__) -class NodeOperatorSummary(NamedTuple): - """getNodeOperatorSummary response, @see IStakingModule.sol""" - - targetLimitMode: int - targetValidatorsCount: int - stuckValidatorsCount: int - refundedValidatorsCount: int - stuckPenaltyEndTimestamp: int - totalExitedValidators: int - totalDepositedValidators: int - depositableValidatorsCount: int - - class CSModuleContract(ContractInterface): abi_path = "./assets/CSModule.json" @@ -43,6 +29,19 @@ def accounting(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAd ) return Web3.to_checksum_address(resp) + def parameters_registry(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: + """Returns the address of the CSParametersRegistry contract""" + + resp = self.functions.PARAMETERS_REGISTRY().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `PARAMETERS_REGISTRY()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return Web3.to_checksum_address(resp) + def is_paused(self, block: BlockIdentifier = "latest") -> bool: resp = self.functions.isPaused().call(block_identifier=block) logger.info( diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py new file mode 100644 index 000000000..1dc7b0631 --- /dev/null +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -0,0 +1,149 @@ +import logging +from collections import UserList +from dataclasses import dataclass + +from web3.types import BlockIdentifier + +from src.constants import TOTAL_BASIS_POINTS, ATTESTATIONS_WEIGHT, BLOCKS_WEIGHT, SYNC_WEIGHT +from src.modules.csm.state import ValidatorDuties +from src.providers.execution.base_interface import ContractInterface +from src.utils.cache import global_lru_cache as lru_cache + +logger = logging.getLogger(__name__) + + +@dataclass +class PerformanceCoefficients: + attestations_weight: int = ATTESTATIONS_WEIGHT + blocks_weight: int = BLOCKS_WEIGHT + sync_weight: int = SYNC_WEIGHT + + def calc_performance(self, duties: ValidatorDuties) -> float: + base = 0 + performance = 0.0 + + if duties.attestation: + base += self.attestations_weight + performance += duties.attestation.perf * self.attestations_weight + + if duties.proposal: + base += self.blocks_weight + performance += duties.proposal.perf * self.blocks_weight + + if duties.sync: + base += self.sync_weight + performance += duties.sync.perf * self.sync_weight + + performance /= base + + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + + return performance + + +@dataclass +class KeyNumberValueInterval: + minKeyNumber: int + value: int + + +class KeyNumberValueIntervalList(UserList[KeyNumberValueInterval]): + + def get_for(self, key_number: int) -> float: + if key_number < 1: + raise ValueError("Key number should be greater than 1 or equal") + for interval in sorted(self, key=lambda x: x.minKeyNumber, reverse=True): + if key_number >= interval.minKeyNumber: + return interval.value / TOTAL_BASIS_POINTS + raise ValueError(f"No value found for key number={key_number}") + + +@dataclass +class StrikesParams: + lifetime: int + threshold: int + + +@dataclass +class CurveParams: + perf_coeffs: PerformanceCoefficients + perf_leeway_data: KeyNumberValueIntervalList + reward_share_data: KeyNumberValueIntervalList + strikes_params: StrikesParams + + +class CSParametersRegistryContract(ContractInterface): + abi_path = "./assets/CSParametersRegistry.json" + + @lru_cache() + def get_performance_coefficients( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest", + ) -> PerformanceCoefficients: + """Returns performance coefficients for given node operator""" + + resp = self.functions.getPerformanceCoefficients(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceCoefficients({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return PerformanceCoefficients(*resp) + + @lru_cache() + def get_reward_share_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest", + ) -> KeyNumberValueIntervalList: + """Returns reward share data for given node operator""" + + resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getRewardShareData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return KeyNumberValueIntervalList([KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) + + @lru_cache() + def get_performance_leeway_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest", + ) -> KeyNumberValueIntervalList: + """Returns performance leeway data for given node operator""" + + resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceLeewayData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return KeyNumberValueIntervalList([KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) + + @lru_cache() + def get_strikes_params( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest", + ) -> StrikesParams: + """Returns strikes params for a given curve id""" + + resp = self.functions.getStrikesParams(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getStrikesParams({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return StrikesParams(*resp) diff --git a/src/providers/execution/contracts/cs_strikes.py b/src/providers/execution/contracts/cs_strikes.py new file mode 100644 index 000000000..05b5ac81d --- /dev/null +++ b/src/providers/execution/contracts/cs_strikes.py @@ -0,0 +1,38 @@ +import logging + +from hexbytes import HexBytes +from web3.types import BlockIdentifier + +from ..base_interface import ContractInterface + +logger = logging.getLogger(__name__) + + +class CSStrikesContract(ContractInterface): + abi_path = "./assets/CSStrikes.json" + + def tree_root(self, block_identifier: BlockIdentifier = "latest") -> HexBytes: + """Root of the latest published Merkle tree""" + + resp = self.functions.treeRoot().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `treeRoot()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return HexBytes(resp) + + def tree_cid(self, block_identifier: BlockIdentifier = "latest") -> str: + """CID of the latest published Merkle tree""" + + resp = self.functions.treeCid().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `treeCid()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return resp diff --git a/src/providers/http_provider.py b/src/providers/http_provider.py index 0dfd38914..4af9c347f 100644 --- a/src/providers/http_provider.py +++ b/src/providers/http_provider.py @@ -1,14 +1,14 @@ import logging from abc import ABC from http import HTTPStatus -from typing import Sequence, Callable +from typing import Any, Callable, NoReturn, Protocol, Sequence from urllib.parse import urljoin, urlparse # NOTE: Missing library stubs or py.typed marker. That's why we use `type: ignore` from json_stream import requests as json_stream_requests # type: ignore -from json_stream.base import TransientStreamingJSONList, TransientStreamingJSONObject # type: ignore +from json_stream.base import TransientStreamingJSONObject # type: ignore from prometheus_client import Histogram -from requests import Session, JSONDecodeError +from requests import JSONDecodeError, Session from requests.adapters import HTTPAdapter from urllib3 import Retry @@ -31,10 +31,34 @@ def __init__(self, *args, status: int, text: str): super().__init__(*args) +class ReturnValueValidator(Protocol): + def __call__(self, data: Any, meta: dict, *, endpoint: str) -> None | NoReturn: ... + + +def data_is_any(data: Any, meta: dict, *, endpoint: str): + pass + + +def data_is_dict(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, dict): + raise ValueError(f"Expected mapping response from {endpoint}") + + +def data_is_list(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, list): + raise ValueError(f"Expected list response from {endpoint}") + + +def data_is_transient_dict(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, TransientStreamingJSONObject): + raise ValueError(f"Expected mapping response from {endpoint}") + + class HTTPProvider(ProviderConsistencyModule, ABC): """ Base HTTP Provider with metrics and retry strategy integrated inside. """ + PROMETHEUS_HISTOGRAM: Histogram request_timeout: int @@ -78,8 +102,9 @@ def _get( path_params: Sequence[str | int] | None = None, query_params: dict | None = None, force_raise: Callable[..., Exception | None] = lambda _: None, + retval_validator: ReturnValueValidator = data_is_any, stream: bool = False, - ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: + ) -> tuple[Any, dict]: """ Get plain or streamed request with fallbacks Returns (data, meta) or raises exception @@ -91,7 +116,14 @@ def _get( for host in self.hosts: try: - return self._get_without_fallbacks(host, endpoint, path_params, query_params, stream) + return self._get_without_fallbacks( + host, + endpoint, + path_params, + query_params, + stream=stream, + retval_validator=retval_validator, + ) except Exception as e: # pylint: disable=W0703 errors.append(e) @@ -117,10 +149,11 @@ def _get_without_fallbacks( path_params: Sequence[str | int] | None = None, query_params: dict | None = None, stream: bool = False, - ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: + retval_validator: ReturnValueValidator = data_is_any, + ) -> tuple[Any, dict]: """ Simple get request without fallbacks - Returns (data, meta) or streamed transient list-like or dict-like object JSON or raises exception + Returns (data, meta) or raises an exception """ complete_endpoint = endpoint.format(*path_params) if path_params else endpoint @@ -130,7 +163,7 @@ def _get_without_fallbacks( self._urljoin(host, complete_endpoint if path_params else endpoint), params=query_params, timeout=self.request_timeout, - stream=stream + stream=stream, ) except Exception as error: logger.error({'msg': str(error)}) @@ -155,11 +188,12 @@ def _get_without_fallbacks( logger.debug({'msg': response_fail_msg}) raise self.PROVIDER_EXCEPTION(response_fail_msg, status=response.status_code, text=response.text) - if stream: - return json_stream_requests.load(response) - try: - json_response = response.json() + if stream: + # There's no guarantee the JSON is valid at this point. + json_response = json_stream_requests.load(response) + else: + json_response = response.json() except JSONDecodeError as error: response_fail_msg = ( f'Failed to decode JSON response from {complete_endpoint} with text: "{str(response.text)}"' @@ -167,14 +201,19 @@ def _get_without_fallbacks( logger.debug({'msg': response_fail_msg}) raise self.PROVIDER_EXCEPTION(status=0, text='JSON decode error.') from error - if 'data' in json_response: - data = json_response['data'] - del json_response['data'] - meta = json_response - else: + try: + data = json_response["data"] + meta = {} + + if not stream: + del json_response["data"] + meta = json_response + except KeyError: + # NOTE: Used by KeysAPIClient only. data = json_response meta = {} + retval_validator(data, meta, endpoint=endpoint) return data, meta def get_all_providers(self) -> list[str]: diff --git a/src/providers/keys/client.py b/src/providers/keys/client.py index ae0ba65a6..70252071b 100644 --- a/src/providers/keys/client.py +++ b/src/providers/keys/client.py @@ -1,9 +1,9 @@ from time import sleep -from typing import cast, TypedDict, List +from typing import List, TypedDict, cast -from src.metrics.prometheus.basic import KEYS_API_REQUESTS_DURATION, KEYS_API_LATEST_BLOCKNUMBER -from src.providers.http_provider import HTTPProvider, NotOkResponse -from src.providers.keys.types import LidoKey, KeysApiStatus +from src.metrics.prometheus.basic import KEYS_API_LATEST_BLOCKNUMBER, KEYS_API_REQUESTS_DURATION +from src.providers.http_provider import HTTPProvider, NotOkResponse, data_is_dict +from src.providers.keys.types import KeysApiStatus, LidoKey from src.types import BlockStamp, StakingModuleAddress from src.utils.cache import global_lru_cache as lru_cache @@ -32,6 +32,7 @@ class KeysAPIClient(HTTPProvider): Keys API specification can be found here https://keys-api.lido.fi/api/static/index.html """ + PROMETHEUS_HISTOGRAM = KEYS_API_REQUESTS_DURATION PROVIDER_EXCEPTION = KAPIClientError @@ -53,15 +54,19 @@ def _get_with_blockstamp(self, url: str, blockstamp: BlockStamp, params: dict | if i != self.retry_count - 1: sleep(self.backoff_factor) - raise KeysOutdatedException(f'Keys API Service stuck, no updates for {self.backoff_factor * self.retry_count} seconds.') + raise KeysOutdatedException( + f'Keys API Service stuck, no updates for {self.backoff_factor * self.retry_count} seconds.' + ) @lru_cache(maxsize=1) def get_used_lido_keys(self, blockstamp: BlockStamp) -> list[LidoKey]: """Docs: https://keys-api.lido.fi/api/static/index.html#/keys/KeysController_get""" - return list(map(lambda x: LidoKey.from_response(**x), self._get_with_blockstamp(self.USED_KEYS, blockstamp))) + return [LidoKey.from_response(**x) for x in self._get_with_blockstamp(self.USED_KEYS, blockstamp)] @lru_cache(maxsize=1) - def get_module_operators_keys(self, module_address: StakingModuleAddress, blockstamp: BlockStamp) -> ModuleOperatorsKeys: + def get_module_operators_keys( + self, module_address: StakingModuleAddress, blockstamp: BlockStamp + ) -> ModuleOperatorsKeys: """ Docs: https://keys-api.lido.fi/api/static/index.html#/operators-keys/SRModulesOperatorsKeysController_getOperatorsKeys """ @@ -71,9 +76,9 @@ def get_module_operators_keys(self, module_address: StakingModuleAddress, blocks def get_status(self) -> KeysApiStatus: """Docs: https://keys-api.lido.fi/api/static/index.html#/status/StatusController_get""" - data, _ = self._get(self.STATUS) - return KeysApiStatus.from_response(**cast(dict, data)) + data, _ = self._get(self.STATUS, retval_validator=data_is_dict) + return KeysApiStatus.from_response(**data) def _get_chain_id_with_provider(self, provider_index: int) -> int: - data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS) - return KeysApiStatus.from_response(**cast(dict, data)).chainId + data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS, retval_validator=data_is_dict) + return KeysApiStatus.from_response(**data).chainId diff --git a/src/variables.py b/src/variables.py index 846d1e6cb..ca36f1f61 100644 --- a/src/variables.py +++ b/src/variables.py @@ -29,7 +29,7 @@ CSM_MODULE_ADDRESS: Final = os.getenv('CSM_MODULE_ADDRESS') FINALIZATION_BATCH_MAX_REQUEST_COUNT: Final = int(os.getenv('FINALIZATION_BATCH_MAX_REQUEST_COUNT', 1000)) EL_REQUESTS_BATCH_SIZE: Final = int(os.getenv('EL_REQUESTS_BATCH_SIZE', 500)) -CSM_ORACLE_MAX_CONCURRENCY: Final = int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2)) or None +CSM_ORACLE_MAX_CONCURRENCY: Final = min(32, int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2))) # We add some gas to the transaction to be sure that we have enough gas to execute corner cases # eg when we tried to submit a few reports in a single block diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 78e0669a2..4262e2c03 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -1,16 +1,12 @@ import logging from functools import partial -from itertools import groupby from time import sleep -from typing import Callable, Iterator, cast +from typing import cast -from eth_typing import BlockNumber from hexbytes import HexBytes from web3 import Web3 -from web3.contract.contract import ContractEvent from web3.exceptions import Web3Exception from web3.module import Module -from web3.types import BlockIdentifier, EventData from src import variables from src.metrics.prometheus.business import FRAME_PREV_REPORT_REF_SLOT @@ -18,11 +14,11 @@ from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract, CurveParams +from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 -from src.types import BlockStamp, SlotNumber -from src.utils.events import get_events_in_range from src.utils.lazy_object_proxy import LazyObjectProxy -from src.web3py.extensions.lido_validators import NodeOperatorId +from src.types import BlockStamp, NodeOperatorId, SlotNumber logger = logging.getLogger(__name__) @@ -31,8 +27,14 @@ class CSM(Module): w3: Web3 oracle: CSFeeOracleContract + accounting: CSAccountingContract fee_distributor: CSFeeDistributorContract + strikes: CSStrikesContract module: CSModuleContract + params: CSParametersRegistryContract + + CONTRACT_LOAD_MAX_RETRIES: int = 100 + CONTRACT_LOAD_RETRY_DELAY: int = 60 def __init__(self, w3: Web3) -> None: super().__init__(w3) @@ -43,79 +45,103 @@ def get_csm_last_processing_ref_slot(self, blockstamp: BlockStamp) -> SlotNumber FRAME_PREV_REPORT_REF_SLOT.labels("csm_oracle").set(result) return result - def get_csm_tree_root(self, blockstamp: BlockStamp) -> HexBytes: + def get_rewards_tree_root(self, blockstamp: BlockStamp) -> HexBytes: return self.fee_distributor.tree_root(blockstamp.block_hash) - def get_csm_tree_cid(self, blockstamp: BlockStamp) -> CID | None: + def get_rewards_tree_cid(self, blockstamp: BlockStamp) -> CID | None: result = self.fee_distributor.tree_cid(blockstamp.block_hash) if result == "": return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) - def get_operators_with_stucks_in_range( - self, - l_block: BlockIdentifier, - r_block: BlockIdentifier, - ) -> Iterator[NodeOperatorId]: - l_block_number = self.w3.eth.get_block(l_block).get("number", BlockNumber(0)) - r_block_number = self.w3.eth.get_block(r_block).get("number", BlockNumber(0)) - - by_no_id: Callable[[EventData], int] = lambda e: e["args"]["nodeOperatorId"] - - events = sorted( - get_events_in_range( - cast(ContractEvent, self.module.events.StuckSigningKeysCountChanged), - l_block_number, - r_block_number, - ), - key=by_no_id, - ) - - for no_id, group in groupby(events, key=by_no_id): - if any(e["args"]["stuckKeysCount"] > 0 for e in group): - yield NodeOperatorId(no_id) + def get_strikes_tree_root(self, blockstamp: BlockStamp) -> HexBytes: + return self.strikes.tree_root(blockstamp.block_hash) + + def get_strikes_tree_cid(self, blockstamp: BlockStamp) -> CID | None: + result = self.strikes.tree_cid(blockstamp.block_hash) + if result == "": + return None + return CIDv0(result) if is_cid_v0(result) else CIDv1(result) + + def get_curve_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> CurveParams: + curve_id = self.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) + perf_coeffs = self.params.get_performance_coefficients(curve_id, blockstamp.block_hash) + perf_leeway_data = self.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) + reward_share_data = self.params.get_reward_share_data(curve_id, blockstamp.block_hash) + strikes_params = self.params.get_strikes_params(curve_id, blockstamp.block_hash) + return CurveParams(perf_coeffs, perf_leeway_data, reward_share_data, strikes_params) def _load_contracts(self) -> None: - try: - self.module = cast( - CSModuleContract, - self.w3.eth.contract( - address=variables.CSM_MODULE_ADDRESS, # type: ignore - ContractFactoryClass=CSModuleContract, - decode_tuples=True, - ), - ) - - accounting = cast( - CSAccountingContract, - self.w3.eth.contract( - address=self.module.accounting(), - ContractFactoryClass=CSAccountingContract, - decode_tuples=True, - ), - ) - - self.fee_distributor = cast( - CSFeeDistributorContract, - self.w3.eth.contract( - address=accounting.fee_distributor(), - ContractFactoryClass=CSFeeDistributorContract, - decode_tuples=True, - ), - ) - - self.oracle = cast( - CSFeeOracleContract, - self.w3.eth.contract( - address=self.fee_distributor.oracle(), - ContractFactoryClass=CSFeeOracleContract, - decode_tuples=True, - ), - ) - except Web3Exception as ex: - logger.error({"msg": "Some of the contracts aren't healthy", "error": str(ex)}) - sleep(60) - self._load_contracts() + last_error = None + + for attempt in range(self.CONTRACT_LOAD_MAX_RETRIES): + try: + self.module = cast( + CSModuleContract, + self.w3.eth.contract( + address=variables.CSM_MODULE_ADDRESS, # type: ignore + ContractFactoryClass=CSModuleContract, + decode_tuples=True, + ), + ) + + self.params = cast( + CSParametersRegistryContract, + self.w3.eth.contract( + address=self.module.parameters_registry(), + ContractFactoryClass=CSParametersRegistryContract, + decode_tuples=True, + ), + ) + + self.accounting = cast( + CSAccountingContract, + self.w3.eth.contract( + address=self.module.accounting(), + ContractFactoryClass=CSAccountingContract, + decode_tuples=True, + ), + ) + + self.fee_distributor = cast( + CSFeeDistributorContract, + self.w3.eth.contract( + address=self.accounting.fee_distributor(), + ContractFactoryClass=CSFeeDistributorContract, + decode_tuples=True, + ), + ) + + self.oracle = cast( + CSFeeOracleContract, + self.w3.eth.contract( + address=self.fee_distributor.oracle(), + ContractFactoryClass=CSFeeOracleContract, + decode_tuples=True, + ), + ) + + self.strikes = cast( + CSStrikesContract, + self.w3.eth.contract( + address=self.oracle.strikes(), + ContractFactoryClass=CSStrikesContract, + decode_tuples=True, + ), + ) + return + except Web3Exception as e: + last_error = e + logger.error({ + "msg": f"Attempt {attempt + 1}/{self.CONTRACT_LOAD_MAX_RETRIES} failed to load contracts", + "error": str(e) + }) + sleep(self.CONTRACT_LOAD_RETRY_DELAY) + + raise Web3Exception( + f"Failed to load contracts in CSM module " + f"after {self.CONTRACT_LOAD_MAX_RETRIES} attempts" + ) from last_error class LazyCSM(CSM): diff --git a/tests/__init__.py b/tests/__init__.py index 66b133d69..58d696a01 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,5 +6,5 @@ line = line.strip() if line.startswith("#") or not line: continue - key, value = line.split("=") + key, value = line.split("=", maxsplit=1) os.environ[key] = value diff --git a/tests/fork/conftest.py b/tests/fork/conftest.py index 8a6867e84..349b141be 100644 --- a/tests/fork/conftest.py +++ b/tests/fork/conftest.py @@ -1,4 +1,5 @@ import json +import logging import subprocess import time from contextlib import contextmanager @@ -6,6 +7,7 @@ from typing import cast, get_args import pytest +import xdist from _pytest.nodes import Item from eth_account import Account from faker.proxy import Faker @@ -14,7 +16,7 @@ from web3_multi_provider import MultiProvider from src import variables -from src.main import ipfs_providers, logger +from src.main import ipfs_providers from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule from src.modules.submodules.types import FrameConfig @@ -35,7 +37,9 @@ from src.web3py.contract_tweak import tweak_w3_contracts from src.web3py.extensions import KeysAPIClientModule, LazyCSM, LidoContracts, LidoValidatorsProvider, TransactionUtils -logger = logger.getChild("fork") +logger = logging.getLogger('fork_tests') + +# pylint: disable=logging-fstring-interpolation class TestRunningException(Exception): @@ -55,6 +59,12 @@ def pytest_collection_modifyitems(items: list[Item]): ) +def pytest_sessionfinish(session, exitstatus): + if xdist.is_xdist_worker(session): + return + subprocess.run(['rm', '-rf', './testruns'], check=True) + + # # Global # @@ -84,6 +94,20 @@ def set_delay_and_sleep(monkeypatch): yield +@pytest.fixture(autouse=True) +def patch_csm_contract_load(monkeypatch): + monkeypatch.setattr( + "src.web3py.extensions.CSM.CONTRACT_LOAD_MAX_RETRIES", + 3, + ) + monkeypatch.setattr( + "src.web3py.extensions.CSM.CONTRACT_LOAD_RETRY_DELAY", + 0, + ) + logger.info("TESTRUN Patched CSM CONTRACT_LOAD_MAX_RETRIES to 3 and CONTRACT_LOAD_RETRY_DELAY to 0") + yield + + @pytest.fixture(autouse=True) def set_cache_path(monkeypatch, testrun_path): with monkeypatch.context(): @@ -96,10 +120,15 @@ def set_cache_path(monkeypatch, testrun_path): yield -@pytest.fixture -def testrun_path(worker_id, testrun_uid): - path = f"./testrun_{worker_id}_{testrun_uid}" - subprocess.run(['mkdir', path], check=True) +@pytest.fixture(scope='session') +def testruns_folder_path(): + return Path("./testruns") + + +@pytest.fixture() +def testrun_path(testruns_folder_path, worker_id, testrun_uid): + path = testruns_folder_path / f"{worker_id}_{testrun_uid}" + subprocess.run(['mkdir', '-p', path], check=True) yield path subprocess.run(['rm', '-rf', path], check=True) @@ -177,7 +206,7 @@ def frame_config(initial_epoch, epochs_per_frame, fast_lane_length_slots): return _frame_config -@pytest.fixture(params=[-2], ids=["fork 2 epochs before initial epoch"]) +@pytest.fixture(params=[-4], ids=["fork 4 epochs before initial epoch"]) def blockstamp_for_forking( request, frame_config: FrameConfig, real_cl_client: ConsensusClient, real_finalized_slot: SlotNumber ) -> BlockStamp: @@ -193,12 +222,16 @@ def blockstamp_for_forking( @pytest.fixture() -def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str): - port = Faker().random_int(min=10000, max=20000) +def anvil_port(): + return Faker().random_int(min=10000, max=20000) + + +@pytest.fixture() +def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str, anvil_port: int): cli_params = [ 'anvil', '--port', - str(port), + str(anvil_port), '--config-out', f'{testrun_path}/localhost.json', '--auto-impersonate', @@ -209,14 +242,8 @@ def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str): ] with subprocess.Popen(cli_params, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) as process: time.sleep(5) - logger.info(f"TESTRUN Started fork on {port=} from {blockstamp_for_forking.block_number=}") - web3 = Web3( - MultiProvider( - endpoint_urls=[f'http://127.0.0.1:{port}'], - request_kwargs={'timeout': 5 * 60}, - cache_allowed_requests=True, - ) - ) + logger.info(f"TESTRUN Started fork on {anvil_port=} from {blockstamp_for_forking.block_number=}") + web3 = Web3(MultiProvider([f'http://127.0.0.1:{anvil_port}'], request_kwargs={'timeout': 5 * 60})) tweak_w3_contracts(web3) web3.provider.make_request(RPCEndpoint('anvil_setBlockTimestampInterval'), [12]) web3.provider.make_request(RPCEndpoint('evm_setAutomine'), [True]) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index c36d99d70..634d84145 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -1,10 +1,15 @@ +import os +import subprocess +from pathlib import Path + import pytest from src.modules.csm.csm import CSOracle from src.modules.submodules.types import FrameConfig from src.utils.range import sequence from src.web3py.types import Web3 -from tests.fork.conftest import first_slot_of_epoch +from tests.fork.conftest import first_slot_of_epoch, logger +from tests.fork.utils.lock import LockedDir @pytest.fixture() @@ -13,8 +18,88 @@ def hash_consensus_bin(): yield f.read() +@pytest.fixture(scope='session') +def csm_repo_path(testruns_folder_path): + return Path(testruns_folder_path) / "community-staking-module" + + +@pytest.fixture(scope='session') +def prepared_csm_repo(testruns_folder_path, csm_repo_path): + + if os.environ.get("GITHUB_ACTIONS") == "true": + # CI should have the repo cloned and prepared + if os.path.exists(csm_repo_path): + return csm_repo_path + raise ValueError("No cloned community-staking-module repo found, but running in CI. Fix the workflow.") + + original_dir = os.getcwd() + + with LockedDir(testruns_folder_path): + if not os.path.exists(csm_repo_path / ".prepared"): + logger.info("TESTRUN Cloning community-staking-module repo") + subprocess.run( + ["git", "clone", "https://github.com/lidofinance/community-staking-module", csm_repo_path], check=True + ) + os.chdir(csm_repo_path) + subprocess.run(["git", "checkout", "develop"], check=True) + subprocess.run(["just", "deps"], check=True) + subprocess.run(["just", "build"], check=True) + subprocess.run(["touch", ".prepared"], check=True) + os.chdir(original_dir) + + return csm_repo_path + + +@pytest.fixture() +def update_csm_to_v2(accounts_from_fork, forked_el_client: Web3, anvil_port: int, prepared_csm_repo: Path): + original_dir = os.getcwd() + + chain = 'mainnet' + + logger.info("TESTRUN Deploying CSM v2") + _, pks = accounts_from_fork + deployer, *_ = pks + + os.chdir(prepared_csm_repo) + + with subprocess.Popen( + ['just', '_deploy-impl', '--broadcast', '--private-key', deployer], + env={ + **os.environ, + "ANVIL_PORT": str(anvil_port), + 'CHAIN': chain, + }, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) as process: + process.wait() + assert process.returncode == 0, "Failed to deploy CSM v2" + logger.info("TESTRUN Deployed CSM v2") + + logger.info("TESTRUN Updating to CSM v2") + with subprocess.Popen( + ['just', "vote-upgrade"], + env={ + **os.environ, + 'CHAIN': chain, + "ANVIL_PORT": str(anvil_port), + "RPC_URL": f"http://127.0.0.1:{anvil_port}", + 'DEPLOY_CONFIG': f'./artifacts/local/upgrade-{chain}.json', + }, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) as process: + process.wait() + assert process.returncode == 0, "Failed to update to CSM v2" + logger.info("TESTRUN Updated to CSM v2") + + os.chdir(original_dir) + # TODO: update ABIs in `assets` folder? + forked_el_client.provider.make_request("anvil_autoImpersonateAccount", [True]) + + @pytest.fixture() -def csm_module(web3: Web3): +def csm_module(web3: Web3, update_csm_to_v2): yield CSOracle(web3) diff --git a/tests/fork/test_lido_oracle_cycle.py b/tests/fork/test_lido_oracle_cycle.py index d13c68bb4..30fad386e 100644 --- a/tests/fork/test_lido_oracle_cycle.py +++ b/tests/fork/test_lido_oracle_cycle.py @@ -56,6 +56,13 @@ def missed_initial_frame(frame_config: FrameConfig): indirect=True, ) def test_lido_module_report(module, set_oracle_members, running_finalized_slots, account_from): + # Skip if consensus version is different + current_consensus_version = module.report_contract.get_consensus_version() + if current_consensus_version != module.COMPATIBLE_CONSENSUS_VERSION: + pytest.skip( + f"Consensus version {current_consensus_version} does not match expected {module.COMPATIBLE_CONSENSUS_VERSION}" + ) + assert module.report_contract.get_last_processing_ref_slot() == 0, "Last processing ref slot should be 0" members = set_oracle_members(count=2) diff --git a/tests/fork/utils/lock.py b/tests/fork/utils/lock.py new file mode 100644 index 000000000..773a6025e --- /dev/null +++ b/tests/fork/utils/lock.py @@ -0,0 +1,23 @@ +import os +import subprocess +import time +from pathlib import Path + + +class LockedDir: + def __init__(self, path): + self.path = path + self._lock_file = ".locked" + self._lock_file_path = Path(self.path) / self._lock_file + + def __enter__(self): + while os.path.exists(self._lock_file_path): + time.sleep(1) + subprocess.run(["mkdir", "-p", self.path], check=True) + subprocess.run(["touch", self._lock_file_path], check=True) + + def __exit__(self, exc_type, exc_val, exc_tb): + subprocess.run(["rm", self._lock_file_path], check=True) + + def is_unlocked(self): + return not os.path.exists(self._lock_file_path) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 13ccd13f8..5e3b94700 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,11 +1,17 @@ from typing import cast import pytest -from web3 import Web3, HTTPProvider +from web3 import HTTPProvider, Web3 from src import variables from src.providers.execution.contracts.accounting_oracle import AccountingOracleContract from src.providers.execution.contracts.burner import BurnerContract +from src.providers.execution.contracts.cs_accounting import CSAccountingContract +from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract +from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract +from src.providers.execution.contracts.cs_module import CSModuleContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract +from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract from src.providers.execution.contracts.lido import LidoContract from src.providers.execution.contracts.lido_locator import LidoLocatorContract @@ -28,6 +34,7 @@ def web3_provider_integration(request): def get_contract(w3, contract_class, address): + assert address, "No address given" return cast( contract_class, w3.eth.contract( @@ -117,3 +124,62 @@ def burner_contract(web3_provider_integration, lido_locator_contract): BurnerContract, lido_locator_contract.burner(), ) + + +# ╔══════════════════════════════════════════════════════════════════════════════════════════════════╗ +# ║ CSM contracts ║ +# ╚══════════════════════════════════════════════════════════════════════════════════════════════════╝ + + +@pytest.fixture +def cs_module_contract(web3_provider_integration): + return get_contract( + web3_provider_integration, + CSModuleContract, + "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F", # mainnet deploy + ) + + +@pytest.fixture +def cs_accounting_contract(web3_provider_integration, cs_module_contract): + return get_contract( + web3_provider_integration, + CSAccountingContract, + cs_module_contract.accounting(), + ) + + +@pytest.fixture +def cs_params_contract(web3_provider_integration, cs_module_contract): + return get_contract( + web3_provider_integration, + CSParametersRegistryContract, + cs_module_contract.parameters_registry(), + ) + + +@pytest.fixture +def cs_fee_distributor_contract(web3_provider_integration, cs_accounting_contract): + return get_contract( + web3_provider_integration, + CSFeeDistributorContract, + cs_accounting_contract.fee_distributor(), + ) + + +@pytest.fixture +def cs_fee_oracle_contract(web3_provider_integration, cs_fee_distributor_contract): + return get_contract( + web3_provider_integration, + CSFeeOracleContract, + cs_fee_distributor_contract.oracle(), + ) + + +@pytest.fixture +def cs_strikes_contract(web3_provider_integration, cs_fee_oracle_contract): + return get_contract( + web3_provider_integration, + CSStrikesContract, + cs_fee_oracle_contract.strikes(), + ) diff --git a/tests/integration/contracts/contract_utils.py b/tests/integration/contracts/contract_utils.py index f0bda942a..3c2a7a4b4 100644 --- a/tests/integration/contracts/contract_utils.py +++ b/tests/integration/contracts/contract_utils.py @@ -1,17 +1,22 @@ import logging import re -from typing import Any, Callable +from typing import Any, Callable, Type -from src.providers.execution.base_interface import ContractInterface +from eth_typing import Address, ChecksumAddress +from src.providers.execution.base_interface import ContractInterface HASH_REGREX = re.compile(r'^0x[0-9,A-F]{64}$', flags=re.IGNORECASE) ADDRESS_REGREX = re.compile('^0x[0-9,A-F]{40}$', flags=re.IGNORECASE) +type FuncName = str +type FuncArgs = tuple +type FuncResp = Any + def check_contract( contract: ContractInterface, - functions_spec: list[tuple[str, tuple | None, Callable[[Any], None]]], + functions_spec: list[tuple[FuncName, FuncArgs | None, Callable[[FuncResp], None]]], caplog, ): caplog.set_level(logging.DEBUG) @@ -29,9 +34,20 @@ def check_contract( assert len(functions_spec) == len(log_with_call) -def check_value_re(regrex, value) -> None: - assert regrex.findall(value) +def check_is_instance_of(type_: Type) -> Callable[[FuncArgs], None]: + if type_ is Address or type_ is ChecksumAddress: + return lambda resp: check_is_address(resp) and check_value_type(resp, type_) + return lambda resp: check_value_type(resp, type_) + +def check_value_type(value, type_) -> None: + assert isinstance(value, type_), f"Got invalid type={type(value)}, expected={repr(type_)}" -def check_value_type(value, _type) -> None: - assert isinstance(value, _type) + +def check_is_address(resp: FuncResp) -> None: + assert isinstance(resp, str), "address should be returned as a string" + check_value_re(ADDRESS_REGREX, resp) + + +def check_value_re(regrex, value) -> None: + assert regrex.findall(value), f"{value=} doesn't match {regrex=}" diff --git a/tests/integration/contracts/test_accounting_oracle.py b/tests/integration/contracts/test_accounting_oracle.py index d1a354f53..adb41541d 100644 --- a/tests/integration/contracts/test_accounting_oracle.py +++ b/tests/integration/contracts/test_accounting_oracle.py @@ -2,7 +2,7 @@ from web3.contract.contract import ContractFunction from src.modules.accounting.types import AccountingProcessingState -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -10,9 +10,9 @@ def test_accounting_oracle_contract(accounting_oracle_contract, caplog): check_contract( accounting_oracle_contract, [ - ('get_processing_state', None, lambda response: check_value_type(response, AccountingProcessingState)), - ('submit_report_extra_data_empty', None, lambda tx: check_value_type(tx, ContractFunction)), - ('submit_report_extra_data_list', (b'',), lambda tx: check_value_type(tx, ContractFunction)), + ('get_processing_state', None, check_is_instance_of(AccountingProcessingState)), + ('submit_report_extra_data_empty', None, check_is_instance_of(ContractFunction)), + ('submit_report_extra_data_list', (b'',), check_is_instance_of(ContractFunction)), ], caplog, ) diff --git a/tests/integration/contracts/test_bunker.py b/tests/integration/contracts/test_bunker.py index b8d4bef94..146810450 100644 --- a/tests/integration/contracts/test_bunker.py +++ b/tests/integration/contracts/test_bunker.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import SharesRequestedToBurn -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,7 +9,7 @@ def test_burner(burner_contract, caplog): check_contract( burner_contract, [ - ('get_shares_requested_to_burn', None, lambda response: check_value_type(response, SharesRequestedToBurn)), + ('get_shares_requested_to_burn', None, check_is_instance_of(SharesRequestedToBurn)), ], caplog, ) diff --git a/tests/integration/contracts/test_cs_accounting.py b/tests/integration/contracts/test_cs_accounting.py new file mode 100644 index 000000000..60ae9ee65 --- /dev/null +++ b/tests/integration/contracts/test_cs_accounting.py @@ -0,0 +1,16 @@ +import pytest +from eth_typing import ChecksumAddress + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_accounting(cs_accounting_contract, caplog): + check_contract( + cs_accounting_contract, + [ + ("fee_distributor", None, check_is_instance_of(ChecksumAddress)), + ("get_bond_curve_id", (0,), check_is_instance_of(int)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_fee_distributor.py b/tests/integration/contracts/test_cs_fee_distributor.py new file mode 100644 index 000000000..4f8c44fe6 --- /dev/null +++ b/tests/integration/contracts/test_cs_fee_distributor.py @@ -0,0 +1,19 @@ +import pytest +from eth_typing import ChecksumAddress +from hexbytes import HexBytes + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_fee_distributor(cs_fee_distributor_contract, caplog): + check_contract( + cs_fee_distributor_contract, + [ + ("oracle", None, check_is_instance_of(ChecksumAddress)), + ("shares_to_distribute", None, check_is_instance_of(int)), + ("tree_root", None, check_is_instance_of(HexBytes)), + ("tree_cid", None, check_is_instance_of(str)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_fee_oracle.py b/tests/integration/contracts/test_cs_fee_oracle.py new file mode 100644 index 000000000..659bd283e --- /dev/null +++ b/tests/integration/contracts/test_cs_fee_oracle.py @@ -0,0 +1,28 @@ +import pytest +from eth_typing import ChecksumAddress +from web3.exceptions import ContractLogicError + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_fee_oracle(cs_fee_oracle_contract, caplog): + check_contract( + cs_fee_oracle_contract, + [ + ("is_paused", None, check_is_instance_of(bool)), + ], + caplog, + ) + + +@pytest.mark.integration +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_fee_oracle_v2(cs_fee_oracle_contract, caplog): + check_contract( + cs_fee_oracle_contract, + [ + ("strikes", None, check_is_instance_of(ChecksumAddress)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_module.py b/tests/integration/contracts/test_cs_module.py new file mode 100644 index 000000000..567189b28 --- /dev/null +++ b/tests/integration/contracts/test_cs_module.py @@ -0,0 +1,31 @@ +import pytest +from eth_typing import ChecksumAddress +from web3.exceptions import ContractLogicError + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +@pytest.mark.mainnet +def test_cs_module(cs_module_contract, caplog): + check_contract( + cs_module_contract, + [ + ("accounting", None, check_is_instance_of(ChecksumAddress)), + ("is_paused", None, check_is_instance_of(bool)), + ], + caplog, + ) + + +@pytest.mark.integration +@pytest.mark.mainnet +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_module_v2(cs_module_contract, caplog): + check_contract( + cs_module_contract, + [ + ("parameters_registry", None, check_is_instance_of(ChecksumAddress)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py new file mode 100644 index 000000000..accd2eedd --- /dev/null +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -0,0 +1,24 @@ +import pytest +from web3.exceptions import ContractLogicError + +from src.providers.execution.contracts.cs_parameters_registry import ( + PerformanceCoefficients, + KeyNumberValueIntervalList, + StrikesParams, +) +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_parameters_registry(cs_params_contract, caplog): + check_contract( + cs_params_contract, + [ + ("get_performance_coefficients", None, check_is_instance_of(PerformanceCoefficients)), + ("get_reward_share_data", None, check_is_instance_of(KeyNumberValueIntervalList)), + ("get_performance_leeway_data", None, check_is_instance_of(KeyNumberValueIntervalList)), + ("get_strikes_params", None, check_is_instance_of(StrikesParams)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_strikes.py b/tests/integration/contracts/test_cs_strikes.py new file mode 100644 index 000000000..360bc4bf6 --- /dev/null +++ b/tests/integration/contracts/test_cs_strikes.py @@ -0,0 +1,18 @@ +import pytest +from hexbytes import HexBytes +from web3.exceptions import ContractLogicError + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_strikes(cs_strikes_contract, caplog): + check_contract( + cs_strikes_contract, + [ + ("tree_root", None, check_is_instance_of(HexBytes)), + ("tree_cid", None, check_is_instance_of(str)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_csm_extension.py b/tests/integration/contracts/test_csm_extension.py new file mode 100644 index 000000000..ea8fbdc9e --- /dev/null +++ b/tests/integration/contracts/test_csm_extension.py @@ -0,0 +1,23 @@ +from unittest.mock import Mock + +import pytest + +from src.web3py.extensions.csm import CSM +from src.web3py.types import Web3 + + +@pytest.fixture +def w3(web3_provider_integration): + web3_provider_integration.attach_modules({"csm": CSM}) + return web3_provider_integration + + +@pytest.mark.integration +@pytest.mark.skip("CSM v2 is not yet live") +def test_csm_extension(w3: Web3): + w3.csm.get_csm_last_processing_ref_slot(Mock(block_hash="latest")) + w3.csm.get_rewards_tree_root(Mock(block_hash="latest")) + w3.csm.get_rewards_tree_cid(Mock(block_hash="latest")) + w3.csm.get_curve_params(Mock(0), Mock(block_hash="latest")) + w3.csm.get_strikes_tree_root(Mock(block_hash="latest")) + w3.csm.get_strikes_tree_cid(Mock(block_hash="latest")) diff --git a/tests/integration/contracts/test_lido_locator.py b/tests/integration/contracts/test_lido_locator.py index ef61a2728..bb6d00a22 100644 --- a/tests/integration/contracts/test_lido_locator.py +++ b/tests/integration/contracts/test_lido_locator.py @@ -1,7 +1,7 @@ import pytest from eth_typing import ChecksumAddress -from tests.integration.contracts.contract_utils import check_contract, check_value_type, check_value_re, ADDRESS_REGREX +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,66 +9,16 @@ def test_lido_locator_contract(lido_locator_contract, caplog): check_contract( lido_locator_contract, [ - ( - 'lido', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'accounting_oracle', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'staking_router', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'validator_exit_bus_oracle', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'withdrawal_queue', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'oracle_report_sanity_checker', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'oracle_daemon_config', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'burner', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'withdrawal_vault', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'el_rewards_vault', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), + ('lido', None, check_is_instance_of(ChecksumAddress)), + ('accounting_oracle', None, check_is_instance_of(ChecksumAddress)), + ('staking_router', None, check_is_instance_of(ChecksumAddress)), + ('validator_exit_bus_oracle', None, check_is_instance_of(ChecksumAddress)), + ('withdrawal_queue', None, check_is_instance_of(ChecksumAddress)), + ('oracle_report_sanity_checker', None, check_is_instance_of(ChecksumAddress)), + ('oracle_daemon_config', None, check_is_instance_of(ChecksumAddress)), + ('burner', None, check_is_instance_of(ChecksumAddress)), + ('withdrawal_vault', None, check_is_instance_of(ChecksumAddress)), + ('el_rewards_vault', None, check_is_instance_of(ChecksumAddress)), ], caplog, ) diff --git a/tests/integration/contracts/test_oracle_daemon_config.py b/tests/integration/contracts/test_oracle_daemon_config.py index aa627a521..d07e85aa9 100644 --- a/tests/integration/contracts/test_oracle_daemon_config.py +++ b/tests/integration/contracts/test_oracle_daemon_config.py @@ -9,7 +9,6 @@ def test_oracle_daemon_config_contract(oracle_daemon_config_contract, caplog): check_contract( oracle_daemon_config_contract, [ - ('normalized_cl_reward_per_epoch', None, lambda response: check_value_type(response, int)), ( 'normalized_cl_reward_mistake_rate_bp', None, diff --git a/tests/integration/contracts/test_oracle_report_sanity_checker.py b/tests/integration/contracts/test_oracle_report_sanity_checker.py index 0b69b3fde..aa99c5343 100644 --- a/tests/integration/contracts/test_oracle_report_sanity_checker.py +++ b/tests/integration/contracts/test_oracle_report_sanity_checker.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import OracleReportLimits -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,7 +9,7 @@ def test_oracle_report_sanity_checker(oracle_report_sanity_checker_contract, cap check_contract( oracle_report_sanity_checker_contract, [ - ('get_oracle_report_limits', None, lambda response: check_value_type(response, OracleReportLimits)), + ('get_oracle_report_limits', None, check_is_instance_of(OracleReportLimits)), ], caplog, ) diff --git a/tests/integration/contracts/test_validator_exit_bus_oracle.py b/tests/integration/contracts/test_validator_exit_bus_oracle.py index d89bad8e1..49675451e 100644 --- a/tests/integration/contracts/test_validator_exit_bus_oracle.py +++ b/tests/integration/contracts/test_validator_exit_bus_oracle.py @@ -1,7 +1,7 @@ import pytest from src.modules.ejector.types import EjectorProcessingState -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of, check_value_type @pytest.mark.integration @@ -9,12 +9,13 @@ def test_vebo(validators_exit_bus_oracle_contract, caplog): check_contract( validators_exit_bus_oracle_contract, [ - ('is_paused', None, lambda response: check_value_type(response, bool)), - ('get_processing_state', None, lambda response: check_value_type(response, EjectorProcessingState)), + ('is_paused', None, check_is_instance_of(bool)), + ('get_processing_state', None, check_is_instance_of(EjectorProcessingState)), ( 'get_last_requested_validator_indices', (1, [1]), - lambda response: check_value_type(response, list) and map(lambda val: check_value_type(val, int)), + lambda response: check_value_type(response, list) + and map(lambda val: check_value_type(val, int), response), ), ], caplog, diff --git a/tests/integration/contracts/test_withdrawal_queue_nft_contract.py b/tests/integration/contracts/test_withdrawal_queue_nft_contract.py index 2abcf811e..5506b54d5 100644 --- a/tests/integration/contracts/test_withdrawal_queue_nft_contract.py +++ b/tests/integration/contracts/test_withdrawal_queue_nft_contract.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import BatchState, WithdrawalRequestStatus -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,13 +9,13 @@ def test_withdrawal_queue(withdrawal_queue_nft_contract, caplog): check_contract( withdrawal_queue_nft_contract, [ - ('unfinalized_steth', None, lambda response: check_value_type(response, int)), - ('bunker_mode_since_timestamp', None, lambda response: check_value_type(response, int)), - ('get_last_finalized_request_id', None, lambda response: check_value_type(response, int)), - ('get_withdrawal_status', (1,), lambda response: check_value_type(response, WithdrawalRequestStatus)), - ('get_last_request_id', None, lambda response: check_value_type(response, int)), - ('is_paused', None, lambda response: check_value_type(response, bool)), - ('max_batches_length', None, lambda response: check_value_type(response, int)), + ('unfinalized_steth', None, check_is_instance_of(int)), + ('bunker_mode_since_timestamp', None, check_is_instance_of(int)), + ('get_last_finalized_request_id', None, check_is_instance_of(int)), + ('get_withdrawal_status', (1,), check_is_instance_of(WithdrawalRequestStatus)), + ('get_last_request_id', None, check_is_instance_of(int)), + ('is_paused', None, check_is_instance_of(bool)), + ('max_batches_length', None, check_is_instance_of(int)), ( 'calculate_finalization_batches', ( @@ -67,7 +67,7 @@ def test_withdrawal_queue(withdrawal_queue_nft_contract, caplog): ), "latest", ), - lambda response: check_value_type(response, BatchState), + check_is_instance_of(BatchState), ), ], caplog, diff --git a/tests/modules/accounting/test_accounting_module.py b/tests/modules/accounting/test_accounting_module.py index 1f09cdb03..aac6fe512 100644 --- a/tests/modules/accounting/test_accounting_module.py +++ b/tests/modules/accounting/test_accounting_module.py @@ -507,7 +507,7 @@ def test_accounting_get_processing_state_no_yet_init_epoch(accounting: Accountin assert isinstance(processing_state, AccountingProcessingState) assert processing_state.current_frame_ref_slot == 100 assert processing_state.processing_deadline_time == 200 - assert processing_state.main_data_submitted == False + assert processing_state.main_data_submitted is False assert processing_state.main_data_hash == ZERO_HASH diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 9975488df..de3496545 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -1,10 +1,11 @@ from copy import deepcopy from typing import cast -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest -from faker import Faker +import src.modules.csm.checkpoint as checkpoint_module +from src.constants import EPOCHS_PER_SYNC_COMMITTEE_PERIOD from src.modules.csm.checkpoint import ( FrameCheckpoint, FrameCheckpointProcessor, @@ -15,6 +16,14 @@ from src.modules.csm.state import State from src.modules.submodules.types import ChainConfig, FrameConfig from src.providers.consensus.client import ConsensusClient +from src.providers.consensus.types import ( + BeaconSpecResponse, + BlockAttestation, + SlotAttestationCommittee, + SyncCommittee, + SyncAggregate, +) +from src.types import EpochNumber, SlotNumber, ValidatorIndex, BlockRoot from src.providers.consensus.types import BeaconSpecResponse, BlockAttestation, SlotAttestationCommittee from src.types import SlotNumber, ValidatorIndex from src.utils.web3converter import Web3Converter @@ -26,6 +35,14 @@ FrameConfigFactory, SlotAttestationCommitteeFactory, ) +from src.modules.csm.checkpoint import ( + FrameCheckpointProcessor, + ValidatorDuty, + SlotNumber, + SlotOutOfRootsRange, + SYNC_COMMITTEES_CACHE, + SyncCommitteesCache, +) @pytest.fixture(autouse=True) @@ -117,7 +134,7 @@ def test_checkpoints_iterator_given_checkpoints(converter, l_epoch, r_epoch, fin @pytest.fixture def consensus_client(): - return ConsensusClient('http://localhost', 5 * 60, 5, 5) + return ConsensusClient('http://localhost/', 5 * 60, 5, 5) @pytest.fixture @@ -187,9 +204,12 @@ def test_checkpoints_processor_select_block_roots( finalized_blockstamp, ) roots = processor._get_block_roots(0) - selected = processor._select_block_roots(10, roots, 8192) - assert len(selected) == 64 - assert selected == [f'0x{r}' for r in range(320, 384)] + selected = processor._select_block_roots(roots, 10, 8192) + duty_epoch_roots, next_epoch_roots = selected + assert len(duty_epoch_roots) == 32 + assert len(next_epoch_roots) == 32 + assert duty_epoch_roots == [(r, f'0x{r}') for r in range(320, 352)] + assert next_epoch_roots == [(r, f'0x{r}') for r in range(352, 384)] @pytest.mark.unit @@ -205,8 +225,8 @@ def test_checkpoints_processor_select_block_roots_out_of_range( finalized_blockstamp, ) roots = processor._get_block_roots(0) - with pytest.raises(ValueError, match="Slot is out of the state block roots range"): - processor._select_block_roots(255, roots, 8192) + with pytest.raises(checkpoint_module.SlotOutOfRootsRange, match="Slot is out of the state block roots range"): + processor._select_block_roots(roots, 255, 8192) @pytest.fixture() @@ -238,7 +258,7 @@ def test_checkpoints_processor_prepare_committees(mock_get_attestation_committee finalized_blockstamp, ) raw = consensus_client.get_attestation_committees(0, 0) - committees = processor._prepare_committees(0) + committees = processor._prepare_attestation_duties(0) assert len(committees) == 2048 for index, (committee_id, validators) in enumerate(committees.items()): slot, committee_index = committee_id @@ -260,7 +280,7 @@ def test_checkpoints_processor_process_attestations(mock_get_attestation_committ converter, finalized_blockstamp, ) - committees = processor._prepare_committees(0) + committees = processor._prepare_attestation_duties(0) # normal attestation attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 0 @@ -294,7 +314,7 @@ def test_checkpoints_processor_process_attestations_undefined_committee( converter, finalized_blockstamp, ) - committees = processor._prepare_committees(0) + committees = processor._prepare_attestation_duties(0) # undefined committee attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 100500 @@ -306,96 +326,239 @@ def test_checkpoints_processor_process_attestations_undefined_committee( assert v.included is False -@pytest.fixture() -def mock_get_block_attestations(consensus_client, faker: Faker): - def _get_block_attestations(root): - slot = faker.random_int() - attestations = [] - for i in range(0, 64): - attestation = deepcopy(cast(BlockAttestation, BlockAttestationFactory.build())) - attestation.data.slot = SlotNumber(slot) - attestation.data.index = i - attestation.aggregation_bits = '0x' + 'f' * 32 - attestations.append(attestation) - return attestations - - consensus_client.get_block_attestations = Mock(side_effect=_get_block_attestations) +@pytest.fixture +def frame_checkpoint_processor(): + cc = Mock() + state = Mock() + converter = Mock() + finalized_blockstamp = Mock(slot_number=SlotNumber(0)) + return FrameCheckpointProcessor(cc, state, converter, finalized_blockstamp) @pytest.mark.unit -def test_checkpoints_processor_check_duty( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, +def test_check_duties_processes_epoch_with_attestations_and_sync_committee(frame_checkpoint_processor): + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + duty_epoch = EpochNumber(10) + duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] + next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] + frame_checkpoint_processor._prepare_attestation_duties = Mock( + return_value={SlotNumber(100): [ValidatorDuty(1, False)]} ) - roots = processor._get_block_roots(0) - processor._check_duty(0, roots[:64]) - assert len(state._processed_epochs) == 1 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 255 - assert len(state.data) == 2048 * 32 + frame_checkpoint_processor._prepare_propose_duties = Mock( + return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} + ) + frame_checkpoint_processor._prepare_sync_committee_duties = Mock( + return_value={ + 100: [ValidatorDuty(1, False) for _ in range(32)], + 101: [ValidatorDuty(1, False) for _ in range(32)], + } + ) + + attestation = Mock() + attestation.data.slot = SlotNumber(100) + attestation.data.index = 0 + attestation.aggregation_bits = "0xff" + attestation.committee_bits = "0xff" + + sync_aggregate = Mock() + sync_aggregate.sync_committee_bits = "0xff" + + frame_checkpoint_processor.cc.get_block_attestations_and_sync = Mock(return_value=([attestation], sync_aggregate)) + frame_checkpoint_processor.state.unprocessed_epochs = [duty_epoch] + + frame_checkpoint_processor._check_duties( + checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots + ) + + frame_checkpoint_processor.state.save_att_duty.assert_called() + frame_checkpoint_processor.state.save_sync_duty.assert_called() + frame_checkpoint_processor.state.save_prop_duty.assert_called() @pytest.mark.unit -def test_checkpoints_processor_process( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, +def test_check_duties_processes_epoch_with_no_attestations(frame_checkpoint_processor): + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + duty_epoch = EpochNumber(10) + duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] + next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] + frame_checkpoint_processor._prepare_attestation_duties = Mock(return_value={}) + frame_checkpoint_processor._prepare_propose_duties = Mock( + return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} + ) + frame_checkpoint_processor._prepare_sync_committee_duties = Mock( + return_value={100: [ValidatorDuty(1, False)], 101: [ValidatorDuty(1, False)]} + ) + + sync_aggregate = Mock() + sync_aggregate.sync_committee_bits = "0x00" + + frame_checkpoint_processor.cc.get_block_attestations_and_sync = Mock(return_value=([], sync_aggregate)) + frame_checkpoint_processor.state.unprocessed_epochs = [duty_epoch] + + frame_checkpoint_processor._check_duties( + checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots ) - roots = processor._get_block_roots(0) - processor._process([0, 1], {0: roots[:64], 1: roots[32:96]}) - assert len(state._processed_epochs) == 2 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 254 - assert len(state.data) == 2048 * 32 + + assert frame_checkpoint_processor.state.save_att_duty.call_count == 0 + assert frame_checkpoint_processor.state.save_sync_duty.call_count == 2 + assert frame_checkpoint_processor.state.save_prop_duty.call_count == 2 @pytest.mark.unit -def test_checkpoints_processor_exec( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, +def test_prepare_sync_committee_returns_duties_for_valid_sync_committee(frame_checkpoint_processor): + epoch = EpochNumber(10) + duty_block_roots = [(SlotNumber(100), Mock()), (SlotNumber(101), Mock())] + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) + + duties = frame_checkpoint_processor._prepare_sync_committee_duties(epoch, duty_block_roots) + + expected_duties = { + SlotNumber(100): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ], + SlotNumber(101): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ], + } + assert duties == expected_duties + + +@pytest.mark.unit +def test_prepare_sync_committee_skips_duties_for_missed_slots(frame_checkpoint_processor): + epoch = EpochNumber(10) + duty_block_roots = [(SlotNumber(100), None), (SlotNumber(101), Mock())] + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) + + duties = frame_checkpoint_processor._prepare_sync_committee_duties(epoch, duty_block_roots) + + expected_duties = { + SlotNumber(101): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ] + } + assert duties == expected_duties + + +@pytest.mark.unit +def test_get_sync_committee_returns_cached_sync_committee(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + cached_sync_committee = Mock(spec=SyncCommittee) + + with patch('src.modules.csm.checkpoint.SYNC_COMMITTEES_CACHE', {sync_committee_period: cached_sync_committee}): + result = frame_checkpoint_processor._get_sync_committee(epoch) + assert result == cached_sync_committee + + +@pytest.mark.unit +def test_get_sync_committee_fetches_and_caches_when_not_cached(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor.converter.get_epoch_first_slot = Mock(return_value=SlotNumber(0)) + frame_checkpoint_processor.cc.get_sync_committee = Mock(return_value=sync_committee) + + prev_slot_response = Mock() + prev_slot_response.message.slot = SlotNumber(0) + prev_slot_response.message.body.execution_payload.block_hash = "0x00" + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + result = frame_checkpoint_processor._get_sync_committee(epoch) + + assert result.validators == sync_committee.validators + assert SYNC_COMMITTEES_CACHE[sync_committee_period].validators == sync_committee.validators + + +@pytest.mark.unit +def test_get_sync_committee_handles_cache_eviction(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + old_sync_committee_period = sync_committee_period - 1 + old_sync_committee = Mock(spec=SyncCommittee) + sync_committee = Mock(spec=SyncCommittee) + frame_checkpoint_processor.converter.get_epoch_first_slot = Mock(return_value=SlotNumber(0)) + frame_checkpoint_processor.cc.get_sync_committee = Mock(return_value=sync_committee) + + with patch('src.modules.csm.checkpoint.SYNC_COMMITTEES_CACHE', SyncCommitteesCache()) as cache: + cache.max_size = 1 + cache[old_sync_committee_period] = old_sync_committee + + prev_slot_response = Mock() + prev_slot_response.message.slot = SlotNumber(0) + prev_slot_response.message.body.execution_payload.block_hash = "0x00" + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + result = frame_checkpoint_processor._get_sync_committee(epoch) + + assert result == sync_committee + assert sync_committee_period in SYNC_COMMITTEES_CACHE + assert old_sync_committee_period not in SYNC_COMMITTEES_CACHE + + +@pytest.mark.unit +def test_prepare_propose_duties(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_root = Mock(spec=BlockRoot) + frame_checkpoint_processor._get_dependent_root_for_proposer_duties = Mock(return_value=dependent_root) + proposer_duty1 = Mock(slot=SlotNumber(101), validator_index=1) + proposer_duty2 = Mock(slot=SlotNumber(102), validator_index=2) + frame_checkpoint_processor.cc.get_proposer_duties = Mock(return_value=[proposer_duty1, proposer_duty2]) + + duties = frame_checkpoint_processor._prepare_propose_duties(epoch, checkpoint_block_roots, checkpoint_slot) + + expected_duties = { + SlotNumber(101): ValidatorDuty(validator_index=1, included=False), + SlotNumber(102): ValidatorDuty(validator_index=2, included=False), + } + assert duties == expected_duties + + +@pytest.mark.unit +def test_get_dependent_root_for_proposer_duties_from_state_block_roots(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_slot = SlotNumber(99) + frame_checkpoint_processor.converter.get_epoch_last_slot = Mock(return_value=dependent_slot) + frame_checkpoint_processor._select_block_root_by_slot = Mock(return_value=checkpoint_block_roots[2]) + + dependent_root = frame_checkpoint_processor._get_dependent_root_for_proposer_duties( + epoch, checkpoint_block_roots, checkpoint_slot ) - iterator = FrameCheckpointsIterator(converter, 0, 1, 255) - for checkpoint in iterator: - processor.exec(checkpoint) - assert len(state._processed_epochs) == 2 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 254 - assert len(state.data) == 2048 * 32 + + assert dependent_root == checkpoint_block_roots[2] + + +@pytest.mark.unit +def test_get_dependent_root_for_proposer_duties_from_cl_when_slot_out_of_range(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_slot = SlotNumber(99) + frame_checkpoint_processor.converter.get_epoch_last_slot = Mock(return_value=dependent_slot) + frame_checkpoint_processor._select_block_root_by_slot = Mock(side_effect=SlotOutOfRootsRange) + non_missed_slot = SlotNumber(98) + + prev_slot_response = Mock() + prev_slot_response.message.slot = non_missed_slot + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + frame_checkpoint_processor.cc.get_block_root = Mock(return_value=Mock(root=checkpoint_block_roots[0])) + + dependent_root = frame_checkpoint_processor._get_dependent_root_for_proposer_duties( + epoch, checkpoint_block_roots, checkpoint_slot + ) + + assert dependent_root == checkpoint_block_roots[0] diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py new file mode 100644 index 000000000..87b40a90c --- /dev/null +++ b/tests/modules/csm/test_csm_distribution.py @@ -0,0 +1,1190 @@ +import re +from collections import defaultdict +from unittest.mock import Mock + +import pytest +from hexbytes import HexBytes +from web3.types import Wei + +from src.constants import TOTAL_BASIS_POINTS +from src.modules.csm.distribution import Distribution, ValidatorDuties, ValidatorDutiesOutcome +from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary, OperatorFrameSummary +from src.modules.csm.state import DutyAccumulator, State, NetworkDuties, Frame +from src.modules.csm.types import StrikesList +from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract +from src.providers.execution.contracts.cs_parameters_registry import ( + StrikesParams, + PerformanceCoefficients, + CurveParams, + KeyNumberValueInterval, + KeyNumberValueIntervalList, +) +from src.providers.execution.exceptions import InconsistentData +from src.types import NodeOperatorId, EpochNumber, ValidatorIndex, ReferenceBlockStamp +from src.web3py.extensions import CSM +from src.web3py.types import Web3 +from tests.factory.blockstamp import ReferenceBlockStampFactory +from tests.factory.no_registry import LidoValidatorFactory, ValidatorStateFactory + + +@pytest.mark.parametrize( + ( + "frames", + "last_report", + "mocked_curve_params", + "frame_blockstamps", + "shares_to_distribute", + "distribution_in_frame", + "expected_total_rewards", + "expected_total_rewards_map", + "expected_total_rebate", + "expected_strikes", + ), + [ + # One frame + ( + [(0, 31)], + Mock( + strikes={(NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1])}, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 500, + {NodeOperatorId(1): 1000}, + 0, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 1, 0, 0, 0, 1]}, + ), + # One frame, no strikes and rewards before + ( + [(0, 31)], + Mock(strikes={}, rewards=[]), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 500, + {NodeOperatorId(1): 500}, + 0, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 0, 0, 0, 0, 0]}, + ), + # Multiple frames + ( + [(0, 31), (32, 63), (64, 95)], + Mock( + strikes={ + (NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1]), + (NodeOperatorId(100500), HexBytes("0x100500")): StrikesList([0, 0, 0, 1, 1, 1]), + }, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ + ReferenceBlockStampFactory.build(ref_epoch=31), + ReferenceBlockStampFactory.build(ref_epoch=63), + ReferenceBlockStampFactory.build(ref_epoch=95), + ], + [ + 500, + 500 + 700, + 500 + 700 + 300, + ], + [ + ( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ), + ( + # rewards + {NodeOperatorId(1): 700}, + # distributed_rewards + 700, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(2), HexBytes("0x02")): 1}, + ), + ( + # rewards + {NodeOperatorId(1): 300}, + # distributed_rewards + 300, + # rebate_to_protocol + 0, + # strikes + {}, + ), + ], + 1500, + {NodeOperatorId(1): 2000}, + 0, + { + (NodeOperatorId(1), HexBytes("0x01")): [0, 0, 1, 1, 0, 0], + (NodeOperatorId(2), HexBytes("0x02")): [0, 1, 0, 0, 0, 0], + }, + ), + # One frame with no distribution + ( + [(0, 31)], + Mock( + strikes={(NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1])}, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 0, + {NodeOperatorId(1): 500}, + 0, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 1, 0, 0, 0, 1]}, + ), + # Multiple frames, some of which are not distributed + ( + [(0, 31), (32, 63), (64, 95)], + Mock( + strikes={ + (NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1]), + (NodeOperatorId(100500), HexBytes("0x100500")): StrikesList([0, 0, 0, 1, 1, 1]), + }, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ + ReferenceBlockStampFactory.build(ref_epoch=31), + ReferenceBlockStampFactory.build(ref_epoch=63), + ReferenceBlockStampFactory.build(ref_epoch=95), + ], + [ + 500, + 500 + 700, + 500 + 700 + 300, + ], + [ + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ), + ( + # rewards + {NodeOperatorId(1): 500 + 700}, + # distributed_rewards + 500 + 700, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(2), HexBytes("0x02")): 1}, + ), + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 0, + # strikes + {}, + ), + ], + 500 + 700, + {NodeOperatorId(1): 500 + 500 + 700}, + 0, + { + (NodeOperatorId(1), HexBytes("0x01")): [0, 0, 1, 1, 0, 0], + (NodeOperatorId(2), HexBytes("0x02")): [0, 1, 0, 0, 0, 0], + }, + ), + ], +) +@pytest.mark.unit +def test_calculate_distribution( + frames: list[Frame], + last_report, + mocked_curve_params, + frame_blockstamps, + shares_to_distribute, + distribution_in_frame, + expected_total_rewards, + expected_total_rewards_map, + expected_total_rebate, + expected_strikes, +): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=shares_to_distribute) + w3.csm.get_curve_params = mocked_curve_params + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.data = {f: {} for f in frames} + distribution._get_frame_blockstamp = Mock(side_effect=frame_blockstamps) + distribution._calculate_distribution_in_frame = Mock(side_effect=distribution_in_frame) + + result = distribution.calculate(blockstamp=..., last_report=last_report) + + assert result.total_rewards == expected_total_rewards + assert result.total_rewards_map == expected_total_rewards_map + assert result.total_rebate == expected_total_rebate + assert result.strikes == expected_strikes + + assert len(result.logs) == len(frames) + for i, log in enumerate(result.logs): + assert log.blockstamp == frame_blockstamps[i] + assert log.frame == frames[i] + + +@pytest.mark.unit +def test_calculate_distribution_handles_invalid_distribution(): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + w3.csm.get_curve_params = Mock(...) + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.data = {(EpochNumber(0), EpochNumber(31)): {}} + distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) + distribution._calculate_distribution_in_frame = Mock( + return_value=( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 1, + # strikes + {}, + ) + ) + + with pytest.raises(ValueError, match=re.escape("Invalid distribution: 500 + 1 > 500")): + distribution.calculate(..., Mock(strikes={}, rewards=[])) + + +@pytest.mark.unit +def test_calculate_distribution_handles_invalid_distribution_in_total(): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + w3.csm.get_curve_params = Mock(...) + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.data = {(EpochNumber(0), EpochNumber(31)): {}} + distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) + distribution._calculate_distribution_in_frame = Mock( + return_value=( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 400, + # rebate_to_protocol + 1, + # strikes + {}, + ) + ) + + with pytest.raises(InconsistentData, match="Invalid distribution"): + distribution.calculate(..., Mock(strikes={}, rewards=[])) + + +@pytest.mark.parametrize( + ( + "to_distribute", + "frame_validators", + "frame_state_data", + "mocked_curve_params", + "expected_rewards_distribution_map", + "expected_distributed_rewards", + "expected_rebate_to_protocol", + "expected_frame_strikes", + "expected_log", + ), + [ + # All above threshold performance + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False) + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)} + ), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)}), + ), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Rewards map + { + NodeOperatorId(1): 100, + }, + # Distributed rewards + 100, + # Rebate to protocol + 0, + # Strikes + {}, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=100, + rebate_to_protocol=0, + operators={ + NodeOperatorId(1): OperatorFrameSummary( + distributed_rewards=100, + performance_coefficients=PerformanceCoefficients(), + validators={ + ValidatorIndex(1): ValidatorFrameSummary( + distributed_rewards=100, + performance=0.6, + threshold=0.5, + rewards_share=1.0, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=6), + proposal_duty=DutyAccumulator(assigned=10, included=6), + sync_duty=DutyAccumulator(assigned=10, included=6), + ) + }, + ) + }, + ), + ), + # All below threshold performance + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x01") + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + proposals=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + syncs=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + ), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Distribution map + {}, + # Distributed rewards + 0, + # Rebate to protocol + 0, + # Strikes + { + (NodeOperatorId(1), HexBytes('0x01')): 1, + }, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=0, + rebate_to_protocol=0, + operators={ + NodeOperatorId(1): OperatorFrameSummary( + distributed_rewards=0, + performance_coefficients=PerformanceCoefficients(), + validators={ + ValidatorIndex(1): ValidatorFrameSummary( + performance=0.5, + threshold=0.65, + rewards_share=1.0, + slashed=False, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=5), + proposal_duty=DutyAccumulator(assigned=10, included=5), + sync_duty=DutyAccumulator(assigned=10, included=5), + ) + }, + ) + }, + ), + ), + # Mixed. With custom threshold and reward share + ( + 100, + { + # Operator 1. One above threshold performance, one slashed + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x01") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(2), validator=ValidatorStateFactory.build(slashed=True, pubkey="0x02") + ), + ], + # Operator 2. One above threshold performance, one below + (..., NodeOperatorId(2)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(3), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x03") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(4), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x04") + ), + ], + # Operator 3. All below threshold performance + (..., NodeOperatorId(3)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(5), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x05") + ), + ], + # Operator 4. No duties + (..., NodeOperatorId(4)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(6), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x06") + ), + ], + # Operator 5. All above threshold performance + (..., NodeOperatorId(5)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(7), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x07") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(8), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x08") + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(3): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(4): DutyAccumulator(assigned=10, included=0), + ValidatorIndex(5): DutyAccumulator(assigned=10, included=0), + ValidatorIndex(7): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(8): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + proposals=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(4): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(7): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + syncs=defaultdict( + DutyAccumulator, + { + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(3): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(8): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + ), + Mock( + side_effect=lambda no_id, _: { + NodeOperatorId(5): CurveParams( + strikes_params=..., + perf_leeway_data=KeyNumberValueIntervalList( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(2, 2000)] + ), + reward_share_data=KeyNumberValueIntervalList( + [KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)] + ), + perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), + ), + }.get( + no_id, + CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ), + ), + ), + # Expected: + # Distribution map + { + NodeOperatorId(1): 25, + NodeOperatorId(2): 25, + NodeOperatorId(5): 47, + }, + # Distributed rewards + 97, + # Rebate to protocol + 3, + # Strikes + { + (NodeOperatorId(1), HexBytes('0x02')): 1, # Slashed + (NodeOperatorId(2), HexBytes('0x04')): 1, # Below threshold + (NodeOperatorId(3), HexBytes('0x05')): 1, # Below threshold + }, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=97, + rebate_to_protocol=3, + operators=defaultdict( + OperatorFrameSummary, + { + NodeOperatorId(1): OperatorFrameSummary( + distributed_rewards=25, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(1): ValidatorFrameSummary( + distributed_rewards=25, + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1, + attestation_duty=DutyAccumulator(assigned=10, included=10), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(2): ValidatorFrameSummary( + slashed=True, + strikes=1, + ), + }, + ), + ), + NodeOperatorId(2): OperatorFrameSummary( + distributed_rewards=25, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(3): ValidatorFrameSummary( + distributed_rewards=25, + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1, + attestation_duty=DutyAccumulator(assigned=10, included=10), + sync_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(4): ValidatorFrameSummary( + distributed_rewards=0, + performance=0.12903225806451613, + threshold=0.8842289719626168, + rewards_share=1, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=0), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + }, + ), + ), + NodeOperatorId(3): OperatorFrameSummary( + distributed_rewards=0, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(5): ValidatorFrameSummary( + performance=0.0, + threshold=0.8842289719626168, + rewards_share=1, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=0), + ), + }, + ), + ), + NodeOperatorId(5): OperatorFrameSummary( + distributed_rewards=47, + performance_coefficients=PerformanceCoefficients( + attestations_weight=1, + blocks_weight=0, + sync_weight=0, + ), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(7): ValidatorFrameSummary( + distributed_rewards=25, + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1.0, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=10), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(8): ValidatorFrameSummary( + distributed_rewards=22, + performance=1.0, + threshold=0.7842289719626168, + rewards_share=0.9, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=10), + sync_duty=DutyAccumulator(assigned=10, included=10), + ), + }, + ), + ), + }, + ), + ), + ), + # No duties + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False) + ), + ], + }, + NetworkDuties(), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Distribution map + {}, + # Distributed rewards + 0, + # Rebate to protocol + 0, + # Strikes + {}, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=0, + rebate_to_protocol=0, + operators={}, + ), + ), + ], +) +@pytest.mark.unit +def test_calculate_distribution_in_frame( + to_distribute, + frame_validators, + frame_state_data, + mocked_curve_params, + expected_rewards_distribution_map, + expected_distributed_rewards, + expected_rebate_to_protocol, + expected_frame_strikes, + expected_log, +): + log = FramePerfLog(blockstamp=..., frame=...) + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM)) + w3.csm.get_curve_params = mocked_curve_params + + frame = (EpochNumber(0), EpochNumber(31)) + state = State() + state.migrate(*frame, epochs_per_frame=32, consensus_version=3) + state.data = {frame: frame_state_data} + + distribution = Distribution(w3, converter=..., state=state) + + (rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame) = ( + distribution._calculate_distribution_in_frame( + frame, + blockstamp=..., + rewards_to_distribute=to_distribute, + operators_to_validators=frame_validators, + log=log, + ) + ) + + assert dict(rewards_distribution) == expected_rewards_distribution_map + assert distributed_rewards == expected_distributed_rewards + assert rebate_to_protocol == expected_rebate_to_protocol + assert strikes_in_frame == expected_frame_strikes + assert log == expected_log + + +@pytest.mark.parametrize( + "att_perf, prop_perf, sync_perf, expected", + [ + (1.0, 1.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 0.0), + (0.5, 0.5, 0.5, 0.5), + (0.9, None, 0.7, pytest.approx(0.8928, rel=1e-4)), + (0.95, None, None, 0.95), + (0.95, 0.5, None, pytest.approx(0.8919, rel=1e-4)), + (0.95, None, 0.7, pytest.approx(0.9410, rel=1e-4)), + (0.95, 0.5, 0.7, pytest.approx(0.8859, rel=1e-4)), + ], +) +@pytest.mark.unit +def test_get_network_performance(att_perf, prop_perf, sync_perf, expected): + distribution = Distribution(Mock(), Mock(), Mock()) + distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=att_perf) if att_perf is not None else None) + distribution.state.get_prop_network_aggr = Mock( + return_value=Mock(perf=prop_perf) if prop_perf is not None else None + ) + distribution.state.get_sync_network_aggr = Mock( + return_value=Mock(perf=sync_perf) if sync_perf is not None else None + ) + frame = Mock(spec=Frame) + + result = distribution._get_network_performance(frame) + + assert result == expected + + +@pytest.mark.unit +def test_get_network_performance_raises_error_for_invalid_performance(): + distribution = Distribution(Mock(), Mock(), Mock()) + distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=1.1)) + distribution.state.get_prop_network_aggr = Mock(return_value=Mock(perf=1.0)) + distribution.state.get_sync_network_aggr = Mock(return_value=Mock(perf=1.0)) + frame = Mock(spec=Frame) + + with pytest.raises(ValueError, match="Invalid performance: performance"): + distribution._get_network_performance(frame) + + +@pytest.mark.parametrize( + "validator_duties, is_slashed, threshold, reward_share, expected_outcome", + [ + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=6), + proposal=DutyAccumulator(assigned=10, included=6), + sync=DutyAccumulator(assigned=10, included=6), + ), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=10, rebate_share=0, strikes=0), + ), + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=4), + proposal=DutyAccumulator(assigned=10, included=4), + sync=DutyAccumulator(assigned=10, included=4), + ), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1), + ), + ( + ValidatorDuties(attestation=None, proposal=None, sync=None), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=0), + ), + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=1, included=1), + proposal=DutyAccumulator(assigned=1, included=1), + sync=DutyAccumulator(assigned=1, included=1), + ), + True, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1), + ), + ], +) +@pytest.mark.unit +def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_share, expected_outcome): + validator = LidoValidatorFactory.build() + validator.validator.slashed = is_slashed + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + + outcome = Distribution.get_validator_duties_outcome( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + log_operator, + ) + + assert outcome == expected_outcome + if validator_duties.attestation and not is_slashed: + assert log_operator.validators[validator.index].threshold == threshold + assert log_operator.validators[validator.index].rewards_share == reward_share + if validator_duties.attestation: + assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation + if validator_duties.proposal: + assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal + if validator_duties.sync: + assert log_operator.validators[validator.index].sync_duty == validator_duties.sync + + if not validator_duties.attestation: + assert validator.index not in log_operator.validators + + assert log_operator.validators[validator.index].slashed is is_slashed + + +@pytest.mark.parametrize( + "participation_shares, rewards_to_distribute, rebate_share, expected_distribution", + [ + ( + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}}, + Wei(1 * 10**18), + 0, + {NodeOperatorId(1): Wei(333333333333333333), NodeOperatorId(2): Wei(666666666666666666)}, + ), + ( + {NodeOperatorId(1): {ValidatorIndex(0): 0}, NodeOperatorId(2): {ValidatorIndex(1): 0}}, + Wei(1 * 10**18), + 0, + {}, + ), + ( + {}, + Wei(1 * 10**18), + 0, + {}, + ), + ( + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 0}}, + Wei(1 * 10**18), + 0, + {NodeOperatorId(1): Wei(1 * 10**18)}, + ), + ( + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}}, + Wei(1 * 10**18), + 10, + {NodeOperatorId(1): Wei(322580645161290322), NodeOperatorId(2): Wei(645161290322580645)}, + ), + ], +) +@pytest.mark.unit +def test_calc_rewards_distribution_in_frame( + participation_shares, rewards_to_distribute, rebate_share, expected_distribution +): + log = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) + rewards_distribution = Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute, log + ) + assert rewards_distribution == expected_distribution + + +@pytest.mark.unit +def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): + participation_shares = {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}} + rewards_to_distribute = Wei(-1) + rebate_share = 0 + + with pytest.raises(ValueError, match="Invalid rewards to distribute"): + Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute, log=Mock() + ) + + +@pytest.mark.parametrize( + ("acc", "strikes_in_frame", "threshold_per_op", "expected"), + [ + pytest.param({}, {}, {}, {}, id="empty_acc_empty_strikes_in_frame"), + pytest.param( + {}, + { + (NodeOperatorId(42), b"00"): 3, + (NodeOperatorId(17), b"01"): 1, + }, + { + NodeOperatorId(42): Mock(lifetime=6), + NodeOperatorId(17): Mock(lifetime=4), + }, + { + (NodeOperatorId(42), b"00"): [3, 0, 0, 0, 0, 0], + (NodeOperatorId(17), b"01"): [1, 0, 0, 0], + }, + id="empty_acc_non_empty_strikes_in_frame", + ), + pytest.param( + { + (NodeOperatorId(42), b"00"): StrikesList([3, 0, 0, 0, 0, 0]), + (NodeOperatorId(17), b"01"): StrikesList([1, 0, 0, 0]), + (NodeOperatorId(17), b"02"): StrikesList([0, 0, 0, 1]), + }, + {}, + { + NodeOperatorId(42): Mock(lifetime=5), + NodeOperatorId(17): Mock(lifetime=4), + }, + { + (NodeOperatorId(42), b"00"): [0, 3, 0, 0, 0], + (NodeOperatorId(17), b"01"): [0, 1, 0, 0], + }, + id="non_empty_acc_empty_strikes_in_frame", + ), + pytest.param( + { + (NodeOperatorId(42), b"00"): StrikesList([3, 0, 0, 0, 0, 0]), + (NodeOperatorId(17), b"01"): StrikesList([1, 0, 0, 0]), + }, + { + (NodeOperatorId(42), b"00"): 2, + (NodeOperatorId(18), b"02"): 1, + }, + { + NodeOperatorId(42): Mock(lifetime=5), + NodeOperatorId(17): Mock(lifetime=4), + NodeOperatorId(18): Mock(lifetime=6), + }, + { + (NodeOperatorId(42), b"00"): [2, 3, 0, 0, 0], + (NodeOperatorId(17), b"01"): [0, 1, 0, 0], + (NodeOperatorId(18), b"02"): [1, 0, 0, 0, 0, 0], + }, + id="non_empty_acc_non_empty_strikes_in_frame", + ), + ], +) +@pytest.mark.unit +def test_merge_strikes( + acc: dict, + strikes_in_frame: dict, + threshold_per_op: dict, + expected: dict, +): + distribution = Distribution(Mock(csm=Mock()), Mock(), Mock()) + distribution.w3.csm.get_curve_params = Mock( + side_effect=lambda no_id, _: Mock(strikes_params=threshold_per_op[no_id]) + ) + + result = distribution._process_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) + + assert result == expected + + +@pytest.mark.parametrize( + "total_distributed_rewards, total_rebate, total_rewards_to_distribute", + [ + (100, 50, 150), + (0, 0, 0), + (50, 50, 100), + ], +) +@pytest.mark.unit +def tests_validates_correct_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + + +@pytest.mark.parametrize( + "total_distributed_rewards, total_rebate, total_rewards_to_distribute", + [ + (100, 51, 150), + (200, 0, 199), + ], +) +@pytest.mark.unit +def test_raises_error_for_invalid_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + with pytest.raises(ValueError, match="Invalid distribution"): + Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + + +@pytest.mark.parametrize( + "attestation_perf, proposal_perf, sync_perf, expected", + [ + (0.95, None, None, 0.95), + (0.95, 0.5, None, pytest.approx(0.8919, rel=1e-4)), + (0.95, None, 0.7, pytest.approx(0.9410, rel=1e-4)), + (0.95, 0.5, 0.7, pytest.approx(0.8859, rel=1e-4)), + (1, 1, 1, 1), + ], +) +@pytest.mark.unit +def test_performance_coefficients_calc_performance(attestation_perf, proposal_perf, sync_perf, expected): + performance_coefficients = PerformanceCoefficients() + duties = ValidatorDuties( + attestation=Mock(perf=attestation_perf), + proposal=Mock(perf=proposal_perf) if proposal_perf is not None else None, + sync=Mock(perf=sync_perf) if sync_perf is not None else None, + ) + assert performance_coefficients.calc_performance(duties) == expected + + +@pytest.mark.parametrize( + "intervals, key_index, expected", + [ + ([KeyNumberValueInterval(1, 10000)], 100500, 10000 / TOTAL_BASIS_POINTS), + ([KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)], 1, 10000 / TOTAL_BASIS_POINTS), + ([KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)], 2, 9000 / TOTAL_BASIS_POINTS), + ( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], + 4, + 1000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], + 9, + 1000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], + 14, + 2000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], + 19, + 2000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], + 24, + 3000 / TOTAL_BASIS_POINTS, + ), + ], +) +@pytest.mark.unit +def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): + reward_share = KeyNumberValueIntervalList(intervals) + assert reward_share.get_for(key_index) == expected + + +@pytest.mark.unit +def test_interval_mapping_raises_error_for_invalid_key_number(): + reward_share = KeyNumberValueIntervalList( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] + ) + with pytest.raises(ValueError, match="Key number should be greater than 1 or equal"): + reward_share.get_for(-1) + + +@pytest.mark.unit +def test_interval_mapping_raises_error_for_key_number_out_of_range(): + reward_share = KeyNumberValueIntervalList([KeyNumberValueInterval(11, 10000)]) + with pytest.raises(ValueError, match="No value found for key number=2"): + reward_share.get_for(2) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 0265b6596..c35cd8997 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -1,30 +1,28 @@ import logging from collections import defaultdict from dataclasses import dataclass -from typing import NoReturn, Iterable, Literal, Type -from unittest.mock import Mock, patch, PropertyMock +from typing import Literal, NoReturn, Type +from unittest.mock import Mock, PropertyMock, patch import pytest from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.csm import CSOracle -from src.modules.csm.state import AttestationsAccumulator, State -from src.modules.csm.tree import Tree +from src.modules.csm.csm import CSOracle, LastReport +from src.modules.csm.distribution import Distribution +from src.modules.csm.state import State +from src.modules.csm.tree import RewardsTree, StrikesTree +from src.modules.csm.types import StrikesList from src.modules.submodules.oracle_module import ModuleExecuteDelay -from src.modules.submodules.types import CurrentFrame, ZERO_HASH -from src.providers.ipfs import CIDv0, CID -from src.types import EpochNumber, NodeOperatorId, SlotNumber, StakingModuleId, ValidatorIndex -from src.web3py.extensions.csm import CSM -from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory +from src.modules.submodules.types import ZERO_HASH, CurrentFrame +from src.providers.ipfs import CID +from src.types import NodeOperatorId, SlotNumber +from src.utils.types import hex_str_to_bytes +from src.web3py.types import Web3 +from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory -@pytest.fixture(autouse=True) -def mock_get_module_id(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(CSOracle, "_get_module_id", Mock()) - - @pytest.fixture(autouse=True) def mock_load_state(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(State, "load", Mock()) @@ -40,199 +38,6 @@ def test_init(module: CSOracle): assert module -@pytest.mark.unit -def test_stuck_operators(module: CSOracle): - module.module = Mock() # type: ignore - module.module_id = StakingModuleId(1) - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_validators.get_lido_node_operators_by_modules = Mock( - return_value={ - 1: { - type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), - type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), - type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), - type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), - }, - 2: {}, - 3: {}, - 4: {}, - } - ) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(6), NodeOperatorId(1337)] - ) - - module.current_frame_range = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = Mock() - blockstamp = Mock() - l_blockstamp.block_hash = "0x01" - blockstamp.slot_number = "1" - blockstamp.block_hash = "0x02" - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.stuck_operators(blockstamp=blockstamp) - - assert stuck == {NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(5), NodeOperatorId(6), NodeOperatorId(1337)} - - -@pytest.mark.unit -def test_stuck_operators_left_border_before_enact(module: CSOracle, caplog: pytest.LogCaptureFixture): - module.module = Mock() # type: ignore - module.module_id = StakingModuleId(3) - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_validators.get_lido_node_operators_by_modules = Mock( - return_value={ - 1: { - type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), - type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), - type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), - type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), - }, - 2: {}, - } - ) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[ - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - ] - ) - - module.current_frame_range = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = BlockStampFactory.build() - blockstamp = BlockStampFactory.build() - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.stuck_operators(blockstamp=blockstamp) - - assert stuck == { - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - } - - assert caplog.messages[0].startswith("No CSM digest at blockstamp") - - -@pytest.mark.unit -def test_calculate_distribution(module: CSOracle): - module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=10_000) - module.w3.csm.oracle.perf_leeway_bp = Mock(return_value=500) - - module.module_validators_by_node_operators = Mock( - return_value={ - (None, NodeOperatorId(0)): [Mock(index=0, validator=Mock(slashed=False))], - (None, NodeOperatorId(1)): [Mock(index=1, validator=Mock(slashed=False))], - (None, NodeOperatorId(2)): [Mock(index=2, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(3)): [Mock(index=3, validator=Mock(slashed=False))], - (None, NodeOperatorId(4)): [Mock(index=4, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(5)): [ - Mock(index=5, validator=Mock(slashed=False)), - Mock(index=6, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(6)): [ - Mock(index=7, validator=Mock(slashed=False)), - Mock(index=8, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(7)): [Mock(index=9, validator=Mock(slashed=False))], - (None, NodeOperatorId(8)): [ - Mock(index=10, validator=Mock(slashed=False)), - Mock(index=11, validator=Mock(slashed=True)), - ], - (None, NodeOperatorId(9)): [Mock(index=12, validator=Mock(slashed=True))], - } - ) - module.stuck_operators = Mock( - return_value=[ - NodeOperatorId(2), - NodeOperatorId(4), - ] - ) - - module.state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming - # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), - } - ) - module.state.migrate(EpochNumber(100), EpochNumber(500), 2) - - _, shares, log = module.calculate_distribution(blockstamp=Mock()) - - assert tuple(shares.items()) == ( - (NodeOperatorId(0), 476), - (NodeOperatorId(1), 2380), - (NodeOperatorId(3), 2380), - (NodeOperatorId(6), 2380), - (NodeOperatorId(8), 2380), - ) - - assert tuple(log.operators.keys()) == ( - NodeOperatorId(0), - NodeOperatorId(1), - NodeOperatorId(2), - NodeOperatorId(3), - NodeOperatorId(4), - NodeOperatorId(5), - NodeOperatorId(6), - # NodeOperatorId(7), # Missing in state - NodeOperatorId(8), - NodeOperatorId(9), - ) - - assert not log.operators[NodeOperatorId(1)].stuck - - assert log.operators[NodeOperatorId(2)].validators == {} - assert log.operators[NodeOperatorId(2)].stuck - assert log.operators[NodeOperatorId(4)].validators == {} - assert log.operators[NodeOperatorId(4)].stuck - - assert 5 in log.operators[NodeOperatorId(5)].validators - assert 6 in log.operators[NodeOperatorId(5)].validators - assert 7 in log.operators[NodeOperatorId(6)].validators - - assert log.operators[NodeOperatorId(0)].distributed == 476 - assert log.operators[NodeOperatorId(1)].distributed == 2380 - assert log.operators[NodeOperatorId(2)].distributed == 0 - assert log.operators[NodeOperatorId(3)].distributed == 2380 - assert log.operators[NodeOperatorId(6)].distributed == 2380 - - assert log.frame == (100, 500) - assert log.threshold == module.state.get_network_aggr().perf - 0.05 - - # Static functions you were dreaming of for so long. @@ -385,11 +190,11 @@ def test_current_frame_range(module: CSOracle, mock_chain_config: NoReturn, para if param.expected_frame is ValueError: with pytest.raises(ValueError): - module.current_frame_range(ReferenceBlockStampFactory.build(slot_number=param.finalized_slot)) + module.get_epochs_range_to_process(ReferenceBlockStampFactory.build(slot_number=param.finalized_slot)) else: bs = ReferenceBlockStampFactory.build(slot_number=param.finalized_slot) - l_epoch, r_epoch = module.current_frame_range(bs) + l_epoch, r_epoch = module.get_epochs_range_to_process(bs) assert (l_epoch, r_epoch) == param.expected_frame @@ -423,7 +228,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(0, 1)), report_blockstamp=Mock(ref_epoch=3), state=Mock(), - expected_msg="Frame has been changed, but the change is not yet observed on finalized epoch 1", + expected_msg="Epochs range has been changed, but the change is not yet observed on finalized epoch 1", expected_result=False, ), id="frame_changed_forward", @@ -434,7 +239,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(0, 2)), report_blockstamp=Mock(ref_epoch=1), state=Mock(), - expected_msg="Frame has been changed, but the change is not yet observed on finalized epoch 1", + expected_msg="Epochs range has been changed, but the change is not yet observed on finalized epoch 1", expected_result=False, ), id="frame_changed_backward", @@ -445,7 +250,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(1, 2)), report_blockstamp=Mock(ref_epoch=2), state=Mock(), - expected_msg="The starting epoch of the frame is not finalized yet", + expected_msg="The starting epoch of the epochs range is not finalized yet", expected_result=False, ), id="starting_epoch_not_finalized", @@ -495,7 +300,7 @@ def test_collect_data( module.w3 = Mock() module._receive_last_finalized_slot = Mock() module.state = param.state - module.current_frame_range = param.collect_frame_range + module.get_epochs_range_to_process = param.collect_frame_range module.get_blockstamp_for_report = Mock(return_value=param.report_blockstamp) with caplog.at_level(logging.DEBUG): @@ -522,7 +327,7 @@ def test_collect_data_outdated_checkpoint( unprocessed_epochs=list(range(0, 101)), is_fulfilled=False, ) - module.current_frame_range = Mock(side_effect=[(0, 100), (50, 150)]) + module.get_epochs_range_to_process = Mock(side_effect=[(0, 100), (50, 150)]) module.get_blockstamp_for_report = Mock(return_value=Mock(ref_epoch=100)) with caplog.at_level(logging.DEBUG): @@ -530,7 +335,10 @@ def test_collect_data_outdated_checkpoint( module.collect_data(blockstamp=Mock(slot_number=640)) msg = list( - filter(lambda log: "Checkpoints were prepared for an outdated frame, stop processing" in log, caplog.messages) + filter( + lambda log: "Checkpoints were prepared for an outdated epochs range, stop processing" in log, + caplog.messages, + ) ) assert len(msg), "Expected message not found in logs" @@ -547,7 +355,7 @@ def test_collect_data_fulfilled_state( unprocessed_epochs=list(range(0, 101)), ) type(module.state).is_fulfilled = PropertyMock(side_effect=[False, True]) - module.current_frame_range = Mock(return_value=(0, 100)) + module.get_epochs_range_to_process = Mock(return_value=(0, 100)) module.get_blockstamp_for_report = Mock(return_value=Mock(ref_epoch=100)) with caplog.at_level(logging.DEBUG): @@ -562,14 +370,14 @@ def test_collect_data_fulfilled_state( @dataclass(frozen=True) class BuildReportTestParam: - prev_tree_root: HexBytes - prev_tree_cid: CID | None - prev_acc_shares: Iterable[tuple[NodeOperatorId, int]] + last_report: LastReport curr_distribution: Mock - curr_tree_root: HexBytes - curr_tree_cid: CID | Literal[""] + curr_rewards_tree_root: HexBytes + curr_rewards_tree_cid: CID | Literal[""] + curr_strikes_tree_root: HexBytes + curr_strikes_tree_cid: CID | Literal[""] curr_log_cid: CID - expected_make_tree_call_args: tuple | None + expected_make_rewards_tree_call_args: tuple | None expected_func_result: tuple @@ -578,46 +386,74 @@ class BuildReportTestParam: [ pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes(ZERO_HASH), - prev_tree_cid=None, - prev_acc_shares=[], + last_report=Mock( + rewards_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=None, + rewards=[], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( - return_value=( - # distributed - 0, - # shares - defaultdict(int), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=0, + total_rewards_map=defaultdict(int), + total_rebate=0, + strikes=defaultdict(dict), + logs=[Mock()], ) ), - curr_tree_root=HexBytes(ZERO_HASH), - curr_tree_cid="", + curr_rewards_tree_root=HexBytes(ZERO_HASH), + curr_rewards_tree_cid="", + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=None, - expected_func_result=(1, 100500, HexBytes(ZERO_HASH), "", CID("QmLOG"), 0), + expected_make_rewards_tree_call_args=None, + expected_func_result=( + 1, + 100500, + HexBytes(ZERO_HASH), + "", + CID("QmLOG"), + 0, + 0, + HexBytes(ZERO_HASH), + CID(""), + ), ), id="empty_prev_report_and_no_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes(ZERO_HASH), - prev_tree_cid=None, - prev_acc_shares=[], + last_report=Mock( + rewards_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=None, + rewards=[], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( - return_value=( - # distributed - 6, - # shares - defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=6, + total_rewards_map=defaultdict( + int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3} + ), + total_rebate=1, + strikes=defaultdict(dict), + logs=[Mock()], ) ), - curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), - curr_tree_cid=CID("QmNEW_TREE"), + curr_rewards_tree_root=HexBytes("NEW_TREE_ROOT".encode()), + curr_rewards_tree_cid=CID("QmNEW_TREE"), + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=(({NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3},),), + expected_make_rewards_tree_call_args=( + ({NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3},), + ), expected_func_result=( 1, 100500, @@ -625,29 +461,47 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + 1, + HexBytes(ZERO_HASH), + CID(""), ), ), id="empty_prev_report_and_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_tree_cid=CID("QmOLD_TREE"), - prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + last_report=Mock( + rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + rewards_tree_cid=CID("QmOLD_TREE"), + rewards=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( - return_value=( - # distributed - 6, - # shares - defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(3): 3}), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=6, + total_rewards_map=defaultdict( + int, + { + NodeOperatorId(0): 101, + NodeOperatorId(1): 202, + NodeOperatorId(2): 300, + NodeOperatorId(3): 3, + }, + ), + total_rebate=1, + strikes=defaultdict(dict), + logs=[Mock()], ) ), - curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), - curr_tree_cid=CID("QmNEW_TREE"), + curr_rewards_tree_root=HexBytes("NEW_TREE_ROOT".encode()), + curr_rewards_tree_cid=CID("QmNEW_TREE"), + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=( + expected_make_rewards_tree_call_args=( ({NodeOperatorId(0): 101, NodeOperatorId(1): 202, NodeOperatorId(2): 300, NodeOperatorId(3): 3},), ), expected_func_result=( @@ -657,29 +511,39 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + 1, + HexBytes(ZERO_HASH), + CID(""), ), ), id="non_empty_prev_report_and_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_tree_cid=CID("QmOLD_TREE"), - prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + last_report=Mock( + rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + rewards_tree_cid=CID("QmOLD_TREE"), + rewards=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( - return_value=( - # distributed - 0, - # shares - defaultdict(int), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=0, + total_rewards_map=defaultdict(int), + total_rebate=0, + strikes=defaultdict(dict), + logs=[Mock()], ) ), - curr_tree_root=HexBytes(32), - curr_tree_cid="", + curr_rewards_tree_root=HexBytes(32), + curr_rewards_tree_cid="", + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=None, + expected_make_rewards_tree_call_args=None, expected_func_result=( 1, 100500, @@ -687,6 +551,9 @@ class BuildReportTestParam: CID("QmOLD_TREE"), CID("QmLOG"), 0, + 0, + HexBytes(ZERO_HASH), + CID(""), ), ), id="non_empty_prev_report_and_no_new_distribution", @@ -697,34 +564,52 @@ class BuildReportTestParam: def test_build_report(module: CSOracle, param: BuildReportTestParam): module.validate_state = Mock() module.report_contract.get_consensus_version = Mock(return_value=1) - # mock previous report - module.w3.csm.get_csm_tree_root = Mock(return_value=param.prev_tree_root) - module.w3.csm.get_csm_tree_cid = Mock(return_value=param.prev_tree_cid) - module.get_accumulated_shares = Mock(return_value=param.prev_acc_shares) + module._get_last_report = Mock(return_value=param.last_report) # mock current frame module.calculate_distribution = param.curr_distribution - module.make_tree = Mock(return_value=Mock(root=param.curr_tree_root)) - module.publish_tree = Mock(return_value=param.curr_tree_cid) + module.make_rewards_tree = Mock(return_value=Mock(root=param.curr_rewards_tree_root)) + module.make_strikes_tree = Mock(return_value=Mock(root=param.curr_strikes_tree_root)) + module.publish_tree = Mock( + side_effect=[ + param.curr_rewards_tree_cid, + param.curr_strikes_tree_cid, + ] + ) module.publish_log = Mock(return_value=param.curr_log_cid) - report = module.build_report(blockstamp=Mock(ref_slot=100500)) + blockstamp = Mock(ref_slot=100500) + report = module.build_report(blockstamp) - assert module.make_tree.call_args == param.expected_make_tree_call_args + assert module.make_rewards_tree.call_args == param.expected_make_rewards_tree_call_args assert report == param.expected_func_result @pytest.mark.unit def test_execute_module_not_collected(module: CSOracle): + module._check_compatability = Mock(return_value=True) + module.collect_data = Mock(return_value=False) + + execute_delay = module.execute_module( + last_finalized_blockstamp=Mock(slot_number=100500), + ) + assert execute_delay is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + + +@pytest.mark.unit +def test_execute_module_skips_collecting_if_forward_compatible(module: CSOracle): + module._check_compatability = Mock(return_value=False) module.collect_data = Mock(return_value=False) execute_delay = module.execute_module( last_finalized_blockstamp=Mock(slot_number=100500), ) assert execute_delay is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + module.collect_data.assert_not_called() @pytest.mark.unit def test_execute_module_no_report_blockstamp(module: CSOracle): + module._check_compatability = Mock(return_value=True) module.collect_data = Mock(return_value=True) module.get_blockstamp_for_report = Mock(return_value=None) @@ -747,92 +632,300 @@ def test_execute_module_processed(module: CSOracle): assert execute_delay is ModuleExecuteDelay.NEXT_SLOT -@pytest.fixture() -def tree(): - return Tree.new( - [ - (NodeOperatorId(0), 0), - (NodeOperatorId(1), 1), - (NodeOperatorId(2), 42), - (NodeOperatorId(UINT64_MAX), 0), - ] - ) - - -@pytest.mark.unit -def test_get_accumulated_shares(module: CSOracle, tree: Tree): - encoded_tree = tree.encode() - module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) - - for i, leaf in enumerate(module.get_accumulated_shares(cid=CIDv0("0x100500"), root=tree.root)): - assert tuple(leaf) == tree.tree.values[i]["value"] +@dataclass(frozen=True) +class RewardsTreeTestParam: + shares: dict[NodeOperatorId, int] + expected_tree_values: list | Type[ValueError] -@pytest.mark.unit -def test_get_accumulated_shares_unexpected_root(module: CSOracle, tree: Tree): - encoded_tree = tree.encode() - module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) +@pytest.mark.parametrize( + "param", + [ + pytest.param(RewardsTreeTestParam(shares={}, expected_tree_values=ValueError), id="empty"), + ], +) +def test_make_rewards_tree_negative(module: CSOracle, param: RewardsTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX with pytest.raises(ValueError): - next(module.get_accumulated_shares(cid=CIDv0("0x100500"), root=HexBytes("0x100500"))) - - -@dataclass(frozen=True) -class MakeTreeTestParam: - shares: dict[NodeOperatorId, int] - expected_tree_values: tuple | Type[ValueError] + module.make_rewards_tree(param.shares) @pytest.mark.parametrize( "param", [ - pytest.param(MakeTreeTestParam(shares={}, expected_tree_values=ValueError), id="empty"), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}, - expected_tree_values=( - {'treeIndex': 4, 'value': (0, 1)}, - {'treeIndex': 2, 'value': (1, 2)}, - {'treeIndex': 3, 'value': (2, 3)}, - ), + expected_tree_values=[ + (0, 1), + (1, 2), + (2, 3), + ], ), id="normal_tree", ), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={NodeOperatorId(0): 1}, - expected_tree_values=( - {'treeIndex': 2, 'value': (0, 1)}, - {'treeIndex': 1, 'value': (18446744073709551615, 0)}, - ), + expected_tree_values=[ + (0, 1), + (UINT64_MAX, 0), + ], ), id="put_stone", ), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={ NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3, - NodeOperatorId(18446744073709551615): 0, + NodeOperatorId(UINT64_MAX): 0, }, - expected_tree_values=( - {'treeIndex': 4, 'value': (0, 1)}, - {'treeIndex': 2, 'value': (1, 2)}, - {'treeIndex': 3, 'value': (2, 3)}, - ), + expected_tree_values=[ + (0, 1), + (1, 2), + (2, 3), + ], ), id="remove_stone", ), ], ) @pytest.mark.unit -def test_make_tree(module: CSOracle, param: MakeTreeTestParam): +def test_make_rewards_tree(module: CSOracle, param: RewardsTreeTestParam): module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX - if param.expected_tree_values is ValueError: - with pytest.raises(ValueError): - module.make_tree(param.shares) - else: - tree = module.make_tree(param.shares) - assert tree.tree.values == param.expected_tree_values + tree = module.make_rewards_tree(param.shares) + assert tree.values == param.expected_tree_values + + +@dataclass(frozen=True) +class StrikesTreeTestParam: + strikes: dict[tuple[NodeOperatorId, HexBytes], StrikesList] + expected_tree_values: list | Type[ValueError] + + +@pytest.mark.parametrize( + "param", + [ + pytest.param(StrikesTreeTestParam(strikes={}, expected_tree_values=ValueError), id="empty"), + ], +) +@pytest.mark.unit +def test_make_strikes_tree_negative(module: CSOracle, param: StrikesTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX + + with pytest.raises(ValueError): + module.make_strikes_tree(param.strikes) + + +@pytest.mark.parametrize( + "param", + [ + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + (NodeOperatorId(1), HexBytes(b"07e8")): StrikesList([1, 2]), + (NodeOperatorId(2), HexBytes(b"0682")): StrikesList([1, 2, 3]), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(1), HexBytes(b"07e8"), StrikesList([1, 2])), + (NodeOperatorId(2), HexBytes(b"0682"), StrikesList([1, 2, 3])), + ], + ), + id="normal_tree", + ), + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(b""), StrikesList()), + ], + ), + id="put_stone", + ), + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + (NodeOperatorId(1), HexBytes(b"07e8")): StrikesList([1, 2]), + (NodeOperatorId(2), HexBytes(b"0682")): StrikesList([1, 2, 3]), + (NodeOperatorId(UINT64_MAX), HexBytes(b"")): StrikesList(), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(1), HexBytes(b"07e8"), StrikesList([1, 2])), + (NodeOperatorId(2), HexBytes(b"0682"), StrikesList([1, 2, 3])), + ], + ), + id="remove_stone", + ), + ], +) +@pytest.mark.unit +def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX + + tree = module.make_strikes_tree(param.strikes) + assert tree.values == param.expected_tree_values + + +class TestLastReport: + @pytest.mark.unit + def test_load(self, web3: Web3): + blockstamp = Mock() + + web3.csm.get_rewards_tree_root = Mock(return_value=HexBytes(b"42")) + web3.csm.get_rewards_tree_cid = Mock(return_value=CID("QmRT")) + web3.csm.get_strikes_tree_root = Mock(return_value=HexBytes(b"17")) + web3.csm.get_strikes_tree_cid = Mock(return_value=CID("QmST")) + + last_report = LastReport.load(web3, blockstamp) + + web3.csm.get_rewards_tree_root.assert_called_once_with(blockstamp) + web3.csm.get_rewards_tree_cid.assert_called_once_with(blockstamp) + web3.csm.get_strikes_tree_root.assert_called_once_with(blockstamp) + web3.csm.get_strikes_tree_cid.assert_called_once_with(blockstamp) + + assert last_report.rewards_tree_root == HexBytes(b"42") + assert last_report.rewards_tree_cid == CID("QmRT") + assert last_report.strikes_tree_root == HexBytes(b"17") + assert last_report.strikes_tree_cid == CID("QmST") + + @pytest.mark.unit + def test_get_rewards_empty(self, web3: Web3): + web3.ipfs = Mock(fetch=Mock()) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=HexBytes(ZERO_HASH), + strikes_tree_root=Mock(), + rewards_tree_cid=None, + strikes_tree_cid=Mock(), + ) + + assert last_report.rewards == [] + web3.ipfs.fetch.assert_not_called() + + @pytest.mark.unit + def test_get_rewards_okay(self, web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=rewards_tree.root, + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + + for value in rewards_tree.values: + assert value in last_report.rewards + + web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) + + @pytest.mark.unit + def test_get_rewards_unexpected_root(self, web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=HexBytes("DOES NOT MATCH".encode()), + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + + with pytest.raises(ValueError, match="tree root"): + last_report.rewards + + web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) + + @pytest.mark.unit + def test_get_strikes_empty(self, web3: Web3): + web3.ipfs = Mock(fetch=Mock()) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=Mock(), + strikes_tree_cid=None, + ) + + assert last_report.strikes == {} + web3.ipfs.fetch.assert_not_called() + + @pytest.mark.unit + def test_get_strikes_okay(self, web3: Web3, strikes_tree: StrikesTree): + encoded_tree = strikes_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=strikes_tree.root, + rewards_tree_cid=Mock(), + strikes_tree_cid=CID("QmST"), + ) + + for no_id, pubkey, value in strikes_tree.values: + assert last_report.strikes[(no_id, pubkey)] == value + + web3.ipfs.fetch.assert_called_once_with(last_report.strikes_tree_cid) + + @pytest.mark.unit + def test_get_strikes_unexpected_root(self, web3: Web3, strikes_tree: StrikesTree): + encoded_tree = strikes_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=HexBytes("DOES NOT MATCH".encode()), + rewards_tree_cid=Mock(), + strikes_tree_cid=CID("QmRT"), + ) + + with pytest.raises(ValueError, match="tree root"): + last_report.strikes + + web3.ipfs.fetch.assert_called_once_with(last_report.strikes_tree_cid) + + @pytest.fixture() + def rewards_tree(self) -> RewardsTree: + return RewardsTree.new( + [ + (NodeOperatorId(0), 0), + (NodeOperatorId(1), 1), + (NodeOperatorId(2), 42), + (NodeOperatorId(UINT64_MAX), 0), + ] + ) + + @pytest.fixture() + def strikes_tree(self) -> StrikesTree: + return StrikesTree.new( + [ + (NodeOperatorId(0), HexBytes(hex_str_to_bytes("0x00")), StrikesList([0])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x01")), StrikesList([1])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x02")), StrikesList([1])), + (NodeOperatorId(2), HexBytes(hex_str_to_bytes("0x03")), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(hex_str_to_bytes("0x64")), StrikesList([1, 0, 1])), + ] + ) diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index ff31bcbad..c8fd64c76 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -1,8 +1,8 @@ import json import pytest -from src.modules.csm.log import FramePerfLog -from src.modules.csm.state import AttestationsAccumulator +from src.modules.csm.log import FramePerfLog, DutyAccumulator +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -25,26 +25,81 @@ def log(ref_blockstamp: ReferenceBlockStamp, frame: tuple[EpochNumber, EpochNumb @pytest.mark.unit def test_fields_access(log: FramePerfLog): log.operators[NodeOperatorId(42)].validators["100500"].slashed = True - log.operators[NodeOperatorId(17)].stuck = True @pytest.mark.unit -def test_log_encode(log: FramePerfLog): +def test_logs_encode(log: FramePerfLog): # Fill in dynamic fields to make sure we have data in it to be encoded. - log.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) - log.operators[NodeOperatorId(42)].distributed = 17 - log.operators[NodeOperatorId(0)].distributed = 0 + log.operators[NodeOperatorId(42)].distributed_rewards = 17 + log.operators[NodeOperatorId(42)].performance_coefficients = PerformanceCoefficients() + log.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = DutyAccumulator(220, 119) + log.operators[NodeOperatorId(42)].validators["41337"].proposal_duty = DutyAccumulator(1, 1) + log.operators[NodeOperatorId(42)].validators["41337"].sync_duty = DutyAccumulator(100500, 100000) + log.operators[NodeOperatorId(42)].validators["41337"].performance = 0.5 + log.operators[NodeOperatorId(42)].validators["41337"].threshold = 0.7 + log.operators[NodeOperatorId(42)].validators["41337"].rewards_share = 0.3 + log.operators[NodeOperatorId(42)].validators["41337"].distributed_rewards = 17 - encoded = log.encode() - decoded = json.loads(encoded) + log.operators[NodeOperatorId(0)].distributed_rewards = 0 + log.operators[NodeOperatorId(0)].performance_coefficients = PerformanceCoefficients(1, 2, 3) - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 - assert decoded["operators"]["42"]["distributed"] == 17 - assert decoded["operators"]["0"]["distributed"] == 0 + log.distributable = 100 + log.distributed_rewards = 50 + log.rebate_to_protocol = 10 - assert decoded["blockstamp"]["block_hash"] == log.blockstamp.block_hash - assert decoded["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + log_2 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) + log_2.operators = log.operators - assert decoded["threshold"] == log.threshold - assert decoded["frame"] == list(log.frame) + log_2.distributable = 100000000 + log_2.distributed_rewards = 0 + log_2.rebate_to_protocol = 0 + + logs = [log, log_2] + + encoded = FramePerfLog.encode(logs) + + decoded_logs = json.loads(encoded) + + for decoded in decoded_logs: + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 + assert decoded["operators"]["42"]["validators"]["41337"]["proposal_duty"]["assigned"] == 1 + assert decoded["operators"]["42"]["validators"]["41337"]["proposal_duty"]["included"] == 1 + assert decoded["operators"]["42"]["validators"]["41337"]["sync_duty"]["assigned"] == 100500 + assert decoded["operators"]["42"]["validators"]["41337"]["sync_duty"]["included"] == 100000 + assert decoded["operators"]["42"]["validators"]["41337"]["performance"] == 0.5 + assert decoded["operators"]["42"]["validators"]["41337"]["threshold"] == 0.7 + assert decoded["operators"]["42"]["validators"]["41337"]["rewards_share"] == 0.3 + assert decoded["operators"]["42"]["validators"]["41337"]["slashed"] == False + assert decoded["operators"]["42"]["validators"]["41337"]["distributed_rewards"] == 17 + assert decoded["operators"]["42"]["distributed_rewards"] == 17 + assert decoded["operators"]["42"]["performance_coefficients"] == { + 'attestations_weight': 54, + 'blocks_weight': 8, + 'sync_weight': 2, + } + + assert decoded["operators"]["0"]["distributed_rewards"] == 0 + assert decoded["operators"]["0"]["performance_coefficients"] == { + 'attestations_weight': 1, + 'blocks_weight': 2, + 'sync_weight': 3, + } + + assert decoded_logs[0]["blockstamp"]["block_hash"] == log.blockstamp.block_hash + assert decoded_logs[0]["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + + assert decoded_logs[0]["frame"] == list(log.frame) + + assert decoded_logs[0]["distributable"] == log.distributable + assert decoded_logs[0]["distributed_rewards"] == log.distributed_rewards + assert decoded_logs[0]["rebate_to_protocol"] == log.rebate_to_protocol + + assert decoded_logs[1]["blockstamp"]["block_hash"] == log_2.blockstamp.block_hash + assert decoded_logs[1]["blockstamp"]["ref_slot"] == log_2.blockstamp.ref_slot + + assert decoded_logs[1]["frame"] == list(log_2.frame) + + assert decoded_logs[1]["distributable"] == log_2.distributable + assert decoded_logs[1]["distributed_rewards"] == log_2.distributed_rewards + assert decoded_logs[1]["rebate_to_protocol"] == log_2.rebate_to_protocol diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 38cd785b7..9f59f43b0 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -1,10 +1,14 @@ +import os +import pickle +from collections import defaultdict from pathlib import Path from unittest.mock import Mock import pytest -from src.modules.csm.state import AttestationsAccumulator, State -from src.types import EpochNumber, ValidatorIndex +from src import variables +from src.modules.csm.state import DutyAccumulator, InvalidState, NetworkDuties, State +from src.types import ValidatorIndex from src.utils.range import sequence @@ -18,212 +22,759 @@ def mock_state_file(state_file_path: Path): State.file = Mock(return_value=state_file_path) -@pytest.mark.unit -def test_attestation_aggregate_perf(): - aggr = AttestationsAccumulator(included=333, assigned=777) - assert aggr.perf == pytest.approx(0.4285, abs=1e-4) +class TestCachePathConfigurable: + @pytest.fixture() + def mock_state_file(self): + # NOTE: Overrides file-level mock_state_file to check the mechanic. + pass + + @pytest.fixture() + def cache_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + monkeypatch.setattr(variables, "CACHE_PATH", tmp_path) + return tmp_path + + @pytest.mark.unit + def test_file_returns_correct_path(self, cache_path: Path): + assert State.file() == cache_path / "cache.pkl" + + @pytest.mark.unit + def test_buffer_returns_correct_path(self, cache_path: Path): + state = State() + assert state.buffer == cache_path / "cache.buf" @pytest.mark.unit -def test_state_avg_perf(): +def test_load_restores_state_from_file(): state = State() + state.data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.commit() + loaded_state = State.load() + assert loaded_state.data == state.data + - assert state.get_network_aggr().perf == 0 +@pytest.mark.unit +def test_load_returns_new_instance_if_file_not_found(state_file_path: Path): + assert not state_file_path.exists() + state = State.load() + assert state.is_empty - state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), - ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=0), - } - ) - assert state.get_network_aggr().perf == 0 +@pytest.mark.unit +def test_load_returns_new_instance_if_empty_object(state_file_path: Path): + with open(state_file_path, "wb") as f: + pickle.dump(None, f) + state = State.load() + assert state.is_empty - state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - ) - assert state.get_network_aggr().perf == 0.5 +@pytest.mark.unit +def test_commit_saves_state_to_file(state_file_path: Path, monkeypatch: pytest.MonkeyPatch): + state = State() + state.data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + with monkeypatch.context() as mp: + os_replace_mock = Mock(side_effect=os.replace) + mp.setattr("os.replace", os_replace_mock) + state.commit() + with open(state_file_path, "rb") as f: + loaded_state = pickle.load(f) + assert loaded_state.data == state.data + os_replace_mock.assert_called_once_with(state_file_path.with_suffix(".buf"), state_file_path) @pytest.mark.unit -def test_state_frame(): +def test_is_empty_returns_true_for_empty_state(): state = State() + assert state.is_empty - state.migrate(EpochNumber(100), EpochNumber(500), 1) - assert state.frame == (100, 500) - state.migrate(EpochNumber(300), EpochNumber(301), 1) - assert state.frame == (300, 301) +@pytest.mark.unit +def test_is_empty_returns_false_for_non_empty_state(): + state = State() + state.data = {(0, 31): NetworkDuties()} + assert not state.is_empty - state.clear() +@pytest.mark.unit +def test_unprocessed_epochs_raises_error_if_epochs_not_set(): + state = State() with pytest.raises(ValueError, match="Epochs to process are not set"): - state.frame + state.unprocessed_epochs @pytest.mark.unit -def test_state_attestations(): - state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - ) +def test_unprocessed_epochs_returns_correct_set(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 63)) + assert state.unprocessed_epochs == set(sequence(64, 95)) - network_aggr = state.get_network_aggr() - assert network_aggr.assigned == 1000 - assert network_aggr.included == 500 +@pytest.mark.unit +def test_is_fulfilled_returns_true_if_no_unprocessed_epochs(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + assert state.is_fulfilled @pytest.mark.unit -def test_state_load(): - orig = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - ) +def test_is_fulfilled_returns_false_if_unprocessed_epochs_exist(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 63)) + assert not state.is_fulfilled - orig.commit() - copy = State.load() - assert copy.data == orig.data + +@pytest.mark.unit +def test_calculate_frames_handles_exact_frame_size(): + epochs = tuple(range(10)) + frames = State._calculate_frames(epochs, 5) + assert frames == [(0, 4), (5, 9)] @pytest.mark.unit -def test_state_clear(): - state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - ) +def test_calculate_frames_raises_error_for_insufficient_epochs(): + epochs = tuple(range(8)) + with pytest.raises(ValueError, match="Insufficient epochs to form a frame"): + State._calculate_frames(epochs, 5) - state._epochs_to_process = (EpochNumber(1), EpochNumber(33)) - state._processed_epochs = {EpochNumber(42), EpochNumber(17)} +@pytest.mark.unit +def test_clear_resets_state_to_empty(): + state = State() + state.data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})} state.clear() assert state.is_empty - assert not state.data @pytest.mark.unit -def test_state_add_processed_epoch(): +def test_find_frame_returns_correct_frame(): state = State() - state.add_processed_epoch(EpochNumber(42)) - state.add_processed_epoch(EpochNumber(17)) - assert state._processed_epochs == {EpochNumber(42), EpochNumber(17)} + state.data = {(0, 31): {}} + assert state.find_frame(15) == (0, 31) @pytest.mark.unit -def test_state_inc(): - state = State( - { - ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), - ValidatorIndex(1): AttestationsAccumulator(included=1, assigned=2), - } - ) +def test_find_frame_raises_error_for_out_of_range_epoch(): + state = State() + state.data = {(0, 31): {}} + with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): + state.find_frame(32) - state.inc(ValidatorIndex(0), True) - state.inc(ValidatorIndex(0), False) - state.inc(ValidatorIndex(1), True) - state.inc(ValidatorIndex(1), True) - state.inc(ValidatorIndex(1), False) +@pytest.mark.unit +def test_increment_att_duty_adds_duty_correctly(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_att_duty(duty_epoch, ValidatorIndex(1), True) + assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].attestations[ValidatorIndex(1)].included == 6 - state.inc(ValidatorIndex(2), True) - state.inc(ValidatorIndex(2), False) - assert tuple(state.data.values()) == ( - AttestationsAccumulator(included=1, assigned=2), - AttestationsAccumulator(included=3, assigned=5), - AttestationsAccumulator(included=1, assigned=2), - ) +@pytest.mark.unit +def test_increment_prop_duty_adds_duty_correctly(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_prop_duty(duty_epoch, ValidatorIndex(1), True) + assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].proposals[ValidatorIndex(1)].included == 6 @pytest.mark.unit -def test_state_file_is_path(): - assert isinstance(State.file(), Path) +def test_increment_sync_duty_adds_duty_correctly(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_sync_duty(duty_epoch, ValidatorIndex(1), True) + assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].syncs[ValidatorIndex(1)].included == 6 @pytest.mark.unit -class TestStateTransition: - """Tests for State's transition for different l_epoch, r_epoch values""" +def test_increment_att_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(), + } + state.save_att_duty(duty_epoch, ValidatorIndex(2), True) + assert state.data[frame].attestations[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].attestations[ValidatorIndex(2)].included == 1 - @pytest.fixture(autouse=True) - def no_commit(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(State, "commit", Mock()) - def test_empty_to_new_frame(self): - state = State() - assert state.is_empty +@pytest.mark.unit +def test_increment_prop_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(), + } + state.save_prop_duty(duty_epoch, ValidatorIndex(2), True) + assert state.data[frame].proposals[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].proposals[ValidatorIndex(2)].included == 1 - l_epoch = EpochNumber(1) - r_epoch = EpochNumber(255) - state.migrate(l_epoch, r_epoch, 1) +@pytest.mark.unit +def test_increment_sync_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(), + } + state.save_sync_duty(duty_epoch, ValidatorIndex(2), True) + assert state.data[frame].syncs[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].syncs[ValidatorIndex(2)].included == 1 - assert not state.is_empty - assert state.unprocessed_epochs == set(sequence(l_epoch, r_epoch)) - @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new"), - [ - pytest.param(1, 255, 256, 510, id="Migrate a..bA..B"), - pytest.param(1, 255, 32, 510, id="Migrate a..A..b..B"), - pytest.param(32, 510, 1, 255, id="Migrate: A..a..B..b"), - ], - ) - def test_new_frame_requires_discarding_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new): - state = State() - state.clear = Mock(side_effect=state.clear) - state.migrate(l_epoch_old, r_epoch_old, 2) - state.clear.assert_not_called() - - state.migrate(l_epoch_new, r_epoch_new, 2) - state.clear.assert_called_once() - - assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - - @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new"), - [ - pytest.param(1, 255, 1, 510, id="Migrate Aa..b..B"), - pytest.param(32, 510, 1, 510, id="Migrate: A..a..b..B"), - ], - ) - def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new): - state = State() - state.clear = Mock(side_effect=state.clear) +@pytest.mark.unit +def test_increment_att_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_att_duty(duty_epoch, ValidatorIndex(1), False) + assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].attestations[ValidatorIndex(1)].included == 5 - state.migrate(l_epoch_old, r_epoch_old, 2) - state.clear.assert_not_called() - state.migrate(l_epoch_new, r_epoch_new, 2) - state.clear.assert_not_called() +@pytest.mark.unit +def test_increment_prop_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_prop_duty(duty_epoch, ValidatorIndex(1), False) + assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].proposals[ValidatorIndex(1)].included == 5 - assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - @pytest.mark.parametrize( - ("old_version", "new_version"), - [ - pytest.param(2, 3, id="Increase consensus version"), - pytest.param(3, 2, id="Decrease consensus version"), - ], - ) - def test_consensus_version_change(self, old_version, new_version): - state = State() - state.clear = Mock(side_effect=state.clear) - state._consensus_version = old_version +@pytest.mark.unit +def test_increment_sync_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + duty_epoch, _ = frame + state.data = { + frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + } + state.save_sync_duty(duty_epoch, ValidatorIndex(1), False) + assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].syncs[ValidatorIndex(1)].included == 5 + + +@pytest.mark.unit +def test_increment_att_duty_raises_error_for_out_of_range_epoch(): + state = State() + state.att_data = { + (0, 31): defaultdict(DutyAccumulator), + } + with pytest.raises(ValueError, match="is out of frames range"): + state.save_att_duty(32, ValidatorIndex(1), True) + + +@pytest.mark.unit +def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): + state = State() + state.att_data = { + (0, 31): defaultdict(DutyAccumulator), + } + with pytest.raises(ValueError, match="is out of frames range"): + state.save_prop_duty(32, ValidatorIndex(1), True) + + +@pytest.mark.unit +def test_increment_sync_duty_raises_error_for_out_of_range_epoch(): + state = State() + state.att_data = { + (0, 31): defaultdict(DutyAccumulator), + } + with pytest.raises(ValueError, match="is out of frames range"): + state.save_sync_duty(32, ValidatorIndex(1), True) + + +@pytest.mark.unit +def test_add_processed_epoch_adds_epoch_to_processed_set(): + state = State() + state.add_processed_epoch(5) + assert 5 in state._processed_epochs + + +@pytest.mark.unit +def test_add_processed_epoch_does_not_duplicate_epochs(): + state = State() + state.add_processed_epoch(5) + state.add_processed_epoch(5) + assert len(state._processed_epochs) == 1 + - l_epoch = r_epoch = EpochNumber(255) +@pytest.mark.unit +def test_migrate_discards_data_on_version_change(): + state = State() + state._consensus_version = 1 + state.clear = Mock() + state.commit = Mock() + state.migrate(0, 63, 32, 2) + + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 2 + state.clear.assert_called_once() + state.commit.assert_called_once() + + +@pytest.mark.unit +def test_migrate_no_migration_needed(): + state = State() + state._consensus_version = 1 + state.data = { + (0, 31): defaultdict(DutyAccumulator), + (32, 63): defaultdict(DutyAccumulator), + } + state._epochs_to_process = tuple(sequence(0, 63)) + state.commit = Mock() + state.migrate(0, 63, 32, 1) + + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 + state.commit.assert_not_called() + + +@pytest.mark.unit +def test_migrate_migrates_data(): + state = State() + state._consensus_version = 1 + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + state.commit = Mock() + state.migrate(0, 63, 64, 1) + + assert state.data == { + (0, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), + } + assert state.frames == [(0, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 + state.commit.assert_called_once() - state.migrate(l_epoch, r_epoch, old_version) - state.clear.assert_not_called() - state.migrate(l_epoch, r_epoch, new_version) - state.clear.assert_called_once() +@pytest.mark.unit +def test_migrate_invalidates_unmigrated_frames(): + state = State() + state._consensus_version = 1 + state.data = { + (0, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), + } + state.commit = Mock() + state.migrate(0, 31, 32, 1) + + assert state.data == { + (0, 31): NetworkDuties(), + } + assert state._processed_epochs == set() + assert state.frames == [(0, 31)] + assert state._epochs_to_process == tuple(sequence(0, 31)) + assert state._consensus_version == 1 + state.commit.assert_called_once() + + +@pytest.mark.unit +def test_migrate_discards_unmigrated_frame(): + state = State() + state._consensus_version = 1 + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + (64, 95): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 25)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 25)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 25)}), + ), + } + state._processed_epochs = set(sequence(0, 95)) + state.commit = Mock() + state.migrate(0, 63, 32, 1) + + assert state.data == { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + assert state._processed_epochs == set(sequence(0, 63)) + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 + state.commit.assert_called_once() + + +@pytest.mark.unit +def test_migrate_frames_data_creates_new_data_correctly(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + + new_frames = [(0, 63)] + state._migrate_frames_data(new_frames) + + assert state.data == { + (0, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), + } + assert state._processed_epochs == set(sequence(0, 20)) + + +@pytest.mark.unit +def test_migrate_frames_data_handles_no_migration(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + + new_frames = [(0, 31)] + state._migrate_frames_data(new_frames) + + assert state.data == { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + } + assert state._processed_epochs == set(sequence(0, 20)) + + +@pytest.mark.unit +def test_migrate_frames_data_handles_partial_migration(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + + new_frames = [(0, 31), (32, 95)] + state._migrate_frames_data(new_frames) + + assert state.data == { + (0, 31): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 95): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + assert state._processed_epochs == set(sequence(0, 20)) + + +@pytest.mark.unit +def test_migrate_frames_data_handles_no_data(): + state = State() + state.data = {frame: NetworkDuties() for frame in state.frames} + + new_frames = [(0, 31)] + state._migrate_frames_data(new_frames) + + assert state.data == {(0, 31): NetworkDuties()} + + +@pytest.mark.unit +def test_migrate_frames_data_handles_wider_old_frame(): + state = State() + state.data = { + (0, 63): NetworkDuties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + + new_frames = [(0, 31), (32, 63)] + state._migrate_frames_data(new_frames) + + assert state.data == { + (0, 31): NetworkDuties(), + (32, 63): NetworkDuties(), + } + assert state._processed_epochs == set() + + +@pytest.mark.unit +def test_validate_raises_error_if_state_not_fulfilled(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 94)) + with pytest.raises(InvalidState, match="State is not fulfilled"): + state.validate(0, 95) + + +@pytest.mark.unit +def test_validate_raises_error_if_processed_epoch_out_of_range(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + state._processed_epochs.add(96) + with pytest.raises(InvalidState, match="Processed epoch 96 is out of range"): + state.validate(0, 95) + + +@pytest.mark.unit +def test_validate_raises_error_if_epoch_missing_in_processed_epochs(): + state = State() + state._epochs_to_process = tuple(sequence(0, 94)) + state._processed_epochs = set(sequence(0, 94)) + with pytest.raises(InvalidState, match="Epoch 95 missing in processed epochs"): + state.validate(0, 95) + + +@pytest.mark.unit +def test_validate_passes_for_fulfilled_state(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + state.validate(0, 95) + + +@pytest.mark.unit +def test_attestation_aggregate_perf(): + aggr = DutyAccumulator(included=333, assigned=777) + assert aggr.perf == pytest.approx(0.4285, abs=1e-4) + + +@pytest.mark.unit +def test_get_validator_duties(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + proposals=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(7, 1), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + syncs=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(3, 2), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + ) + } + duties = state.get_validator_duties((0, 31), ValidatorIndex(1)) + assert duties.attestation.assigned == 10 + assert duties.attestation.included == 5 + assert duties.proposal.assigned == 7 + assert duties.proposal.included == 1 + assert duties.sync.assigned == 3 + assert duties.sync.included == 2 + + +@pytest.mark.unit +def test_get_att_network_aggr_computes_correctly(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) + ) + } + aggr = state.get_att_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +@pytest.mark.unit +def test_get_sync_network_aggr_computes_correctly(): + state = State() + state.data = { + (0, 31): NetworkDuties( + syncs=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) + ) + } + aggr = state.get_sync_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +@pytest.mark.unit +def test_get_prop_network_aggr_computes_correctly(): + state = State() + state.data = { + (0, 31): NetworkDuties( + proposals=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) + ) + } + aggr = state.get_prop_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +@pytest.mark.unit +def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.data = { + (0, 31): NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_att_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.data = { + (0, 31): NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_prop_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.data = { + (0, 31): NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_sync_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_att_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_att_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_prop_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_prop_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_sync_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_sync_network_aggr((0, 31)) + + +@pytest.mark.unit +def test_get_att_network_aggr_handles_empty_frame_data(): + state = State() + state.data = {(0, 31): NetworkDuties()} + aggr = state.get_att_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 + + +@pytest.mark.unit +def test_get_prop_network_aggr_handles_empty_frame_data(): + state = State() + state.data = {(0, 31): NetworkDuties()} + aggr = state.get_prop_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 + + +@pytest.mark.unit +def test_get_sync_network_aggr_handles_empty_frame_data(): + state = State() + state.data = {(0, 31): NetworkDuties()} + aggr = state.get_sync_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py new file mode 100644 index 000000000..1ec3e7c66 --- /dev/null +++ b/tests/modules/csm/test_strikes.py @@ -0,0 +1,71 @@ +import pytest +from eth_utils.types import is_list_like + +from src.modules.csm.types import StrikesList + + +@pytest.mark.unit +def test_create_empty(): + strikes = StrikesList([]) + assert not len(strikes) + + strikes = StrikesList() + assert not len(strikes) + + +@pytest.mark.unit +def test_create_not_empty(): + strikes = StrikesList([1, 2, 3]) + assert strikes == [1, 2, 3] + + +@pytest.mark.unit +def test_eq(): + assert StrikesList([1, 2, 3]) == StrikesList([1, 2, 3]) + assert StrikesList([1, 2, 3]) != StrikesList([1, 2]) + assert StrikesList([1, 2, 3]) == [1, 2, 3] + + +@pytest.mark.unit +def test_create_maxlen_smaller_than_iterable(): + strikes = StrikesList([1, 2, 3], maxlen=5) + assert strikes == [1, 2, 3, 0, 0] + + +@pytest.mark.unit +def test_create_maxlen_larger_than_iterable(): + strikes = StrikesList([1, 2, 3], maxlen=2) + assert strikes == [1, 2] + + +@pytest.mark.unit +def test_create_resize_to_smaller(): + strikes = StrikesList([1, 2, 3]) + strikes.resize(2) + assert strikes == [1, 2] + + +@pytest.mark.unit +def test_create_resize_to_larger(): + strikes = StrikesList([1, 2, 3]) + strikes.resize(5) + assert strikes == [1, 2, 3, 0, 0] + + +@pytest.mark.unit +def test_push_element(): + strikes = StrikesList([1, 2, 3]) + strikes.push(4) + assert strikes == [4, 1, 2, 3] + + +@pytest.mark.unit +def test_is_list_like(): + strikes = StrikesList([1, 2, 3]) + assert is_list_like(strikes) + + arr = [4, 5] + arr.extend(strikes) + assert arr == [4, 5, 1, 2, 3] + + assert sum(strikes) == 6 diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 8c5682f82..2ce44aca0 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -1,40 +1,90 @@ +from abc import ABC, abstractmethod +from json import JSONDecoder, JSONEncoder +from typing import Iterable + import pytest +from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.tree import StandardMerkleTree, Tree, TreeJSONEncoder +from src.modules.csm.tree import RewardsTree, StandardMerkleTree, StrikesTree, Tree +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf from src.types import NodeOperatorId +from src.utils.types import hex_str_to_bytes + + +class TreeTestBase[LeafType: Iterable](ABC): + type TreeType = Tree[LeafType] + + cls: type[Tree[LeafType]] + + @property + def encoder(self) -> JSONEncoder: + return self.cls.encoder() + + @property + def decoder(self) -> JSONDecoder: + return self.cls.decoder() + + @property + @abstractmethod + def values(self) -> list[LeafType]: + raise NotImplementedError + + @pytest.fixture() + def tree(self) -> TreeType: + return self.cls.new(self.values) + + @pytest.mark.unit + def test_non_null_root(self, tree: TreeType): + assert tree.root + @pytest.mark.unit + def test_encode_decode(self, tree: TreeType): + decoded = self.cls.decode(tree.encode()) + assert decoded.values == self.values + assert decoded.root == tree.root -@pytest.fixture() -def tree(): - return Tree.new( - [ + @pytest.mark.unit + def test_decode_plain_tree_dump(self, tree: TreeType): + decoded = self.cls.decode(self.encoder.encode(tree.tree.dump()).encode()) + assert decoded.root == tree.root + + @pytest.mark.unit + def test_dump_compatibility(self, tree: TreeType): + loaded = StandardMerkleTree.load(tree.dump()) + assert loaded.root == tree.root + + +class TestRewardsTree(TreeTestBase[RewardsTreeLeaf]): + cls = RewardsTree + + @property + def values(self) -> list[RewardsTreeLeaf]: + return [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), (NodeOperatorId(2), 42), (NodeOperatorId(UINT64_MAX), 0), ] - ) - -@pytest.mark.unit -def test_non_null_root(tree: Tree): - assert tree.root +class TestStrikesTree(TreeTestBase[StrikesTreeLeaf]): + cls = StrikesTree -@pytest.mark.unit -def test_encode_decode(tree: Tree): - decoded = Tree.decode(tree.encode()) - assert decoded.root == tree.root - - -@pytest.mark.unit -def test_decode_plain_tree_dump(tree: Tree): - decoded = Tree.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) - assert decoded.root == tree.root - + @property + def values(self) -> list[StrikesTreeLeaf]: + return [ + (NodeOperatorId(0), HexBytes(hex_str_to_bytes("0x00")), StrikesList([0])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x01")), StrikesList([1])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x02")), StrikesList([1])), + (NodeOperatorId(2), HexBytes(hex_str_to_bytes("0x03")), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(hex_str_to_bytes("0x64")), StrikesList([1, 0, 1])), + ] -@pytest.mark.unit -def test_dump_compatibility(tree: Tree): - loaded = StandardMerkleTree.load(tree.dump()) - assert loaded.root == tree.root + @pytest.mark.unit + def test_decoded_types(self, tree: StrikesTree) -> None: + decoded = self.cls.decode(tree.encode()) + no_id, pk, strikes = decoded.values[0] + assert isinstance(no_id, int) + assert isinstance(pk, HexBytes) + assert isinstance(strikes, StrikesList) diff --git a/tests/modules/ejector/test_ejector.py b/tests/modules/ejector/test_ejector.py index 939a51761..4ae7dbef6 100644 --- a/tests/modules/ejector/test_ejector.py +++ b/tests/modules/ejector/test_ejector.py @@ -447,13 +447,11 @@ def test_get_churn_limit_validators_less_than_min_churn( self, ejector: Ejector, ref_blockstamp: ReferenceBlockStamp, - monkeypatch: pytest.MonkeyPatch, ) -> None: - with monkeypatch.context() as m: - ejector.w3.cc.get_validators = Mock(return_value=[1, 1, 0]) - result = ejector._get_churn_limit(ref_blockstamp) - assert result == 4, "Unexpected churn limit" - ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) + ejector.w3.cc.get_validators = Mock(return_value=[1, 1, 0]) + result = ejector._get_churn_limit(ref_blockstamp) + assert result == 4, "Unexpected churn limit" + ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) def test_get_churn_limit_basic( self, @@ -500,7 +498,7 @@ def test_ejector_get_processing_state_no_yet_init_epoch(ejector: Ejector): assert isinstance(processing_state, EjectorProcessingState) assert processing_state.current_frame_ref_slot == 100 assert processing_state.processing_deadline_time == 200 - assert processing_state.data_submitted == False + assert processing_state.data_submitted is False @pytest.mark.unit diff --git a/tests/providers/consensus/test_consensus_client.py b/tests/providers/consensus/test_consensus_client.py index 8cad2a869..87ee5a188 100644 --- a/tests/providers/consensus/test_consensus_client.py +++ b/tests/providers/consensus/test_consensus_client.py @@ -1,14 +1,16 @@ # pylint: disable=protected-access """Simple tests for the consensus client responses validity.""" + from unittest.mock import Mock import pytest +import requests +from src import variables from src.providers.consensus.client import ConsensusClient from src.providers.consensus.types import Validator -from src.types import SlotNumber +from src.types import EpochNumber, SlotNumber from src.utils.blockstamp import build_blockstamp -from src import variables from tests.factory.blockstamp import BlockStampFactory @@ -36,11 +38,14 @@ def test_get_block_details(consensus_client: ConsensusClient, web3): @pytest.mark.integration @pytest.mark.testnet -def test_get_block_attestations(consensus_client: ConsensusClient): +def test_get_block_attestations_and_sync(consensus_client: ConsensusClient): root = consensus_client.get_block_root('finalized').root - attestations = list(consensus_client.get_block_attestations(root)) + attestations_and_syncs = consensus_client.get_block_attestations_and_sync(root) + assert attestations_and_syncs + attestations, syncs = attestations_and_syncs assert attestations + assert syncs @pytest.mark.integration @@ -91,7 +96,11 @@ def test_get_state_view(consensus_client: ConsensusClient): @pytest.mark.unit def test_get_returns_nor_dict_nor_list(consensus_client: ConsensusClient): - consensus_client._get_without_fallbacks = Mock(return_value=(1, None)) + resp = requests.Response() + resp.status_code = 200 + resp._content = b'{"data": 1}' + + consensus_client.session.get = Mock(return_value=resp) bs = BlockStampFactory.build() raises = pytest.raises(ValueError, match='Expected (mapping|list) response') @@ -111,8 +120,38 @@ def test_get_returns_nor_dict_nor_list(consensus_client: ConsensusClient): with raises: consensus_client.get_block_details(SlotNumber(0)) + with raises: + consensus_client.get_block_attestations_and_sync(SlotNumber(0)) + + with raises: + consensus_client.get_attestation_committees(bs) + + with raises: + consensus_client.get_sync_committee(bs, EpochNumber(0)) + + with raises: + consensus_client.get_proposer_duties(EpochNumber(0), Mock()) + + with raises: + consensus_client.get_state_block_roots(SlotNumber(0)) + + with raises: + consensus_client.get_state_view_no_cache(bs) + with raises: consensus_client.get_validators_no_cache(bs) with raises: consensus_client._get_chain_id_with_provider(0) + + +@pytest.mark.unit +def test_get_proposer_duties_fails_on_root_check(consensus_client: ConsensusClient): + resp = requests.Response() + resp.status_code = 200 + resp._content = b'{"data": [], "dependent_root": "0x01"}' + + consensus_client.session.get = Mock(return_value=resp) + + with pytest.raises(ValueError, match="Dependent root for proposer duties request mismatch"): + consensus_client.get_proposer_duties(EpochNumber(0), "0x02") diff --git a/tests/providers_clients/test_http_provider.py b/tests/providers_clients/test_http_provider.py index 1129c0b81..b797f0434 100644 --- a/tests/providers_clients/test_http_provider.py +++ b/tests/providers_clients/test_http_provider.py @@ -1,9 +1,11 @@ # pylint: disable=protected-access -from unittest.mock import Mock, MagicMock +from unittest.mock import MagicMock, Mock import pytest +from requests import Response -from src.providers.http_provider import HTTPProvider, NoHostsProvided, NotOkResponse +from src.metrics.prometheus.basic import CL_REQUESTS_DURATION +from src.providers.http_provider import HTTPProvider, NoHostsProvided, NotOkResponse, data_is_any @pytest.mark.unit @@ -28,7 +30,7 @@ def test_no_providers(): @pytest.mark.unit def test_all_fallbacks_ok(): provider = HTTPProvider(['http://localhost:1', 'http://localhost:2'], 5 * 60, 1, 1) - provider._get_without_fallbacks = lambda host, endpoint, path_params, query_params, stream: (host, endpoint) + provider._get_without_fallbacks = lambda host, endpoint, path_params, query_params, stream, **_: (host, endpoint) assert provider._get('test') == ('http://localhost:1', 'test') assert len(provider.get_all_providers()) == 2 @@ -42,7 +44,7 @@ def test_all_fallbacks_bad(): @pytest.mark.unit def test_first_fallback_bad(): - def _simple_get(host, endpoint, *_): + def _simple_get(host, endpoint, *args, **kwargs): if host == 'http://localhost:1': raise Exception('Bad host') # pylint: disable=broad-exception-raised return host, endpoint @@ -57,7 +59,7 @@ def test_force_raise(): class CustomError(Exception): pass - def _simple_get(host, endpoint, *_): + def _simple_get(host, endpoint, *args, **kwargs): if host == 'http://localhost:1': raise Exception('Bad host') # pylint: disable=broad-exception-raised return host, endpoint @@ -66,7 +68,31 @@ def _simple_get(host, endpoint, *_): provider._get_without_fallbacks = Mock(side_effect=_simple_get) with pytest.raises(CustomError): provider._get('test', force_raise=lambda _: CustomError()) - provider._get_without_fallbacks.assert_called_once_with('http://localhost:1', 'test', None, None, False) + provider._get_without_fallbacks.assert_called_once_with( + 'http://localhost:1', + 'test', + None, + None, + stream=False, + retval_validator=data_is_any, + ) + + +@pytest.mark.unit +def test_retval_validator(): + provider = HTTPProvider(['http://localhost:1', 'http://localhost:2'], 5 * 60, 1, 1) + provider.PROMETHEUS_HISTOGRAM = CL_REQUESTS_DURATION + + resp = Response() + resp.status_code = 200 + resp._content = b'{"data": {}}' + provider.session.get = Mock(return_value=resp) + + def failed_validation(*args, **kwargs): + raise ValueError("Validation failed") + + with pytest.raises(ValueError, match="Validation failed"): + provider._get('test', retval_validator=failed_validation) @pytest.mark.unit diff --git a/tests/utils/test_dataclass.py b/tests/utils/test_dataclass.py index 61548dab0..62f64ca80 100644 --- a/tests/utils/test_dataclass.py +++ b/tests/utils/test_dataclass.py @@ -153,7 +153,7 @@ def test_dataclass_ignore_extra_fields(): def test_dataclass_raises_missing_field(): response = {"name": "Bob"} with pytest.raises(TypeError, match="age"): - pet = Pet.from_response(**response) + Pet.from_response(**response) @dataclass