diff --git a/Dockerfile b/Dockerfile index 29c1187c..dd535b71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:14.18.1-alpine3.13 as building # needed for git dependencies RUN apk update && apk upgrade && \ -apk add --no-cache bash=5.1.16-r0 git=2.30.6-r0 openssh=8.4_p1-r4 python3=3.8.15-r0 make=4.3-r0 g++=10.2.1_pre1-r3 + apk add --no-cache bash=5.1.16-r0 git=2.30.6-r0 openssh=8.4_p1-r4 python3=3.8.15-r0 make=4.3-r0 g++=10.2.1_pre1-r3 RUN mkdir /council @@ -13,10 +13,9 @@ RUN npm i -g npm@7.19.0 COPY ./package*.json ./ COPY ./yarn*.lock ./ -RUN yarn install --frozen-lockfile --non-interactive && yarn cache clean - COPY ./tsconfig*.json ./ COPY ./src ./src +RUN yarn install --frozen-lockfile --non-interactive && yarn cache clean RUN yarn typechain && yarn build diff --git a/package.json b/package.json index 51e5343e..95fd1a9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lido-council-daemon", - "version": "3.0.0", + "version": "3.0.2", "description": "Lido Council Daemon", "author": "Lido team", "private": true, @@ -31,7 +31,7 @@ "dependencies": { "@chainsafe/blst": "^0.2.4", "@chainsafe/ssz": "^0.9.2", - "@ethersproject/providers": "^5.4.5", + "@ethersproject/providers": "5.7.2", "@lido-nestjs/fetch": "^1.3.1", "@lido-nestjs/key-validation": "^7.4.0", "@lido-nestjs/middleware": "^1.1.1", @@ -48,7 +48,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "compare-versions": "^6.1.0", - "ethers": "^5.4.7", + "ethers": "5.7.2", "glob": "^7.1.2", "kafkajs": "^1.15.0", "level": "^8.0.1", diff --git a/src/abi/IStakingModule.abi.json b/src/abi/IStakingModule.abi.json index 0afd7662..34008ae9 100644 --- a/src/abi/IStakingModule.abi.json +++ b/src/abi/IStakingModule.abi.json @@ -1,4 +1,17 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "name": "NonceChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -10,30 +23,43 @@ }, { "indexed": false, - "internalType": "uint256", - "name": "trimmedKeysCount", - "type": "uint256" + "internalType": "bytes", + "name": "pubkey", + "type": "bytes" } ], - "name": "UnusedValidatorsKeysTrimmed", + "name": "SigningKeyAdded", "type": "event" }, { "anonymous": false, "inputs": [ { - "indexed": false, + "indexed": true, "internalType": "uint256", - "name": "validatorsKeysNonce", + "name": "nodeOperatorId", "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "pubkey", + "type": "bytes" } ], - "name": "ValidatorsKeysNonceChanged", + "name": "SigningKeyRemoved", "type": "event" }, { - "inputs": [], - "name": "finishUpdatingExitedValidatorsKeysCount", + "inputs": [ + { "internalType": "bytes", "name": "_nodeOperatorIds", "type": "bytes" }, + { + "internalType": "bytes", + "name": "_vettedSigningKeysCounts", + "type": "bytes" + } + ], + "name": "decreaseVettedSigningKeysCount", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -45,6 +71,22 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { "internalType": "uint256", "name": "_offset", "type": "uint256" }, + { "internalType": "uint256", "name": "_limit", "type": "uint256" } + ], + "name": "getNodeOperatorIds", + "outputs": [ + { + "internalType": "uint256[]", + "name": "nodeOperatorIds", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -58,27 +100,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "getNodeOperatorsCount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getType", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getValidatorsKeysNonce", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -87,21 +108,46 @@ "type": "uint256" } ], - "name": "getValidatorsKeysStats", + "name": "getNodeOperatorSummary", "outputs": [ { "internalType": "uint256", - "name": "exitedValidatorsCount", + "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": "activeValidatorsKeysCount", + "name": "totalDepositedValidators", "type": "uint256" }, { "internalType": "uint256", - "name": "readyToDepositValidatorsKeysCount", + "name": "depositableValidatorsCount", "type": "uint256" } ], @@ -110,58 +156,85 @@ }, { "inputs": [], - "name": "getValidatorsKeysStats", + "name": "getNodeOperatorsCount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNonce", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getStakingModuleSummary", "outputs": [ { "internalType": "uint256", - "name": "exitedValidatorsCount", + "name": "totalExitedValidators", "type": "uint256" }, { "internalType": "uint256", - "name": "activeValidatorsKeysCount", + "name": "totalDepositedValidators", "type": "uint256" }, { "internalType": "uint256", - "name": "readyToDepositValidatorsKeysCount", + "name": "depositableValidatorsCount", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getType", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ - { "internalType": "uint256", "name": "_totalShares", "type": "uint256" } + { + "internalType": "uint256", + "name": "_depositsCount", + "type": "uint256" + }, + { "internalType": "bytes", "name": "_depositCalldata", "type": "bytes" } + ], + "name": "obtainDepositData", + "outputs": [ + { "internalType": "bytes", "name": "publicKeys", "type": "bytes" }, + { "internalType": "bytes", "name": "signatures", "type": "bytes" } ], - "name": "handleRewardsMinted", - "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], - "name": "invalidateReadyToDepositKeys", + "name": "onExitedAndStuckValidatorsCountsUpdated", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { "internalType": "uint256", "name": "_keysCount", "type": "uint256" }, - { "internalType": "bytes", "name": "_calldata", "type": "bytes" } - ], - "name": "requestValidatorsKeysForDeposits", - "outputs": [ - { - "internalType": "uint256", - "name": "returnedKeysCount", - "type": "uint256" - }, - { "internalType": "bytes", "name": "publicKeys", "type": "bytes" }, - { "internalType": "bytes", "name": "signatures", "type": "bytes" } + { "internalType": "uint256", "name": "_totalShares", "type": "uint256" } ], + "name": "onRewardsMinted", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "onWithdrawalCredentialsChanged", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -174,16 +247,30 @@ }, { "internalType": "uint256", - "name": "_exitedValidatorsKeysCount", + "name": "_exitedValidatorsCount", "type": "uint256" }, { "internalType": "uint256", - "name": "_stuckValidatorsKeysCount", + "name": "_stuckValidatorsCount", "type": "uint256" } ], - "name": "unsafeUpdateValidatorsKeysCount", + "name": "unsafeUpdateValidatorsCount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "_nodeOperatorIds", "type": "bytes" }, + { + "internalType": "bytes", + "name": "_exitedValidatorsCounts", + "type": "bytes" + } + ], + "name": "updateExitedValidatorsCount", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -197,12 +284,26 @@ }, { "internalType": "uint256", - "name": "_exitedValidatorKeysCount", + "name": "_refundedValidatorsCount", "type": "uint256" } ], - "name": "updateExitedValidatorsKeysCount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "updateRefundedValidatorsCount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "_nodeOperatorIds", "type": "bytes" }, + { + "internalType": "bytes", + "name": "_stuckValidatorsCounts", + "type": "bytes" + } + ], + "name": "updateStuckValidatorsCount", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -215,11 +316,12 @@ }, { "internalType": "uint256", - "name": "_stuckValidatorKeysCount", + "name": "_targetLimitMode", "type": "uint256" - } + }, + { "internalType": "uint256", "name": "_targetLimit", "type": "uint256" } ], - "name": "updateStuckValidatorsKeysCount", + "name": "updateTargetValidatorsLimits", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/src/abi/csm.abi.json b/src/abi/csm.abi.json deleted file mode 100644 index bb3bcb56..00000000 --- a/src/abi/csm.abi.json +++ /dev/null @@ -1,2666 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "moduleType", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "elStealingFine", - "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": "DEPOSIT_SIZE", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "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": "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": "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": "toRemove", - "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": "decreaseOperatorVettedKeys", - "inputs": [ - { - "name": "nodeOperatorIds", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "vettedKeysByOperator", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "depositETH", - "inputs": [ - { - "name": "nodeOperatorId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "depositQueueItem", - "inputs": [ - { - "name": "index", - "type": "uint128", - "internalType": "uint128" - } - ], - "outputs": [ - { - "name": "item", - "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": "managerAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "proposedManagerAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "rewardAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "proposedRewardAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "active", - "type": "bool", - "internalType": "bool" - }, - { - "name": "targetLimit", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "targetLimitMode", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "stuckPenaltyEndTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalExitedKeys", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalAddedKeys", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalWithdrawnKeys", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalDepositedKeys", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalVettedKeys", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "stuckValidatorsCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "refundedValidatorsCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "depositableValidatorsCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "enqueuedCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "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": "getNodeOperatorRewardAddress", - "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": "uint8", - "internalType": "uint8" - }, - { - "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": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "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": "verifier", - "type": "address", - "internalType": "address" - }, - { - "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": "queue", - "inputs": [], - "outputs": [ - { - "name": "head", - "type": "uint128", - "internalType": "uint128" - }, - { - "name": "length", - "type": "uint128", - "internalType": "uint128" - } - ], - "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": "requestRewardsETH", - "inputs": [ - { - "name": "nodeOperatorId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "ethAmount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "cumulativeFeeShares", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rewardsProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - } - ], - "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": "nodeOperatorId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "refundedValidatorsCount", - "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": "uint8", - "internalType": "uint8" - }, - { - "name": "targetLimit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "BatchEnqueued", - "inputs": [ - { - "name": "nodeOperatorId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "count", - "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": "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": "RefundedKeysCountChanged", - "inputs": [ - { - "name": "nodeOperatorId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "refundedKeysCount", - "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": "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": "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": "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": "PauseUntilMustBeInFuture", - "inputs": [] - }, - { - "type": "error", - "name": "PausedExpected", - "inputs": [] - }, - { - "type": "error", - "name": "QueueIsEmpty", - "inputs": [] - }, - { - "type": "error", - "name": "QueueLookupNoLimit", - "inputs": [] - }, - { - "type": "error", - "name": "ResumedExpected", - "inputs": [] - }, - { - "type": "error", - "name": "SenderIsNotEligible", - "inputs": [] - }, - { - "type": "error", - "name": "SigningKeysInvalidOffset", - "inputs": [] - }, - { - "type": "error", - "name": "StuckKeysHigherThanTotalDepositedMinusTotalExited", - "inputs": [] - }, - { - "type": "error", - "name": "ZeroPauseDuration", - "inputs": [] - } -] diff --git a/src/abi/security.pause.v2.abi.json b/src/abi/security.deprecated.pause.abi.json similarity index 100% rename from src/abi/security.pause.v2.abi.json rename to src/abi/security.deprecated.pause.abi.json diff --git a/src/abi/signing-key.abi.json b/src/abi/signing-key.abi.json deleted file mode 100644 index c1a16a87..00000000 --- a/src/abi/signing-key.abi.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "nodeOperatorId", "type": "uint256" }, - { "indexed": false, "name": "pubkey", "type": "bytes" } - ], - "name": "SigningKeyAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "nodeOperatorId", "type": "uint256" }, - { "indexed": false, "name": "pubkey", "type": "bytes" } - ], - "name": "SigningKeyRemoved", - "type": "event" - } -] diff --git a/src/common/config/config-loader.service.spec.ts b/src/common/config/config-loader.service.spec.ts index 69a650cd..9f33789c 100644 --- a/src/common/config/config-loader.service.spec.ts +++ b/src/common/config/config-loader.service.spec.ts @@ -1,7 +1,8 @@ +import { BigNumber } from '@ethersproject/bignumber'; import { Test } from '@nestjs/testing'; import { plainToClass } from 'class-transformer'; +import { validateOrReject, ValidationError } from 'class-validator'; import { ConfigLoaderService } from './config-loader.service'; -import { BadConfigException } from './exceptions'; import { InMemoryConfiguration } from './in-memory-configuration'; const FAKE_FS = { @@ -13,6 +14,25 @@ const DEFAULTS = { RPC_URL: 'some-rpc-url', RABBITMQ_URL: 'some-rabbit-url', RABBITMQ_LOGIN: 'some-rabbit-login', + KEYS_API_URL: 'keys-api', +}; + +const extractError = async ( + fn: Promise, +): Promise<[ValidationError[], T]> => { + try { + return [[], await fn]; + } catch (error: any) { + return [error as ValidationError[], undefined as unknown as T]; + } +}; + +const toHaveProblemWithRecords = ( + recordsKeys: string[], + errors: ValidationError[], +) => { + const errorKeys = errors.map((error) => error.property); + expect(recordsKeys.sort()).toEqual(errorKeys.sort()); }; describe('ConfigLoaderService base spec', () => { @@ -105,6 +125,96 @@ describe('ConfigLoaderService base spec', () => { }); }); + describe('kapi url config', () => { + test('all invariants are empty', async () => { + const prepConfig = plainToClass(InMemoryConfiguration, { + RABBITMQ_PASSCODE: 'some-rabbit-passcode', + ...DEFAULTS, + KEYS_API_URL: undefined, + }); + const [validationErrors] = await extractError( + configLoaderService.loadSecrets(prepConfig), + ); + + toHaveProblemWithRecords( + ['KEYS_API_URL', 'KEYS_API_PORT', 'KEYS_API_HOST'], + validationErrors, + ); + }); + + test('KEYS_API_URL is set and the rest is default', async () => { + const KEYS_API_URL = 'kapi-url'; + const KEYS_API_HOST = ''; + const KEYS_API_PORT = 0; + const prepConfig = plainToClass(InMemoryConfiguration, { + RABBITMQ_PASSCODE: 'some-rabbit-passcode', + ...DEFAULTS, + KEYS_API_URL, + }); + const [validationErrors, result] = await extractError( + configLoaderService.loadSecrets(prepConfig), + ); + expect(validationErrors).toHaveLength(0); + expect(result.KEYS_API_URL).toBe(KEYS_API_URL); + expect(result.KEYS_API_HOST).toBe(KEYS_API_HOST); + expect(result.KEYS_API_PORT).toBe(KEYS_API_PORT); + }); + + test('KEYS_API_URL is empty and the rest is set', async () => { + const KEYS_API_URL = undefined; + const KEYS_API_HOST = 'kapi-host'; + const KEYS_API_PORT = 2222; + const prepConfig = plainToClass(InMemoryConfiguration, { + RABBITMQ_PASSCODE: 'some-rabbit-passcode', + ...DEFAULTS, + KEYS_API_URL, + KEYS_API_HOST, + KEYS_API_PORT, + }); + const [validationErrors, result] = await extractError( + configLoaderService.loadSecrets(prepConfig), + ); + expect(validationErrors).toHaveLength(0); + expect(result.KEYS_API_URL).toBe(KEYS_API_URL); + expect(result.KEYS_API_HOST).toBe(KEYS_API_HOST); + expect(result.KEYS_API_PORT).toBe(KEYS_API_PORT); + }); + + test('KEYS_API_URL and KEYS_API_PORT are empty and the KEYS_API_HOST is set', async () => { + const KEYS_API_URL = undefined; + const KEYS_API_HOST = 'kapi-host'; + const KEYS_API_PORT = 0; + const prepConfig = plainToClass(InMemoryConfiguration, { + RABBITMQ_PASSCODE: 'some-rabbit-passcode', + ...DEFAULTS, + KEYS_API_URL, + KEYS_API_HOST, + KEYS_API_PORT, + }); + const [validationErrors] = await extractError( + configLoaderService.loadSecrets(prepConfig), + ); + toHaveProblemWithRecords(['KEYS_API_PORT'], validationErrors); + }); + + test('KEYS_API_URL and KEYS_API_HOST are empty and the KEYS_API_PORT is set', async () => { + const KEYS_API_URL = undefined; + const KEYS_API_HOST = ''; + const KEYS_API_PORT = 2222; + const prepConfig = plainToClass(InMemoryConfiguration, { + RABBITMQ_PASSCODE: 'some-rabbit-passcode', + ...DEFAULTS, + KEYS_API_URL, + KEYS_API_HOST, + KEYS_API_PORT, + }); + const [validationErrors] = await extractError( + configLoaderService.loadSecrets(prepConfig), + ); + toHaveProblemWithRecords(['KEYS_API_HOST'], validationErrors); + }); + }); + describe('wallet', () => { let configLoaderService: ConfigLoaderService; const DEFAULTS_WITH_RABBIT = { @@ -171,82 +281,171 @@ describe('ConfigLoaderService base spec', () => { }); describe('balance', () => { - let configLoaderService: ConfigLoaderService; const DEFAULTS_WITH_RABBIT = { ...DEFAULTS, RABBITMQ_PASSCODE: 'some-rabbit-passcode', }; - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ConfigLoaderService], - }).compile(); + test('should throw an error for an excessively small WALLET_CRITICAL_BALANCE', async () => { + const WALLET_CRITICAL_BALANCE = '0.0000000000000000001'; + const plainConfig = plainToClass(InMemoryConfiguration, { + WALLET_CRITICAL_BALANCE, + ...DEFAULTS_WITH_RABBIT, + }); - configLoaderService = moduleRef.get(ConfigLoaderService); + expect(plainConfig).toHaveProperty('WALLET_CRITICAL_BALANCE'); + expect(plainConfig.WALLET_CRITICAL_BALANCE).toBeNaN(); + + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).catch((errors) => { + expect(errors).toBeInstanceOf(Array); + expect(errors.length).toBe(1); + expect(errors[0]).toBeInstanceOf(ValidationError); + expect(errors[0].property).toBe('WALLET_CRITICAL_BALANCE'); + expect(errors[0].constraints).toHaveProperty( + 'isInstance', + 'WALLET_CRITICAL_BALANCE must be an instance of BigNumber', + ); + }); }); - test('should throw an error for an excessively small WALLET_CRITICAL_BALANCE', async () => { - const WALLET_CRITICAL_BALANCE = '0.0000000000000000001'; - try { - plainToClass(InMemoryConfiguration, { - WALLET_CRITICAL_BALANCE, - }); - - throw new Error('Expected BadConfigException was not thrown'); - } catch (error) { - if (error instanceof BadConfigException) { - expect(error.message).toBe( - `Invalid WALLET_CRITICAL_BALANCE value: ${WALLET_CRITICAL_BALANCE}. Please ensure it's a valid Ether amount that can be converted to Wei.`, - ); - } else { - throw new Error(`Unexpected error type`); - } - } + test('should throw an error for an empty WALLET_CRITICAL_BALANCE', async () => { + const WALLET_CRITICAL_BALANCE = ''; + const plainConfig = plainToClass(InMemoryConfiguration, { + WALLET_CRITICAL_BALANCE, + ...DEFAULTS_WITH_RABBIT, + }); + + expect(plainConfig).toHaveProperty('WALLET_CRITICAL_BALANCE'); + expect(plainConfig.WALLET_CRITICAL_BALANCE).toBeNaN(); + + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).catch((errors) => { + expect(errors).toBeInstanceOf(Array); + expect(errors.length).toBe(1); + expect(errors[0]).toBeInstanceOf(ValidationError); + expect(errors[0].property).toBe('WALLET_CRITICAL_BALANCE'); + expect(errors[0].constraints).toHaveProperty( + 'isInstance', + 'WALLET_CRITICAL_BALANCE must be an instance of BigNumber', + ); + }); }); test('should handle normal WALLET_CRITICAL_BALANCE values correctly', async () => { - const prepConfig = plainToClass(InMemoryConfiguration, { + const plainConfig = plainToClass(InMemoryConfiguration, { WALLET_CRITICAL_BALANCE: '0.2', ...DEFAULTS_WITH_RABBIT, }); - const config = await configLoaderService.loadSecrets(prepConfig); + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).then(() => { + expect(plainConfig).toHaveProperty('WALLET_CRITICAL_BALANCE'); + expect(plainConfig.WALLET_CRITICAL_BALANCE).toBeInstanceOf(BigNumber); + expect(plainConfig.WALLET_CRITICAL_BALANCE.toString()).toBe( + '200000000000000000', + ); + }); + }); + + test('should use default WALLET_CRITICAL_BALANCE value', async () => { + const plainConfig = plainToClass(InMemoryConfiguration, { + ...DEFAULTS_WITH_RABBIT, + }); - expect(config).toHaveProperty('WALLET_CRITICAL_BALANCE'); - expect(config.WALLET_CRITICAL_BALANCE.toString()).toBe( - '200000000000000000', - ); // Equivalent of 0.2 ETH in Wei + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).then(() => { + expect(plainConfig).toHaveProperty('WALLET_CRITICAL_BALANCE'); + expect(plainConfig.WALLET_CRITICAL_BALANCE).toBeInstanceOf(BigNumber); + expect(plainConfig.WALLET_CRITICAL_BALANCE.toString()).toBe( + '200000000000000000', + ); + }); }); test('should throw an error for an excessively small WALLET_MIN_BALANCE', async () => { const WALLET_MIN_BALANCE = '0.0000000000000000001'; - try { - plainToClass(InMemoryConfiguration, { - WALLET_MIN_BALANCE, - }); - - throw new Error('Expected BadConfigException was not thrown'); - } catch (error) { - if (error instanceof BadConfigException) { - expect(error.message).toBe( - `Invalid WALLET_MIN_BALANCE value: ${WALLET_MIN_BALANCE}. Please ensure it's a valid Ether amount that can be converted to Wei.`, - ); - } else { - throw new Error(`Unexpected error type`); - } - } + const plainConfig = plainToClass(InMemoryConfiguration, { + WALLET_MIN_BALANCE, + ...DEFAULTS_WITH_RABBIT, + }); + + expect(plainConfig).toHaveProperty('WALLET_MIN_BALANCE'); + expect(plainConfig.WALLET_MIN_BALANCE).toBeNaN(); + + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).catch((errors) => { + expect(errors).toBeInstanceOf(Array); + expect(errors.length).toBe(1); + expect(errors[0]).toBeInstanceOf(ValidationError); + expect(errors[0].property).toBe('WALLET_MIN_BALANCE'); + expect(errors[0].constraints).toHaveProperty( + 'isInstance', + 'WALLET_MIN_BALANCE must be an instance of BigNumber', + ); + }); + }); + + test('should throw an error for an empty WALLET_MIN_BALANCE', async () => { + const WALLET_MIN_BALANCE = ''; + const plainConfig = plainToClass(InMemoryConfiguration, { + WALLET_MIN_BALANCE, + ...DEFAULTS_WITH_RABBIT, + }); + + expect(plainConfig).toHaveProperty('WALLET_MIN_BALANCE'); + expect(plainConfig.WALLET_MIN_BALANCE).toBeNaN(); + + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).catch((errors) => { + expect(errors).toBeInstanceOf(Array); + expect(errors.length).toBe(1); + expect(errors[0]).toBeInstanceOf(ValidationError); + expect(errors[0].property).toBe('WALLET_MIN_BALANCE'); + expect(errors[0].constraints).toHaveProperty( + 'isInstance', + 'WALLET_MIN_BALANCE must be an instance of BigNumber', + ); + }); }); test('should handle normal WALLET_MIN_BALANCE values correctly', async () => { - const prepConfig = plainToClass(InMemoryConfiguration, { + const plainConfig = plainToClass(InMemoryConfiguration, { WALLET_MIN_BALANCE: '0.2', ...DEFAULTS_WITH_RABBIT, }); - const config = await configLoaderService.loadSecrets(prepConfig); + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).then(() => { + expect(plainConfig).toHaveProperty('WALLET_MIN_BALANCE'); + expect(plainConfig.WALLET_MIN_BALANCE).toBeInstanceOf(BigNumber); + expect(plainConfig.WALLET_MIN_BALANCE.toString()).toBe( + '200000000000000000', + ); + }); + }); - expect(config).toHaveProperty('WALLET_MIN_BALANCE'); - expect(config.WALLET_MIN_BALANCE.toString()).toBe('200000000000000000'); // Equivalent of 0.2 ETH in Wei + test('should use default WALLET_MIN_BALANCE value', async () => { + const plainConfig = plainToClass(InMemoryConfiguration, { + ...DEFAULTS_WITH_RABBIT, + }); + + await validateOrReject(plainConfig, { + validationError: { target: false, value: false }, + }).then(() => { + expect(plainConfig).toHaveProperty('WALLET_MIN_BALANCE'); + expect(plainConfig.WALLET_MIN_BALANCE).toBeInstanceOf(BigNumber); + expect(plainConfig.WALLET_MIN_BALANCE.toString()).toBe( + '500000000000000000', + ); + }); }); }); }); diff --git a/src/common/config/configuration.ts b/src/common/config/configuration.ts index 349a6f51..5539d95a 100644 --- a/src/common/config/configuration.ts +++ b/src/common/config/configuration.ts @@ -31,6 +31,7 @@ export interface Configuration { REGISTRY_KEYS_QUERY_CONCURRENCY: number; KEYS_API_PORT: number; KEYS_API_HOST: string; + KEYS_API_URL: string; LOCATOR_DEVNET_ADDRESS: string; WALLET_MIN_BALANCE: ethers.BigNumber; WALLET_CRITICAL_BALANCE: ethers.BigNumber; diff --git a/src/common/config/exceptions.ts b/src/common/config/exceptions.ts deleted file mode 100644 index 9810c4c0..00000000 --- a/src/common/config/exceptions.ts +++ /dev/null @@ -1 +0,0 @@ -export class BadConfigException extends Error {} diff --git a/src/common/config/in-memory-configuration.ts b/src/common/config/in-memory-configuration.ts index 636631df..b69809cd 100644 --- a/src/common/config/in-memory-configuration.ts +++ b/src/common/config/in-memory-configuration.ts @@ -1,6 +1,7 @@ import { Transform } from 'class-transformer'; import { IsIn, + IsInstance, IsNotEmpty, IsNumber, IsOptional, @@ -12,8 +13,8 @@ import { Injectable } from '@nestjs/common'; import { Configuration, PubsubService } from './configuration'; import { SASLMechanism } from '../../transport'; import { implementationOf } from '../di/decorators/implementationOf'; -import { ethers } from 'ethers'; -import { BadConfigException } from './exceptions'; +import { ethers, BigNumber } from 'ethers'; +import { TransformToWei } from 'common/decorators/transform-to-wei'; const RABBITMQ = 'rabbitmq'; const KAFKA = 'kafka'; @@ -127,49 +128,36 @@ export class InMemoryConfiguration implements Configuration { @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) REGISTRY_KEYS_QUERY_CONCURRENCY = 5; + @ValidateIf((conf) => !conf.KEYS_API_URL) @IsNotEmpty() @IsNumber() @Min(1) @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) - KEYS_API_PORT = 3001; + KEYS_API_PORT = 0; - @IsOptional() + @ValidateIf((conf) => !conf.KEYS_API_URL) + @IsNotEmpty() + @IsString() + KEYS_API_HOST = ''; + + @ValidateIf((conf) => { + return !conf.KEYS_API_PORT && !conf.KEYS_API_HOST; + }) + @IsNotEmpty() @IsString() - KEYS_API_HOST = 'http://localhost'; + KEYS_API_URL = ''; @IsOptional() @IsString() LOCATOR_DEVNET_ADDRESS = ''; @IsOptional() - @Transform( - ({ value }) => { - try { - const weiValue = ethers.utils.parseEther(value || '0.5'); - return weiValue; - } catch (error) { - throw new BadConfigException( - `Invalid WALLET_MIN_BALANCE value: ${value}. Please ensure it's a valid Ether amount that can be converted to Wei.`, - ); - } - }, - { toClassOnly: true }, - ) - WALLET_MIN_BALANCE: ethers.BigNumber = ethers.utils.parseEther('0.5'); + @TransformToWei() + @IsInstance(BigNumber) + WALLET_MIN_BALANCE: BigNumber = ethers.utils.parseEther('0.5'); @IsOptional() - @Transform( - ({ value }) => { - try { - const weiValue = ethers.utils.parseEther(value || '0.2'); - return weiValue; - } catch (error) { - throw new BadConfigException( - `Invalid WALLET_CRITICAL_BALANCE value: ${value}. Please ensure it's a valid Ether amount that can be converted to Wei.`, - ); - } - }, - { toClassOnly: true }, - ) - WALLET_CRITICAL_BALANCE: ethers.BigNumber = ethers.utils.parseEther('0.2'); + @TransformToWei() + @IsInstance(BigNumber) + WALLET_CRITICAL_BALANCE: BigNumber = ethers.utils.parseEther('0.2'); } diff --git a/src/common/decorators/one-at-time.spec.ts b/src/common/decorators/one-at-time.spec.ts index 3088667c..514b14d7 100644 --- a/src/common/decorators/one-at-time.spec.ts +++ b/src/common/decorators/one-at-time.spec.ts @@ -1,8 +1,8 @@ -import { OneAtTime, StakingModuleId } from './one-at-time'; +import { OneAtTime, OneAtTimeCallId } from './one-at-time'; class TestOneAtTime { public value; - public stakingModuleId = new Map(); + public oneAtTimeCallId = new Map(); public executionLog: string[] = []; @@ -20,9 +20,9 @@ class TestOneAtTime { } @OneAtTime(2000) - async testStakingModuleId(@StakingModuleId id, value) { + async testOneAtTimeCallId(@OneAtTimeCallId id, value) { this.executionLog.push(`start-${id}-${value}`); - this.stakingModuleId.set(id, value); + this.oneAtTimeCallId.set(id, value); await this.sleep(1000); this.executionLog.push(`end-${id}-${value}`); @@ -50,12 +50,12 @@ it('OneAtTime', async () => { it('StakingModuleId', async () => { const testOneAtTime = new TestOneAtTime(); - expect(testOneAtTime.stakingModuleId.get(1)).toBeUndefined(); - expect(testOneAtTime.stakingModuleId.get(2)).toBeUndefined(); + expect(testOneAtTime.oneAtTimeCallId.get(1)).toBeUndefined(); + expect(testOneAtTime.oneAtTimeCallId.get(2)).toBeUndefined(); - testOneAtTime.testStakingModuleId(1, 1); - testOneAtTime.testStakingModuleId(1, 2); - testOneAtTime.testStakingModuleId(2, 2); + testOneAtTime.testOneAtTimeCallId(1, 1); + testOneAtTime.testOneAtTimeCallId(1, 2); + testOneAtTime.testOneAtTimeCallId(2, 2); await testOneAtTime.sleep(1500); @@ -64,15 +64,15 @@ it('StakingModuleId', async () => { expect.arrayContaining(['start-1-1', 'end-1-1', 'start-2-2', 'end-2-2']), ); - expect(testOneAtTime.stakingModuleId.get(1)).toEqual(1); - expect(testOneAtTime.stakingModuleId.get(2)).toEqual(2); + expect(testOneAtTime.oneAtTimeCallId.get(1)).toEqual(1); + expect(testOneAtTime.oneAtTimeCallId.get(2)).toEqual(2); testOneAtTime.executionLog = []; - await testOneAtTime.testStakingModuleId(1, 2); + await testOneAtTime.testOneAtTimeCallId(1, 2); expect(testOneAtTime.executionLog.length).toEqual(2); expect(testOneAtTime.executionLog).toEqual( expect.arrayContaining(['start-1-2', 'end-1-2']), ); - expect(testOneAtTime.stakingModuleId.get(1)).toEqual(2); + expect(testOneAtTime.oneAtTimeCallId.get(1)).toEqual(2); }); diff --git a/src/common/decorators/one-at-time.ts b/src/common/decorators/one-at-time.ts index 501ac74e..9060a03b 100644 --- a/src/common/decorators/one-at-time.ts +++ b/src/common/decorators/one-at-time.ts @@ -1,27 +1,28 @@ import 'reflect-metadata'; -const stakingModuleId = Symbol('StakingModuleId'); +const oneAtTimeCallIdKey = Symbol('OneAtTimeCallId'); /** - * A decorator that marks a specific parameter in a method for identifying the staking module ID + * A decorator that marks a specific parameter in a method for identifying the OneAtTime call ID. + * This ID allows the same method to be executed concurrently with different parameters. */ -export function StakingModuleId( +export function OneAtTimeCallId( target: any, propertyKey: string | symbol, parameterIndex: number, ) { const existingMetadata: number[] = - Reflect.getOwnMetadata(stakingModuleId, target, propertyKey) || []; + Reflect.getOwnMetadata(oneAtTimeCallIdKey, target, propertyKey) || []; if (existingMetadata.length === 0) { Reflect.defineMetadata( - stakingModuleId, + oneAtTimeCallIdKey, [parameterIndex], target, propertyKey, ); } else { throw new Error( - `StakingModuleId decorator can only be applied to one parameter in method ${String( + `OneAtTimeCallId decorator can only be applied to one parameter in method ${String( propertyKey, )}. It is already applied to parameter index ${existingMetadata[0]}`, ); @@ -29,9 +30,9 @@ export function StakingModuleId( } /** - * A decorator factory that produces a method decorator ensuring a function executes one at a time. + * A decorator factory that ensures a function executes one at a time. * Calls to the decorated method are restricted so that only one instance can be executed concurrently, - * either globally or per staking module ID + * either globally or per OneAtTime call ID. * A stuck function with the OneAtTime decorator will prevent the next executions of this function. * That is why a timeout is set. If the execution of the promise is stuck, a timeout will occur. The default timeout is 10 minutes. */ @@ -48,13 +49,13 @@ export function OneAtTime Promise>( const isExecutingMap = new Map(); descriptor.value = async function (this: any, ...args) { - const stakingModuleIdArgs = - Reflect.getMetadata(stakingModuleId, target, propertyName) || []; + const oneAtTimeCallIdArgs = + Reflect.getMetadata(oneAtTimeCallIdKey, target, propertyName) || []; - const moduleId = - stakingModuleIdArgs.length > 0 ? args[stakingModuleIdArgs[0]] : null; + const callId = + oneAtTimeCallIdArgs.length > 0 ? args[oneAtTimeCallIdArgs[0]] : null; - if ((moduleId && isExecutingMap.get(moduleId)) || isExecuting) { + if ((callId && isExecutingMap.get(callId)) || isExecuting) { this.logger?.debug(`Already running ${propertyName}`, { propertyName, executing: isExecuting, @@ -63,8 +64,8 @@ export function OneAtTime Promise>( return; } - if (moduleId) { - isExecutingMap.set(moduleId, true); + if (callId) { + isExecutingMap.set(callId, true); } else { isExecuting = true; } @@ -86,8 +87,8 @@ export function OneAtTime Promise>( } catch (error) { this.logger.error(error); } finally { - if (moduleId) { - isExecutingMap.set(moduleId, false); + if (callId) { + isExecutingMap.set(callId, false); } else { isExecuting = false; } diff --git a/src/common/decorators/transform-to-wei.ts b/src/common/decorators/transform-to-wei.ts new file mode 100644 index 00000000..5960636a --- /dev/null +++ b/src/common/decorators/transform-to-wei.ts @@ -0,0 +1,16 @@ +import { Transform } from 'class-transformer'; +import { ethers } from 'ethers'; + +export function TransformToWei() { + return Transform( + ({ value }) => { + try { + const weiValue = ethers.utils.parseEther(value); + return weiValue; + } catch (error) { + return NaN; + } + }, + { toClassOnly: true }, + ); +} diff --git a/src/contracts/deposit/deposit-tree/deposit-tree.spec.ts b/src/contracts/deposit/deposit-tree/deposit-tree.spec.ts deleted file mode 100644 index 97cacbcb..00000000 --- a/src/contracts/deposit/deposit-tree/deposit-tree.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { DepositTree } from './deposit-tree'; - -describe('DepositTree', () => { - let depositTree; - - beforeEach(() => { - depositTree = new DepositTree(); - }); - - test('should correctly initialize', () => { - expect(depositTree.nodeCount).toBe(0); - expect(depositTree.branch.length).toBe(0); - expect(depositTree.zeroHashes[0]).toEqual(DepositTree.ZERO_HASH); - }); - - test('insert should correctly modify the tree', () => { - depositTree.insert({ - pubkey: '0xaabbccdd', - wc: '0x11223344', - amount: '0x0000000000000001', // Little endian of 1 - signature: '0x55667788', - }); - - expect(depositTree.nodeCount).toBe(1); - - depositTree.insert({ - pubkey: '0xaabbccdd', - wc: '0x11223344', - amount: '0x0000000000000002', // Little endian of 2 - signature: '0x55667788', - }); - - expect(depositTree.nodeCount).toBe(2); - }); - - test('clone should create an exact copy of the tree', () => { - const nodeData = { - pubkey: '0xaabbccdd', - wc: '0x11223344', - amount: '0x0000000000000001', - signature: '0x55667788', - }; - depositTree.insert(nodeData); - const clonedTree = depositTree.clone(); - expect(clonedTree).toEqual(depositTree); - expect(clonedTree.getRoot()).toEqual(depositTree.getRoot()); - }); -}); diff --git a/src/contracts/deposit/deposit.constants.ts b/src/contracts/deposit/deposit.constants.ts deleted file mode 100644 index 3620efba..00000000 --- a/src/contracts/deposit/deposit.constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CHAINS } from '@lido-sdk/constants'; - -export const DEPLOYMENT_BLOCK_NETWORK: { - [key in CHAINS]?: number; -} = { - [CHAINS.Mainnet]: 11052984, - [CHAINS.Goerli]: 4367322, - [CHAINS.Holesky]: 0, -}; - -export const getDeploymentBlockByNetwork = (chainId: CHAINS): number => { - const address = DEPLOYMENT_BLOCK_NETWORK[chainId]; - if (address == null) throw new Error(`Chain ${chainId} is not supported`); - - return address; -}; - -export const DEPOSIT_EVENTS_CACHE_LAG_BLOCKS = 100; -export const DEPOSIT_EVENTS_STEP = 10_000; -export const DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE = 10; - -export const DEPOSIT_CACHE_FILE_NAME = 'deposit.events.json'; -export const DEPOSIT_CACHE_BATCH_SIZE = 100_000; - -export const DEPOSIT_CACHE_DEFAULT = Object.freeze({ - headers: { - startBlock: 0, - endBlock: 0, - }, - data: [], -}); diff --git a/src/contracts/deposit/deposit.module.ts b/src/contracts/deposit/deposit.module.ts deleted file mode 100644 index 96e5a19b..00000000 --- a/src/contracts/deposit/deposit.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SecurityModule } from 'contracts/security'; -import { LevelDBModule } from './leveldb'; -import { BlsModule } from 'bls'; -import { DepositService } from './deposit.service'; -import { DEPOSIT_CACHE_DEFAULT } from './deposit.constants'; -import { DepositIntegrityCheckerService } from './integrity-checker'; - -@Module({ - imports: [ - BlsModule, - SecurityModule, - LevelDBModule.register(DEPOSIT_CACHE_DEFAULT), - ], - providers: [DepositService, DepositIntegrityCheckerService], - exports: [DepositService], -}) -export class DepositModule {} diff --git a/src/contracts/deposit/deposit.service.spec.ts b/src/contracts/deposit/deposit.service.spec.ts deleted file mode 100644 index 53565a16..00000000 --- a/src/contracts/deposit/deposit.service.spec.ts +++ /dev/null @@ -1,562 +0,0 @@ -jest.mock('utils/sleep'); - -import { CHAINS } from '@lido-sdk/constants'; -import { Test } from '@nestjs/testing'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { Interface } from '@ethersproject/abi'; -import { LoggerService } from '@nestjs/common'; -import { getNetwork } from '@ethersproject/networks'; -import { sleep } from 'utils'; -import { LevelDBService } from './leveldb'; -import { - ERRORS_LIMIT_EXCEEDED, - MockProviderModule, - ProviderService, -} from 'provider'; -import { DepositAbi__factory } from 'generated'; -import { RepositoryModule, RepositoryService } from 'contracts/repository'; - -import { DepositModule } from './deposit.module'; -import { DepositService } from './deposit.service'; -import { PrometheusModule } from 'common/prometheus'; -import { LoggerModule } from 'common/logger'; -import { ConfigModule } from 'common/config'; -import { BlsService } from 'bls'; -import { LocatorService } from 'contracts/repository/locator/locator.service'; -import { mockLocator } from 'contracts/repository/locator/locator.mock'; -import { mockRepository } from 'contracts/repository/repository.mock'; -import { DepositTree } from './deposit-tree'; - -const mockSleep = sleep as jest.MockedFunction; - -describe('DepositService', () => { - let providerService: ProviderService; - let cacheService: LevelDBService; - let depositService: DepositService; - let loggerService: LoggerService; - let repositoryService: RepositoryService; - let blsService: BlsService; - let locatorService: LocatorService; - - const depositAddress = '0x' + '1'.repeat(40); - - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot(), - MockProviderModule.forRoot(), - DepositModule, - PrometheusModule, - LoggerModule, - RepositoryModule, - ], - }).compile(); - - providerService = moduleRef.get(ProviderService); - cacheService = moduleRef.get(LevelDBService); - depositService = moduleRef.get(DepositService); - repositoryService = moduleRef.get(RepositoryService); - blsService = moduleRef.get(BlsService); - loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); - - locatorService = moduleRef.get(LocatorService); - - jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); - jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); - jest.spyOn(loggerService, 'debug').mockImplementation(() => undefined); - - mockLocator(locatorService); - await mockRepository(repositoryService); - - jest - .spyOn(repositoryService, 'getDepositAddress') - .mockImplementation(async () => depositAddress); - }); - - describe('getDeploymentBlockByNetwork', () => { - it('should return block number for goerli', async () => { - jest - .spyOn(providerService.provider, 'detectNetwork') - .mockImplementation(async () => getNetwork(CHAINS.Goerli)); - - const blockNumber = await depositService.getDeploymentBlockByNetwork(); - expect(typeof blockNumber).toBe('number'); - expect(blockNumber).toBeGreaterThan(0); - }); - - it('should return block number for mainnet', async () => { - jest - .spyOn(providerService.provider, 'detectNetwork') - .mockImplementation(async () => getNetwork(CHAINS.Mainnet)); - - const blockNumber = await depositService.getDeploymentBlockByNetwork(); - expect(typeof blockNumber).toBe('number'); - expect(blockNumber).toBeGreaterThan(0); - }); - }); - - describe('deposit cache', () => { - beforeEach(async () => { - await cacheService.initialize(); - }); - - afterEach(async () => { - await cacheService.deleteCache(); - await cacheService.close(); - }); - describe('getCachedEvents', () => { - const deploymentBlock = 100; - - beforeEach(async () => { - jest - .spyOn(depositService, 'getDeploymentBlockByNetwork') - .mockImplementation(async () => deploymentBlock); - }); - - it('should return events from cache', async () => { - const cache = { - data: [{} as any], - headers: { - startBlock: deploymentBlock, - endBlock: deploymentBlock + 100, - }, - }; - - const mockCache = jest - .spyOn(cacheService, 'getEventsCache') - .mockImplementation(async () => cache); - - const result = await depositService.getCachedEvents(); - - expect(mockCache).toBeCalledTimes(1); - expect(result).toEqual(cache); - }); - - it('should return deploymentBlock if cache is empty', async () => { - const cache = { - data: [{} as any], - headers: { - startBlock: 0, - endBlock: 0, - }, - }; - - const mockCache = jest - .spyOn(cacheService, 'getEventsCache') - .mockImplementation(async () => cache); - - const result = await depositService.getCachedEvents(); - - expect(mockCache).toBeCalledTimes(1); - expect(result.headers.startBlock).toBe(deploymentBlock); - expect(result.headers.endBlock).toBe(deploymentBlock); - }); - }); - - describe('setCachedEvents', () => { - it('should call setCache from the cacheService', async () => { - const eventGroup = {} as any; - - const mockSetCache = jest - .spyOn(cacheService, 'insertEventsCacheBatch') - .mockImplementation(async () => undefined); - - await depositService.setCachedEvents(eventGroup); - - expect(mockSetCache).toBeCalledTimes(1); - expect(mockSetCache).toBeCalledWith({ headers: {} }); - }); - }); - - describe('fetchEventsFallOver', () => { - it('should fetch events', async () => { - const expected = { - endBlock: 0, - events: [], - startBlock: 10, - }; - - const from = 0; - const to = 10; - - const mockFetchEvents = jest - .spyOn(depositService, 'fetchEvents') - .mockImplementation(async () => expected); - - const result = await depositService.fetchEventsFallOver(from, to); - - expect(mockFetchEvents).toBeCalledTimes(1); - expect(mockFetchEvents).toBeCalledWith(from, to); - expect(result).toEqual(expected); - }); - - it('should fetch recursive if limit exceeded', async () => { - const event1 = {} as any; - const event2 = {} as any; - const expectedFirst = { events: [event1], startBlock: 0, endBlock: 4 }; - const expectedSecond = { - events: [event2], - startBlock: 5, - endBlock: 10, - }; - - const startBlock = 0; - const endBlock = 10; - - const mockFetchEvents = jest - .spyOn(depositService, 'fetchEvents') - .mockImplementationOnce(async () => { - throw { error: { code: ERRORS_LIMIT_EXCEEDED[0] } }; - }) - .mockImplementationOnce(async () => expectedFirst) - .mockImplementationOnce(async () => expectedSecond); - - const result = await depositService.fetchEventsFallOver( - startBlock, - endBlock, - ); - - const { calls, results } = mockFetchEvents.mock; - const events = [event1, event2]; - - expect(result).toEqual({ events, startBlock, endBlock }); - expect(mockFetchEvents).toBeCalledTimes(3); - expect(calls[0]).toEqual([startBlock, endBlock]); - expect(calls[1]).toEqual([ - expectedFirst.startBlock, - expectedFirst.endBlock, - ]); - expect(calls[2]).toEqual([ - expectedSecond.startBlock, - expectedSecond.endBlock, - ]); - await expect(results[1].value).resolves.toEqual(expectedFirst); - await expect(results[2].value).resolves.toEqual(expectedSecond); - }); - - it('should retry if error is unknown', async () => { - const events = []; - const startBlock = 0; - const endBlock = 10; - const expected = { events, startBlock, endBlock }; - - mockSleep.mockImplementationOnce(async () => undefined); - - const mockFetchEvents = jest - .spyOn(depositService, 'fetchEvents') - .mockImplementationOnce(async () => { - throw new Error(); - }) - .mockImplementationOnce(async () => expected); - - const result = await depositService.fetchEventsFallOver( - startBlock, - endBlock, - ); - - const { calls, results } = mockFetchEvents.mock; - - expect(result).toEqual(expected); - expect(mockFetchEvents).toBeCalledTimes(2); - expect(calls[0]).toEqual([startBlock, endBlock]); - expect(calls[1]).toEqual([startBlock, endBlock]); - await expect(results[0].value).rejects.toThrow(); - await expect(results[1].value).resolves.toEqual(expected); - - expect(mockSleep).toBeCalledTimes(1); - expect(mockSleep).toBeCalledWith(expect.any(Number)); - }); - }); - - describe('fetchEvents', () => { - it('should fetch events', async () => { - const freshPubkeys = ['0x4321', '0x8765']; - const startBlock = 100; - const endBlock = 200; - - jest - .spyOn(providerService.provider, 'getBlockNumber') - .mockImplementation(async () => endBlock); - - jest.spyOn(blsService, 'verify').mockImplementation(() => true); - - const mockProviderCall = jest - .spyOn(providerService.provider, 'getLogs') - .mockImplementation(async () => { - const iface = new Interface(DepositAbi__factory.abi); - const eventFragment = iface.getEvent('DepositEvent'); - - return freshPubkeys.map((pubkey) => { - const args = [pubkey, '0x', '0x', '0x', 1]; - return iface.encodeEventLog(eventFragment, args) as any; - }); - }); - - const result = await depositService.fetchEvents(startBlock, endBlock); - expect(result).toEqual( - expect.objectContaining({ - startBlock, - endBlock, - events: freshPubkeys.map((pubkey) => - expect.objectContaining({ pubkey }), - ), - }), - ); - expect(mockProviderCall).toBeCalledTimes(1); - }); - }); - - describe('updateEventsCache', () => { - const cachedPubkeys = ['0x1234', '0x5678']; - const cache = { - headers: { - startBlock: 0, - endBlock: 2, - }, - data: cachedPubkeys.map((pubkey) => ({ pubkey } as any)), - }; - const currentBlock = 1000; - const firstNotCachedBlock = cache.headers.endBlock + 1; - - beforeEach(async () => { - jest - .spyOn(depositService, 'getCachedEvents') - .mockImplementation(async () => ({ ...cache })); - - jest - .spyOn(providerService, 'getBlockNumber') - .mockImplementation(async () => currentBlock); - }); - - it('should collect events', async () => { - const mockFetchEventsFallOver = jest - .spyOn(depositService, 'fetchEventsFallOver') - .mockImplementation(async (startBlock, endBlock) => ({ - startBlock, - endBlock, - events: [], - })); - - jest - .spyOn(depositService, 'setCachedEvents') - .mockImplementation(async () => undefined); - - await depositService.updateEventsCache(); - - expect(mockFetchEventsFallOver).toBeCalledTimes(1); - const { calls: fetchCalls } = mockFetchEventsFallOver.mock; - expect(fetchCalls[0][0]).toBe(firstNotCachedBlock); - expect(fetchCalls[0][1]).toBeLessThan(currentBlock); - }); - - it('should save events to the cache', async () => { - jest - .spyOn(depositService, 'fetchEventsFallOver') - .mockImplementation(async (startBlock, endBlock) => ({ - startBlock, - endBlock, - events: [], - })); - - const mockSetCachedEvents = jest - .spyOn(cacheService, 'insertEventsCacheBatch') - .mockImplementation(async () => undefined); - - await depositService.updateEventsCache(); - - expect(mockSetCachedEvents).toBeCalledTimes(1); - const { calls: cacheCalls } = mockSetCachedEvents.mock; - expect(cacheCalls[0][0].headers.startBlock).toBe( - cache.headers.startBlock, - ); - expect(cacheCalls[0][0].headers.endBlock).toBeLessThan(currentBlock); - }); - }); - - describe('getAllDepositedEvents', () => { - const cachedPubkeys = ['0x1234', '0x5678']; - const freshPubkeys = ['0x4321', '0x8765']; - const cachedEvents = { - headers: { - startBlock: 0, - endBlock: 2, - }, - data: cachedPubkeys.map((pubkey) => ({ pubkey } as any)), - }; - const currentBlock = 10; - const currentBlockHash = '0x12'; - const firstNotCachedBlock = cachedEvents.headers.endBlock + 1; - - beforeEach(async () => { - jest - .spyOn(depositService, 'getCachedEvents') - .mockImplementation(async () => ({ ...cachedEvents })); - - jest - .spyOn(providerService, 'getBlockNumber') - .mockImplementation(async () => currentBlock); - }); - - it('should return cached events', async () => { - const tree = new DepositTree(); - - jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(DepositAbi__factory.abi); - return iface.encodeFunctionResult('get_deposit_root', [ - tree.getRoot(), - ]); - }); - const mockFetchEventsFallOver = jest - .spyOn(depositService, 'fetchEventsFallOver') - .mockImplementation(async () => ({ - startBlock: firstNotCachedBlock, - endBlock: currentBlock, - events: [], - })); - - const result = await depositService.getAllDepositedEvents( - currentBlock, - currentBlockHash, - ); - expect(result).toEqual({ - events: cachedEvents.data, - startBlock: cachedEvents.headers.startBlock, - endBlock: currentBlock, - checkRoot: expect.any(Function), - }); - - expect(mockFetchEventsFallOver).toBeCalledTimes(1); - expect(mockFetchEventsFallOver).toBeCalledWith( - firstNotCachedBlock, - currentBlock, - ); - }); - - it('should return merged pub keys', async () => { - const depositDataRoot = new Uint8Array([ - 185, 198, 196, 67, 108, 68, 92, 238, 17, 164, 72, 110, 30, 168, 28, - 57, 33, 93, 199, 57, 212, 165, 179, 74, 247, 55, 220, 97, 138, 135, - 59, 101, - ]); - - const events = freshPubkeys.map((pubkey) => ({ - pubkey, - depositDataRoot, - })); - - const mockFetchEventsFallOver = jest - .spyOn(depositService, 'fetchEventsFallOver') - .mockImplementation(async () => ({ - startBlock: firstNotCachedBlock, - endBlock: currentBlock, - events: events as any, - })); - - const tree = new DepositTree(); - events.map(({ depositDataRoot }) => { - tree.insertNode(depositDataRoot); - }); - - jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(DepositAbi__factory.abi); - return iface.encodeFunctionResult('get_deposit_root', [ - tree.getRoot(), - ]); - }); - - const result = await depositService.getAllDepositedEvents( - currentBlock, - currentBlockHash, - ); - - expect(result).toEqual({ - startBlock: cachedEvents.headers.startBlock, - endBlock: currentBlock, - events: cachedPubkeys - .map((pubkey) => ({ pubkey } as any)) - .concat( - freshPubkeys.map( - (pubkey) => ({ pubkey, depositDataRoot } as any), - ), - ), - checkRoot: expect.any(Function), - }); - - expect(mockFetchEventsFallOver).toBeCalledTimes(1); - expect(mockFetchEventsFallOver).toBeCalledWith( - firstNotCachedBlock, - currentBlock, - ); - }); - - it('should throw if event blockhash is different', async () => { - const anotherBlockHash = '0x34'; - - jest - .spyOn(depositService, 'fetchEventsFallOver') - .mockImplementation(async () => ({ - startBlock: firstNotCachedBlock, - endBlock: currentBlock, - events: freshPubkeys.map( - (pubkey) => - ({ - pubkey, - blockNumber: currentBlock, - blockHash: anotherBlockHash, - } as any), - ), - })); - - await expect( - depositService.getAllDepositedEvents(currentBlock, currentBlockHash), - ).rejects.toThrow(); - }); - }); - - describe('checkEventsBlockHash', () => { - const events = [ - { blockNumber: 1, blockHash: '0x1' }, - { blockNumber: 2, blockHash: '0x2' }, - ] as any; - - it('should throw if blockhash is different', async () => { - expect(() => { - depositService.checkEventsBlockHash(events, 2, '0x3'); - }).toThrow(); - }); - - it('should not throw if there are no events for the block', async () => { - expect(() => { - depositService.checkEventsBlockHash(events, 3, '0x3'); - }).not.toThrow(); - }); - - it('should not throw if blockhash is the same', async () => { - expect(() => { - depositService.checkEventsBlockHash(events, 2, '0x2'); - }).not.toThrow(); - }); - }); - - describe('getDepositRoot', () => { - it('should return deposit root', async () => { - const expected = '0x' + '0'.repeat(64); - - const mockProviderCall = jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(DepositAbi__factory.abi); - return iface.encodeFunctionResult('get_deposit_root', [expected]); - }); - - const result = await depositService.getDepositRoot(); - expect(result).toEqual(expected); - expect(mockProviderCall).toBeCalledTimes(1); - }); - }); - }); -}); diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts deleted file mode 100644 index fbad8ad8..00000000 --- a/src/contracts/deposit/deposit.service.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { performance } from 'perf_hooks'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { ProviderService } from 'provider'; -import { DepositEventEvent } from 'generated/DepositAbi'; -import { - DEPOSIT_EVENTS_STEP, - getDeploymentBlockByNetwork, - DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE, - DEPOSIT_EVENTS_CACHE_LAG_BLOCKS, -} from './deposit.constants'; -import { - DepositEvent, - VerifiedDepositEventsCache, - VerifiedDepositEventGroup, - VerifiedDepositedEventGroup, -} from './interfaces'; -import { RepositoryService } from 'contracts/repository'; -import { BlockTag } from 'provider'; -import { BlsService } from 'bls'; -import { DepositIntegrityCheckerService } from './integrity-checker'; -import { parseLittleEndian64 } from './deposit.utils'; -import { DepositTree } from './deposit-tree'; -import { LevelDBService } from './leveldb'; -import { DepositCacheIntegrityError } from './integrity-checker/constants'; - -@Injectable() -export class DepositService { - constructor( - @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, - private providerService: ProviderService, - private repositoryService: RepositoryService, - - private blsService: BlsService, - private depositIntegrityCheckerService: DepositIntegrityCheckerService, - private levelDBCacheService: LevelDBService, - ) {} - - public async handleNewBlock(blockNumber: number): Promise { - if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return; - - // The event cache is stored with an N block lag to avoid caching data from uncle blocks - // so we don't worry about blockHash here - const toBlockNumber = await this.updateEventsCache(); - await this.checkDepositCacheIntegrity(toBlockNumber); - } - - public async initialize(blockNumber: number) { - await this.levelDBCacheService.initialize(); - - const cachedEvents = await this.levelDBCacheService.getEventsCache(); - const isCacheValid = this.validateCache(cachedEvents, blockNumber); - - if (!isCacheValid) { - process.exit(1); - } - await this.depositIntegrityCheckerService.initialize(cachedEvents); - // it is necessary to load fresh events before integrity check - // because we can only compare roots of the last 128 blocks. - const toBlockNumber = await this.updateEventsCache(); - await this.checkDepositCacheIntegrity(toBlockNumber); - } - - public async checkDepositCacheIntegrity(toBlockNumber: number) { - try { - await this.depositIntegrityCheckerService.checkFinalizedRoot( - toBlockNumber, - ); - } catch (error) { - if (error instanceof DepositCacheIntegrityError) { - return this.logger.error( - `Deposit event cache integrity error on block number: ${toBlockNumber}`, - ); - } - throw error; - } - } - - /** - * Validates the app cache - * @param cachedEvents - cached events - * @param currentBlock - current block number - * @returns true if cache is valid - */ - public validateCache( - cachedEvents: VerifiedDepositEventsCache, - currentBlock: number, - ): boolean { - return this.validateCacheBlock(cachedEvents, currentBlock); - } - - /** - * Validates block number in the cache - * @param cachedEvents - cached events - * @param currentBlock - current block number - * @returns true if cached app version is the same - */ - public validateCacheBlock( - cachedEvents: VerifiedDepositEventsCache, - currentBlock: number, - ): boolean { - const isCacheValid = currentBlock >= cachedEvents.headers.endBlock; - - const blocks = { - cachedStartBlock: cachedEvents.headers.startBlock, - cachedEndBlock: cachedEvents.headers.endBlock, - currentBlock, - }; - - if (isCacheValid) { - this.logger.log('Deposit events cache has valid age', blocks); - } - - if (!isCacheValid) { - this.logger.warn( - 'Deposit events cache is newer than the current block', - blocks, - ); - } - - return isCacheValid; - } - - /** - * Returns only required information about the event, - * to reduce the size of the information stored in the cache - */ - public formatEvent(rawEvent: DepositEventEvent): DepositEvent { - const { - args, - transactionHash: tx, - blockNumber, - blockHash, - logIndex, - } = rawEvent; - const { - withdrawal_credentials: wc, - pubkey, - amount, - signature, - index, - ...rest - } = args; - - const depositCount = rest['4']; - - const depositDataRoot = DepositTree.formDepositNode({ - pubkey, - wc, - signature, - amount, - }); - - return { - pubkey, - wc, - amount, - signature, - tx, - blockNumber, - blockHash, - logIndex, - index, - depositCount: parseLittleEndian64(depositCount), - depositDataRoot, - }; - } - - /** - * Returns a block number when the deposited contract was deployed - * @returns block number - */ - public async getDeploymentBlockByNetwork(): Promise { - const chainId = await this.providerService.getChainId(); - return getDeploymentBlockByNetwork(chainId); - } - - /** - * Gets node operators data from cache - * @returns event group - */ - public async getCachedEvents(): Promise { - const { headers, ...rest } = - await this.levelDBCacheService.getEventsCache(); - const deploymentBlock = await this.getDeploymentBlockByNetwork(); - - return { - headers: { - ...headers, - startBlock: Math.max(headers.startBlock, deploymentBlock), - endBlock: Math.max(headers.endBlock, deploymentBlock), - }, - ...rest, - }; - } - - /** - * Saves deposited events to cache - */ - public async setCachedEvents( - cachedEvents: VerifiedDepositEventsCache, - ): Promise { - await this.levelDBCacheService.deleteCache(); - await this.levelDBCacheService.insertEventsCacheBatch({ - ...cachedEvents, - headers: { - ...cachedEvents.headers, - }, - }); - } - - /** - * Returns events in the block range - * If the request failed, it tries to repeat it or split it into two - * @param startBlock - start of the range - * @param endBlock - end of the range - * @returns event group - */ - public async fetchEventsFallOver( - startBlock: number, - endBlock: number, - ): Promise { - return await this.providerService.fetchEventsFallOver( - startBlock, - endBlock, - this.fetchEvents.bind(this), - ); - } - - /** - * Returns events in the block range - * @param startBlock - start of the range - * @param endBlock - end of the range - * @returns event group - */ - public async fetchEvents( - startBlock: number, - endBlock: number, - ): Promise { - const contract = await this.repositoryService.getCachedDepositContract(); - const filter = contract.filters.DepositEvent(); - const rawEvents = await contract.queryFilter(filter, startBlock, endBlock); - const events = rawEvents.map((rawEvent) => { - const formatted = this.formatEvent(rawEvent); - const valid = this.verifyDeposit(formatted); - return { valid, ...formatted }; - }); - - return { events, startBlock, endBlock }; - } - - /** - * Updates the cache deposited events - * The last N blocks are not stored, in order to avoid storing reorganized blocks - */ - public async updateEventsCache(): Promise { - const fetchTimeStart = performance.now(); - - const [currentBlock, initialCache] = await Promise.all([ - this.providerService.getBlockNumber(), - this.getCachedEvents(), - ]); - - const firstNotCachedBlock = initialCache.headers.endBlock + 1; - const toBlock = currentBlock - DEPOSIT_EVENTS_CACHE_LAG_BLOCKS; - - const totalEventsCount = initialCache.data.length; - let newEventsCount = 0; - - for ( - let block = firstNotCachedBlock; - block <= toBlock; - block += DEPOSIT_EVENTS_STEP - ) { - const chunkStartBlock = block; - const chunkToBlock = Math.min(toBlock, block + DEPOSIT_EVENTS_STEP - 1); - - const chunkEventGroup = await this.fetchEventsFallOver( - chunkStartBlock, - chunkToBlock, - ); - - await this.levelDBCacheService.insertEventsCacheBatch({ - headers: { - ...initialCache.headers, - endBlock: chunkEventGroup.endBlock, - }, - data: chunkEventGroup.events, - }); - - await this.depositIntegrityCheckerService.putFinalizedEvents( - chunkEventGroup.events, - ); - - newEventsCount += chunkEventGroup.events.length; - - this.logger.log('Historical events are fetched', { - toBlock, - startBlock: chunkStartBlock, - endBlock: chunkToBlock, - }); - } - - const fetchTimeEnd = performance.now(); - const fetchTime = Math.ceil(fetchTimeEnd - fetchTimeStart) / 1000; - // TODO: replace timer with metric - - this.logger.log('Deposit events cache is updated', { - newEventsCount, - totalEventsCount: totalEventsCount + newEventsCount, - fetchTime, - }); - - return toBlock; - } - - /** - * Returns all deposited events based on cache and fresh data - */ - public async getAllDepositedEvents( - blockNumber: number, - blockHash: string, - ): Promise { - const endBlock = blockNumber; - const cachedEvents = await this.getCachedEvents(); - - const isCacheValid = this.validateCacheBlock(cachedEvents, blockNumber); - if (!isCacheValid) process.exit(1); - - const firstNotCachedBlock = cachedEvents.headers.endBlock + 1; - const freshEventGroup = await this.fetchEventsFallOver( - firstNotCachedBlock, - endBlock, - ); - const freshEvents = freshEventGroup.events; - const lastEvent = freshEvents[freshEvents.length - 1]; - const lastEventBlockHash = lastEvent?.blockHash; - - this.checkEventsBlockHash(freshEvents, blockNumber, blockHash); - - this.logger.debug?.('Fresh deposit events are fetched', { - events: freshEvents.length, - startBlock: firstNotCachedBlock, - endBlock, - blockHash, - lastEventBlockHash, - }); - - const mergedEvents = cachedEvents.data.concat(freshEvents); - - return { - events: mergedEvents, - startBlock: cachedEvents.headers.startBlock, - endBlock, - // declare a separate method where we store the latest events in the closure - checkRoot: async () => { - await this.depositIntegrityCheckerService.checkLatestRoot( - blockNumber, - freshEvents, - ); - }, - }; - } - - /** - * Checks events block hash - * An additional check to avoid events processing in an alternate chain - */ - public checkEventsBlockHash( - events: DepositEvent[], - blockNumber: number, - blockHash: string, - ): void { - events.forEach((event) => { - if (event.blockNumber === blockNumber && event.blockHash !== blockHash) { - throw new Error( - 'Blockhash of the received events does not match the current blockhash', - ); - } - }); - } - - /** - * Returns a deposit root - */ - public async getDepositRoot(blockTag?: BlockTag): Promise { - const contract = await this.repositoryService.getCachedDepositContract(); - const depositRoot = await contract.get_deposit_root({ - blockTag: blockTag as any, - }); - - return depositRoot; - } - - /** - * Verifies a deposit signature - */ - public verifyDeposit(depositEvent: DepositEvent): boolean { - const { pubkey, wc, amount, signature } = depositEvent; - return this.blsService.verify({ pubkey, wc, amount, signature }); - } -} diff --git a/src/contracts/deposit/deposit.utils.ts b/src/contracts/deposit/deposit.utils.ts deleted file mode 100644 index 555b676a..00000000 --- a/src/contracts/deposit/deposit.utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -const changeEndianness = (string: any) => { - string = string.replace('0x', ''); - const result: string[] = []; - let len = string.length - 2; - while (len >= 0) { - result.push(string.substr(len, 2)); - len -= 2; - } - return '0x' + result.join(''); -}; - -export const parseLittleEndian64 = (str: string) => { - return parseInt(changeEndianness(str), 16); -}; - -export const toLittleEndian64 = (value: number): string => { - const buffer = Buffer.allocUnsafe(8); - buffer.writeBigUInt64LE(BigInt(value)); - return '0x' + buffer.toString('hex'); -}; diff --git a/src/contracts/deposit/index.ts b/src/contracts/deposit/index.ts deleted file mode 100644 index 753548e5..00000000 --- a/src/contracts/deposit/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './deposit.module'; -export * from './deposit.service'; -export * from './interfaces'; diff --git a/src/contracts/deposit/leveldb/index.ts b/src/contracts/deposit/leveldb/index.ts deleted file mode 100644 index eed6aa71..00000000 --- a/src/contracts/deposit/leveldb/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './leveldb.constants'; -export * from './leveldb.module'; -export * from './leveldb.service'; diff --git a/src/contracts/deposit/leveldb/leveldb.service.spec.ts b/src/contracts/deposit/leveldb/leveldb.service.spec.ts deleted file mode 100644 index 8af08776..00000000 --- a/src/contracts/deposit/leveldb/leveldb.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { MockProviderModule } from 'provider'; -import { ConfigModule } from 'common/config'; -import { LoggerModule } from 'common/logger'; -import { LevelDBModule } from './leveldb.module'; -import { LevelDBService } from './leveldb.service'; -import { cacheMock } from './leveldb.fixtures'; - -describe('dbService', () => { - const defaultCacheValue = { - headers: {}, - data: [] as any[], - }; - - let dbService: LevelDBService; - - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot(), - MockProviderModule.forRoot(), - LevelDBModule.register(defaultCacheValue, 'leveldb-spec'), - LoggerModule, - ], - }).compile(); - - dbService = moduleRef.get(LevelDBService); - await dbService.initialize(); - }); - - afterEach(async () => { - try { - await dbService.deleteCache(); - await dbService.close(); - } catch (error) {} - }); - - it('should return default cache', async () => { - const result = await dbService.getEventsCache(); - expect(result).toEqual(defaultCacheValue); - }); - - it('should return saved cache', async () => { - const expected = cacheMock; - - await dbService.insertEventsCacheBatch(expected); - const result = await dbService.getEventsCache(); - - expect(result).toEqual(expected); - }); -}); diff --git a/src/contracts/deposits-registry/crypto/containers.ts b/src/contracts/deposits-registry/crypto/containers.ts new file mode 100644 index 00000000..572bb858 --- /dev/null +++ b/src/contracts/deposits-registry/crypto/containers.ts @@ -0,0 +1 @@ +export { DepositData } from 'bls/bls.containers'; diff --git a/src/contracts/deposits-registry/crypto/index.ts b/src/contracts/deposits-registry/crypto/index.ts new file mode 100644 index 00000000..3400a40c --- /dev/null +++ b/src/contracts/deposits-registry/crypto/index.ts @@ -0,0 +1,3 @@ +export * from './containers'; +export * from './utils'; +export { toHexString } from '@chainsafe/ssz'; diff --git a/src/contracts/deposits-registry/crypto/utils.ts b/src/contracts/deposits-registry/crypto/utils.ts new file mode 100644 index 00000000..5c7505ed --- /dev/null +++ b/src/contracts/deposits-registry/crypto/utils.ts @@ -0,0 +1,16 @@ +import { fromHexString, toHexString } from '@chainsafe/ssz'; +import { UintNum64 } from 'bls/bls.constants'; +export { digest2Bytes32 } from '@chainsafe/as-sha256'; +export { fromHexString, toHexString }; + +export const parseLittleEndian64 = (str: string) => { + return UintNum64.deserialize(fromHexString(str)); +}; + +export const toLittleEndian64 = (value: number): string => { + return toHexString(UintNum64.serialize(value)); +}; + +export const toLittleEndian64BigInt = (value: bigint): string => { + return toHexString(UintNum64.serialize(Number(value))); +}; diff --git a/src/contracts/deposits-registry/deposits-registry.constants.ts b/src/contracts/deposits-registry/deposits-registry.constants.ts new file mode 100644 index 00000000..417a338f --- /dev/null +++ b/src/contracts/deposits-registry/deposits-registry.constants.ts @@ -0,0 +1,23 @@ +import { CHAINS } from '@lido-sdk/constants'; + +export const DEPLOYMENT_BLOCK_NETWORK: { + [key in CHAINS]?: number; +} = { + [CHAINS.Mainnet]: 11052984, + [CHAINS.Goerli]: 4367322, + [CHAINS.Holesky]: 0, +}; + +export const DEPOSIT_EVENTS_STEP = 10_000; + +export const DEPOSIT_CACHE_DEFAULT = Object.freeze({ + headers: { + startBlock: 0, + endBlock: 0, + }, + data: [], +}); + +export const DEPOSIT_REGISTRY_FINALIZED_TAG = Symbol.for( + 'DEPOSIT_REGISTRY_FINALIZED_TAG', +); diff --git a/src/contracts/deposits-registry/deposits-registry.module.ts b/src/contracts/deposits-registry/deposits-registry.module.ts new file mode 100644 index 00000000..f53689be --- /dev/null +++ b/src/contracts/deposits-registry/deposits-registry.module.ts @@ -0,0 +1,43 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { SecurityModule } from 'contracts/security'; +import { DepositsRegistryStoreModule } from './store'; +import { DepositRegistryService } from './deposits-registry.service'; +import { + DEPOSIT_CACHE_DEFAULT, + DEPOSIT_REGISTRY_FINALIZED_TAG, +} from './deposits-registry.constants'; +import { DepositsRegistryFetcherModule } from './fetcher'; +import { DepositRegistrySanityCheckerModule } from './sanity-checker'; + +@Module({}) +export class DepositsRegistryModule { + /** + * Registers the deposits module with a specific tag to handle block finality. + * The `finalizedTag` is primarily used to address issues with the Ganache handling of the 'finalized' tag, + * where it needs to be substituted with 'latest' for end-to-end tests. This tag is necessary only on a Ethereum node + * to avoid issues with blockchain reorganizations. + * In a production environment, this argument should either be empty or set to 'finalized'. + * + * @param {string} [finalizedTag='finalized'] - The tag to be used for identifying the status of blocks concerning finality. + * @returns {DynamicModule} - The dynamic module configuration for the Deposits Registry. + */ + static register(finalizedTag = 'finalized'): DynamicModule { + return { + module: DepositsRegistryModule, + imports: [ + SecurityModule, + DepositsRegistryFetcherModule, + DepositRegistrySanityCheckerModule, + DepositsRegistryStoreModule.register(DEPOSIT_CACHE_DEFAULT), + ], + providers: [ + DepositRegistryService, + { + provide: DEPOSIT_REGISTRY_FINALIZED_TAG, + useValue: finalizedTag, + }, + ], + exports: [DepositRegistryService], + }; + } +} diff --git a/src/contracts/deposits-registry/deposits-registry.service.ts b/src/contracts/deposits-registry/deposits-registry.service.ts new file mode 100644 index 00000000..66fa09c5 --- /dev/null +++ b/src/contracts/deposits-registry/deposits-registry.service.ts @@ -0,0 +1,255 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { performance } from 'perf_hooks'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { ProviderService } from 'provider'; +import { + DEPOSIT_EVENTS_STEP, + DEPOSIT_REGISTRY_FINALIZED_TAG, +} from './deposits-registry.constants'; +import { + VerifiedDepositEventsCache, + VerifiedDepositedEventGroup, + VerifiedDepositEvent, +} from './interfaces'; +import { RepositoryService } from 'contracts/repository'; +import { BlockTag } from 'provider'; +import { DepositsRegistryStoreService } from './store'; +import { DepositsRegistryFetcherService } from './fetcher/fetcher.service'; +import { DepositRegistrySanityCheckerService } from './sanity-checker/sanity-checker.service'; +import { toHexString } from './crypto'; + +@Injectable() +export class DepositRegistryService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + private providerService: ProviderService, + private repositoryService: RepositoryService, + + private sanityChecker: DepositRegistrySanityCheckerService, + private fetcher: DepositsRegistryFetcherService, + private store: DepositsRegistryStoreService, + + @Inject(DEPOSIT_REGISTRY_FINALIZED_TAG) private finalizedTag: string, + ) {} + + public async handleNewBlock(): Promise { + await this.updateEventsCache(); + } + + public async initialize() { + await this.store.initialize(); + const cachedEvents = await this.store.getEventsCache(); + await this.sanityChecker.initialize(cachedEvents); + + await this.updateEventsCache(); + } + + /** + * Gets node operators data from cache + * @returns event group + */ + public async getCachedEvents(): Promise { + const { headers, ...rest } = await this.store.getEventsCache(); + const deploymentBlock = await this.fetcher.getDeploymentBlockByNetwork(); + + return { + headers: { + ...headers, + startBlock: Math.max(headers.startBlock, deploymentBlock), + endBlock: Math.max(headers.endBlock, deploymentBlock), + }, + ...rest, + }; + } + + /** + * Updates the cache deposited events + * The last N blocks are not stored, in order to avoid storing reorganized blocks + */ + public async updateEventsCache(): Promise { + const fetchTimeStart = performance.now(); + + const [finalizedBlock, initialCache] = await Promise.all([ + this.providerService.getBlock(this.finalizedTag), + this.getCachedEvents(), + ]); + + const { number: finalizedBlockNumber, hash: finalizedBlockHash } = + finalizedBlock; + const firstNotCachedBlock = initialCache.headers.endBlock + 1; + + const totalEventsCount = initialCache.data.length; + let newEventsCount = 0; + + // check that the cache is written to a block less than or equal to the current block + // otherwise we consider that the Ethereum node has started sending incorrect data + const isCacheValid = this.sanityChecker.verifyCacheBlock( + initialCache, + finalizedBlockNumber, + ); + + if (!isCacheValid) return; + + let lastIndexedEvent: VerifiedDepositEvent | undefined = undefined; + + for ( + let block = firstNotCachedBlock; + block <= finalizedBlockNumber; + block += DEPOSIT_EVENTS_STEP + ) { + const chunkStartBlock = block; + const chunkToBlock = Math.min( + finalizedBlockNumber, + block + DEPOSIT_EVENTS_STEP - 1, + ); + + const chunkEventGroup = await this.fetcher.fetchEventsFallOver( + chunkStartBlock, + chunkToBlock, + ); + + await this.sanityChecker.addEventGroupToIndex( + chunkStartBlock, + chunkToBlock, + chunkEventGroup.events, + ); + + // Even if the cache is not valid we can't help but write it down + // because the delay in updating the cache will eventually cause + // the getAllDepositedEvents method to take a very long time to process, as changes + // will be accumulated and not processed. + await this.store.insertEventsCacheBatch({ + headers: { + ...initialCache.headers, + endBlock: chunkEventGroup.endBlock, + }, + data: chunkEventGroup.events, + }); + + newEventsCount += chunkEventGroup.events.length; + + const lastEventFromGroup = + chunkEventGroup.events[chunkEventGroup.events.length - 1]; + + if (lastEventFromGroup) lastIndexedEvent = lastEventFromGroup; + + this.logger.log('Historical events are fetched', { + finalizedBlockNumber, + startBlock: chunkStartBlock, + endBlock: chunkToBlock, + }); + } + + const fetchTimeEnd = performance.now(); + const fetchTime = Math.ceil(fetchTimeEnd - fetchTimeStart) / 1000; + + const isRootValid = await this.sanityChecker.verifyUpdatedEvents( + finalizedBlockHash, + ); + + // Store the last event from the list of updated events separately + // Unfortunately, we cannot validate each event individually upon insertion + // because this would require an archival node + if (isRootValid && lastIndexedEvent) { + await this.store.insertLastValidEvent(lastIndexedEvent); + } + + if (!isRootValid) { + this.logger.error('Integrity check failed on block', { + finalizedBlock, + finalizedBlockHash, + }); + + // Delete invalid cache only after full synchronization due to: + // - we cannot check root at arbitrary times, only if the backlog is less than 120 blocks + await this.store.clearFromLastValidEvent(); + } + + this.logger.log('Deposit events cache is updated', { + newEventsCount, + totalEventsCount: totalEventsCount + newEventsCount, + fetchTime, + }); + } + + /** + * Returns all deposited events based on cache and fresh data + */ + public async getAllDepositedEvents( + blockNumber: number, + blockHash: string, + ): Promise { + const endBlock = blockNumber; + const cachedEvents = await this.getCachedEvents(); + + const isCacheValid = this.sanityChecker.verifyCacheBlock( + cachedEvents, + blockNumber, + ); + + if (!isCacheValid) { + throw new Error( + `Deposit events cache is newer than the current block ${blockNumber}`, + ); + } + + const firstNotCachedBlock = cachedEvents.headers.endBlock + 1; + const freshEventGroup = await this.fetcher.fetchEventsFallOver( + firstNotCachedBlock, + endBlock, + ); + const freshEvents = freshEventGroup.events; + const lastEvent = freshEvents[freshEvents.length - 1]; + const lastEventBlockHash = lastEvent?.blockHash; + + const isValid = await this.sanityChecker.verifyFreshEvents( + blockHash, + freshEvents, + ); + + if (!isValid) { + const { lastValidEvent } = cachedEvents; + this.logger.warn('Integrity check failed on block', { + currentBlockNumber: blockNumber, + currentBlockHash: blockHash, + lastValidBlockNumber: lastValidEvent?.blockNumber, + lastValidBlockHash: lastValidEvent?.blockHash, + lastValidEventIndex: lastValidEvent?.index, + lastValidEventDepositDataRoot: lastValidEvent?.depositDataRoot + ? toHexString(lastValidEvent?.depositDataRoot) + : '', + lastValidEventDepositCount: lastValidEvent?.depositCount, + }); + + throw new Error(`Integrity check failed on block ${blockNumber}`); + } + + this.logger.debug?.('Fresh deposit events are fetched', { + events: freshEvents.length, + startBlock: firstNotCachedBlock, + endBlock, + blockHash, + lastEventBlockHash, + }); + + const mergedEvents = cachedEvents.data.concat(freshEvents); + + return { + events: mergedEvents, + startBlock: cachedEvents.headers.startBlock, + endBlock, + }; + } + + /** + * Returns a deposit root + */ + public async getDepositRoot(blockTag?: BlockTag): Promise { + const contract = await this.repositoryService.getCachedDepositContract(); + const depositRoot = await contract.get_deposit_root({ + blockTag: blockTag as any, + }); + + return depositRoot; + } +} diff --git a/src/contracts/deposits-registry/fetcher/fetcher.module.ts b/src/contracts/deposits-registry/fetcher/fetcher.module.ts new file mode 100644 index 00000000..88fced62 --- /dev/null +++ b/src/contracts/deposits-registry/fetcher/fetcher.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BlsModule } from 'bls'; +import { DepositsRegistryFetcherService } from './fetcher.service'; + +@Module({ + imports: [BlsModule], + providers: [DepositsRegistryFetcherService], + exports: [DepositsRegistryFetcherService], +}) +export class DepositsRegistryFetcherModule {} diff --git a/src/contracts/deposits-registry/fetcher/fetcher.service.ts b/src/contracts/deposits-registry/fetcher/fetcher.service.ts new file mode 100644 index 00000000..1c7ce144 --- /dev/null +++ b/src/contracts/deposits-registry/fetcher/fetcher.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@nestjs/common'; +import { BlsService } from 'bls'; +import { RepositoryService } from 'contracts/repository'; +import { DepositEventEvent } from 'generated/DepositAbi'; + +import { ProviderService } from 'provider'; +import { parseLittleEndian64 } from '../crypto'; +import { DEPLOYMENT_BLOCK_NETWORK } from '../deposits-registry.constants'; +import { DepositEvent, VerifiedDepositEventGroup } from '../interfaces'; +import { DepositTree } from '../sanity-checker/integrity-checker/deposit-tree'; + +@Injectable() +export class DepositsRegistryFetcherService { + constructor( + private providerService: ProviderService, + private repositoryService: RepositoryService, + private blsService: BlsService, + ) {} + + /** + * Returns events in the block range and verify signature + * If the request failed, it tries to repeat it or split it into two + * @param startBlock - start of the range + * @param endBlock - end of the range + * @returns event group + */ + public async fetchEventsFallOver( + startBlock: number, + endBlock: number, + ): Promise { + return await this.providerService.fetchEventsFallOver( + startBlock, + endBlock, + this.fetchEvents.bind(this), + ); + } + + /** + * Returns events in the block range and verify signature + * @param startBlock - start of the range + * @param endBlock - end of the range + * @returns event group + */ + public async fetchEvents( + startBlock: number, + endBlock: number, + ): Promise { + const contract = await this.repositoryService.getCachedDepositContract(); + const filter = contract.filters.DepositEvent(); + const rawEvents = await contract.queryFilter(filter, startBlock, endBlock); + const events = rawEvents.map((rawEvent) => { + const formatted = this.formatEvent(rawEvent); + const valid = this.verifyDeposit(formatted); + return { valid, ...formatted }; + }); + + return { events, startBlock, endBlock }; + } + + /** + * Returns only required information about the event, + * to reduce the size of the information stored in the cache + */ + public formatEvent(rawEvent: DepositEventEvent): DepositEvent { + const { + args, + transactionHash: tx, + blockNumber, + blockHash, + logIndex, + } = rawEvent; + const { + withdrawal_credentials: wc, + pubkey, + amount, + signature, + index, + ...rest + } = args; + + const depositCount = rest['4']; + + const depositDataRoot = DepositTree.formDepositNode({ + pubkey, + wc, + signature, + amount, + }); + + return { + pubkey, + wc, + amount, + signature, + tx, + blockNumber, + blockHash, + logIndex, + index, + depositCount: parseLittleEndian64(depositCount), + depositDataRoot, + }; + } + + /** + * Verifies a deposit signature + */ + public verifyDeposit(depositEvent: DepositEvent): boolean { + const { pubkey, wc, amount, signature } = depositEvent; + return this.blsService.verify({ pubkey, wc, amount, signature }); + } + + /** + * Returns a block number when the deposited contract was deployed + * @returns block number + */ + public async getDeploymentBlockByNetwork(): Promise { + const chainId = await this.providerService.getChainId(); + const address = DEPLOYMENT_BLOCK_NETWORK[chainId]; + if (address == null) throw new Error(`Chain ${chainId} is not supported`); + + return address; + } +} diff --git a/src/contracts/deposits-registry/fetcher/index.ts b/src/contracts/deposits-registry/fetcher/index.ts new file mode 100644 index 00000000..128136bc --- /dev/null +++ b/src/contracts/deposits-registry/fetcher/index.ts @@ -0,0 +1,2 @@ +export * from './fetcher.module'; +export * from './fetcher.service'; diff --git a/src/contracts/deposits-registry/index.ts b/src/contracts/deposits-registry/index.ts new file mode 100644 index 00000000..e573762e --- /dev/null +++ b/src/contracts/deposits-registry/index.ts @@ -0,0 +1,3 @@ +export * from './deposits-registry.module'; +export * from './deposits-registry.service'; +export * from './interfaces'; diff --git a/src/contracts/deposit/interfaces/cache.interface.ts b/src/contracts/deposits-registry/interfaces/cache.interface.ts similarity index 87% rename from src/contracts/deposit/interfaces/cache.interface.ts rename to src/contracts/deposits-registry/interfaces/cache.interface.ts index b42b7fcb..a421da53 100644 --- a/src/contracts/deposit/interfaces/cache.interface.ts +++ b/src/contracts/deposits-registry/interfaces/cache.interface.ts @@ -8,4 +8,5 @@ export interface VerifiedDepositEventsCacheHeaders { export interface VerifiedDepositEventsCache { headers: VerifiedDepositEventsCacheHeaders; data: VerifiedDepositEvent[]; + lastValidEvent?: VerifiedDepositEvent; } diff --git a/src/contracts/deposit/interfaces/deposit-tree.interface.ts b/src/contracts/deposits-registry/interfaces/deposit-tree.interface.ts similarity index 100% rename from src/contracts/deposit/interfaces/deposit-tree.interface.ts rename to src/contracts/deposits-registry/interfaces/deposit-tree.interface.ts diff --git a/src/contracts/deposit/interfaces/event.interface.ts b/src/contracts/deposits-registry/interfaces/event.interface.ts similarity index 83% rename from src/contracts/deposit/interfaces/event.interface.ts rename to src/contracts/deposits-registry/interfaces/event.interface.ts index 45da4f73..470d453f 100644 --- a/src/contracts/deposit/interfaces/event.interface.ts +++ b/src/contracts/deposits-registry/interfaces/event.interface.ts @@ -26,6 +26,5 @@ export interface VerifiedDepositEventGroup extends DepositEventGroup { events: VerifiedDepositEvent[]; } -export interface VerifiedDepositedEventGroup extends VerifiedDepositEventGroup { - checkRoot(): Promise; -} +export interface VerifiedDepositedEventGroup + extends VerifiedDepositEventGroup {} diff --git a/src/contracts/deposit/interfaces/index.ts b/src/contracts/deposits-registry/interfaces/index.ts similarity index 100% rename from src/contracts/deposit/interfaces/index.ts rename to src/contracts/deposits-registry/interfaces/index.ts diff --git a/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.module.ts b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.module.ts new file mode 100644 index 00000000..5bc33ab5 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { BlockchainCheckerService } from './blockchain-checker.service'; + +@Module({ + providers: [BlockchainCheckerService], + exports: [BlockchainCheckerService], +}) +export class BlockchainCheckerModule {} diff --git a/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.service.ts b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.service.ts new file mode 100644 index 00000000..5f2d4706 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/blockchain-checker.service.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { DepositEvent, VerifiedDepositEventsCache } from '../../interfaces'; + +@Injectable() +export class BlockchainCheckerService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + ) {} + + /** + * Validates block number in the cache + * @param cachedEvents - cached events + * @param currentBlock - current block number + * @returns true if cached app version is the same + */ + public validateCacheBlock( + cachedEvents: VerifiedDepositEventsCache, + currentBlock: number, + ): boolean { + const isCacheValid = currentBlock >= cachedEvents.headers.endBlock; + + return isCacheValid; + } + + /** + * Checks events block hash + * An additional check to avoid events processing in an alternate chain + */ + public findReorganizedEvent( + events: DepositEvent[], + blockNumber: number, + blockHash: string, + ): DepositEvent | null { + return ( + events.find( + (event) => + event.blockNumber === blockNumber && event.blockHash !== blockHash, + ) || null + ); + } +} diff --git a/src/contracts/deposits-registry/sanity-checker/blockchain-checker/index.ts b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/index.ts new file mode 100644 index 00000000..30d286a5 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/blockchain-checker/index.ts @@ -0,0 +1,2 @@ +export * from './blockchain-checker.module'; +export * from './blockchain-checker.service'; diff --git a/src/contracts/deposits-registry/sanity-checker/index.ts b/src/contracts/deposits-registry/sanity-checker/index.ts new file mode 100644 index 00000000..3af948e6 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/index.ts @@ -0,0 +1,4 @@ +export * from './sanity-checker.module'; +export * from './sanity-checker.service'; + +export * from './integrity-checker'; diff --git a/src/contracts/deposit/integrity-checker/constants.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/constants.ts similarity index 100% rename from src/contracts/deposit/integrity-checker/constants.ts rename to src/contracts/deposits-registry/sanity-checker/integrity-checker/constants.ts diff --git a/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.fixture.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.fixture.ts new file mode 100644 index 00000000..32ebec88 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.fixture.ts @@ -0,0 +1,1160 @@ +export const dataTransformFixtures = [ + { + valid: true, + pubkey: + '0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95', + wc: '0x00f50428677c60f997aadeab24aabf7fceaef491c96a52b463ae91f95611cf71', + amount: '0x00ca9a3b00000000', + signature: + '0xa29d01cc8c6296a8150e515b5995390ef841dc18948aa3e79be6d7c1851b4cbb5d6ff49fa70b9c782399506a22a85193151b9b691245cebafd2063012443c1324b6c36debaedefb7b2d71b0503ffdc00150aaffd42e63358238ec888901738b8', + tx: '0x7085c586686d666e8bb6e9477a0f0b09565b2060a11f1c4209d3a52295033832', + blockNumber: 11185311, + blockHash: + '0x1ecb9dd23676c9201af1e8026e7d83a1f979b8abd381064fba0b593fcff7b235', + logIndex: 131, + index: '0x0000000000000000', + depositCount: 0, + depositDataRoot: + '0xaa4a8d0b7d9077248630f1a4701ae9764e42271d7f22b7838778411857fd349e', + }, + { + valid: true, + pubkey: + '0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c', + wc: '0x0092c20062cee70389f1cb4fa566a2be5e2319ff43965db26dbaa3ce90b9df99', + amount: '0x00ca9a3b00000000', + signature: + '0x985f365b3459176da437560337cc074d153663f65e3c6bab28197e34cd7f926fa940176ba43484fb5297f679bc869f5d10ee62f64a119d756182005fbb28046c0541f627b430cabfeb3599ebaa1b8efd08de562ec03a8d78c2f9e1b6f01d8aba', + tx: '0xa90ed27521c07e66d52db6ee47d729d118229925303706b35e4d36d8e830ba7a', + blockNumber: 11191448, + blockHash: + '0xa51cbe797cac4ef0297862576e64444a90d3bec332949c352f253405aa129f1e', + logIndex: 126, + index: '0x0100000000000000', + depositCount: 1, + depositDataRoot: + '0x76fffc948646005fce32e27555238dfe801c9e7eea28ff40dbe2afe8f83cf0c6', + }, + { + valid: true, + pubkey: + '0xb2ff4716ed345b05dd1dfc6a5a9fa70856d8c75dcc9e881dd2f766d5f891326f0d10e96f3a444ce6c912b69c22c6754d', + wc: '0x00d66cf353931500a54cbd0bc59cbaac6690cb0932f42dc8afeddc88feeaad6f', + amount: '0x00ca9a3b00000000', + signature: + '0xb868229df29f2b48409c5aac70594c9882be4a7b1e60ba1a9c985f87f4a9cad18bbf74a78734cd9b4911b57a23dc9d4118b70da8e2ae1faaab91c04076d66ead359a0be26845410d18a42910bdf0b9ae4b4bfcc90f8bb528f1a92c91a1ad6547', + tx: '0x14f1d17ef6051109bf4b9e5dd9b494f12580a508a8d412af6d5e857f8d6a0f0b', + blockNumber: 11191495, + blockHash: + '0x37c7097adfd4e30c93b8840d32c215e189d95200a9ef1e3445b926efb48ae99f', + logIndex: 76, + index: '0x0200000000000000', + depositCount: 2, + depositDataRoot: + '0x3e74b357fbf0bf36bed50de7ee3a3caaa11006e7ea5ce644d16de8d666b2c7a9', + }, + { + valid: true, + pubkey: + '0x8e323fd501233cd4d1b9d63d74076a38de50f2f584b001a5ac2412e4e46adb26d2fb2a6041e7e8c57cd4df0916729219', + wc: '0x00d6b91fbbce0146739afb0f541d6c21e8c41e92b97874828f402597bf530ce4', + amount: '0x00ca9a3b00000000', + signature: + '0xb9a4bccc6fc91192b603dd7ee1c99eabee415bdde9d96146c71b2ce4ce9e292ded93fa150850242c327e6ce2f50cb75b134afe5bb7ecca9c328e6f2dc1da931389a2d15d435eaed1222991d22aeecc026b2390afa5f941d2ed5277b3d3fbc350', + tx: '0x6e1e30cb4b6e0029fc4762cf74b264ce66a9d078a0f732583a71544fdadddf72', + blockNumber: 11191501, + blockHash: + '0xa6d7b9926b794b8de798720f058badbf436ef52907b19ebd090172b75d24ed20', + logIndex: 328, + index: '0x0300000000000000', + depositCount: 3, + depositDataRoot: + '0x790284c0a36abd53ec0ce9284f4ad4af72c891a38456e73e15685bb99dfc09e9', + }, +]; + +export const depositDataRootsFixture10k = { + events: [ + '0xaa4a8d0b7d9077248630f1a4701ae9764e42271d7f22b7838778411857fd349e', + '0x76fffc948646005fce32e27555238dfe801c9e7eea28ff40dbe2afe8f83cf0c6', + '0x3e74b357fbf0bf36bed50de7ee3a3caaa11006e7ea5ce644d16de8d666b2c7a9', + '0x790284c0a36abd53ec0ce9284f4ad4af72c891a38456e73e15685bb99dfc09e9', + '0x0fd0bdf2cee28566c006a622cf5a1763de51233faa3a4bf7d95fed76c9a5fd05', + '0xdbd986dc85ceb382708cf90a3500f500f0a393c5ece76963ac3ed72eccd2c301', + '0x0cf210b6d6dcf3581ab34266ae25e2b0e9b20c50581b004dea1080dd0939a97d', + '0x227f7f4f8a112532572bc53ae300d06e7aed9b3ea531a36221629af6783cb1f8', + '0xf963d928e334ef682fa5cf0e651988ea32fddada361f09d04e951b952038dc74', + '0xaacbc03b795b8aec28b10500d3410dd7c5aa4031b083b3d20388e52fdacc7f6e', + '0x01a9de4c3f8775c14384c405745bc6bb34ef87cb9bf8275201f19a28b3f99aa2', + '0x64dc8d9e3cec6a41a42ea105bb38b59d7ca49b14d6c7e678cba309fbfb0e7af4', + '0xbbabd378476db7f6f0520a943b2e6186c305610601ca59fa11212eacdc2ef317', + '0x685ed3e1f3d46604214a101cd5825d55e0df8250af7442403f4ebcfc15be6e99', + '0x6faa51b25ff2f0a1297feec95e1d95e6041c3b2270b55fd46d52e09557bac5fc', + '0x6292c84fb0b7f5bd88fbe9ba7ec38bf589ae2811a8503a6c60cd76f52b63586f', + '0xbb88366a75271d570e88f940fc7de86f119575b755fea171867b38c9e9b4e1df', + '0xaa28011f9bbab5d924e249e9516bdbdc48aa80110e9f59b09b74373c91866002', + '0x4d1ce04b3aebac268eb9c329c8c96e3da8ba133e73dac32f6e14423e1df4f9fe', + '0x291684946eb577357efd7d119f05addc928a780961f3cab307dceb9ed346f26f', + '0x5c094a65e756690096e53f90c262fd88b2ed3bd7b2a1ec943df138d9b6794b7a', + '0xebe8ec1d0329d2bb285efcc157df907d48d0c56eadcd7e2be7e5a71d6d4f59a8', + '0x8053b81185dd809b973c71f54df69c6e82eef774e43f8b12c9a113a4374118bb', + '0xa9ed7ddf21134290767102defa883b87861c891f5d4a8d2d78632a791116258f', + '0x15afffabc64e3eb7b6833494bd38808a2c3600f8c40ecbe4500d1602f6330552', + '0x8f2e40a9507594979456ab30d150905cd85c585e83bfc57ad2b7d301dccfdec4', + '0x8abe605ff94d099346225b998a4986271d0445efd2329ced0db45d0ca3228fcc', + '0x31d3bf344ecfb44fc014abd2f86340f47263fc59bc11b20783cd1e4456a0f177', + '0x27a4028494edee3471bf6d6681db3558f468d347b1be12899e0416f011f1caaa', + '0x240389c323a9830f1c4354d32e0b5a319edfcd8f786d8a709e6c605206b64d27', + '0x2202f5507ec3a809d26ad969b4bae1a34d21574de375347c1749b926e223b3a0', + '0x3f51ee386cc183b1e43da81c5aee14e817496f3cca30bac5a896e58578b71f77', + '0x773bc477c150bcd733d4e8413d682e0d411fa0f68605cf8b86108efc9040b8f6', + '0xa9d57ad371b3889f5f50c241f5e8de1d2f0f053dbd3011934266670989e79133', + '0x5c2ee552a2907fcef809b9b34045644bf635d72ac6f579c8ea8f17e2545e0447', + '0x8b212b94494bdd11fee558807ea2365048d3a2d965e5801f5f6bf29ed385f674', + '0xb468dd1ee4bac68b8dd6980fb806fd6fc158acac1b267dda9383df724776e32d', + '0x1f50cda24d72ee068b4683862919f1b30f15fce6e3bc31f0af8651b9cd59f9a2', + '0x9bc78e7b0c29ec858370dcd9604e7c4a118f8647770f0be8d92ac0a044897b63', + '0x256287af00e8ab72e0947bcea414427154f9d72062d1079c7c6fbb1a8be97af1', + '0xdc53ebcd02d9a355be29b66821abc916e720883e1024b5adf62b4219d5480b44', + '0x7a6e5ae6f425fb538ae763a6900285f6433a476d2ace3d4a50dda1b5be55041a', + '0x0461628cdc499ae9d52b45fa6df2c64fcaf3c70a13142b66e6e9fbfb3374746e', + '0xc148821c28b65063d3863505de5efd14dbae01200f4dcd04e732f579e657d9df', + '0xcdbe7bff81fb547017918e5634fe40d3b35ee91b32831dab8ef8ac99d57bdbf0', + '0xd8d4dd4db26e35cd1a00c899c7286bc3a883427a475aac6365139bca33e04ffa', + '0x14c289d0754ebccc4642bbd494fee4624323febb7573b4589ba300d45616c0d5', + '0xbfd2bd584723243047c5aaabf613b0a52bcbab481983d600a57addde642227a9', + '0xf98dce5a5e98ad51843f922b26b685de87f9041277a4c6a8a12756bac2ae1e80', + '0x3ed5bb4a73e8485df93d3588bf91a5408bd3ba635167656d360077d12f861e31', + '0xabdc41954cc9c783d64acdcf610322ccd29cb2288d1544dd31b9a6e72c370deb', + '0x844b44d5a0738358a3938646881b563a8068aaeb7e10c97ae7a1251147039a9a', + '0x9b9cf0518542594e9d5fab0e65494bf6e62186c3a94da4e19b89b8a4d5f8eaa1', + '0x8a7192e917fdb883bc7bb3c20077c2e211e18c9ef27cfa8016763a90b0fc9056', + '0x68656891f8d5517f59f8b520819440a2e4a6519c3168a18218307d60fd2d1d31', + '0xf4bc40e60d41266effe10f3c128e7ec2745cc18d190be8ec0cadb1ab461bb6f3', + '0xaa2848f1960fd195f924cd96dbe0f490dc7d184010d7dc970e3f54c8291da5dd', + '0x5c351787db91f668b3c70801cdb8d3087a4469ad88235f02fa74ef880341028f', + '0xda17919d50092ae60e1e2df4a5f5537ed3d963be358ef6b56af9f1080f588ad5', + '0xb05465f51be5137b84e1d04d2f2ef9bf7dbd82542c70bc562fcbbdfdd362d96a', + '0x48b36388ae89979c433535573f3f9f347067cc159338d33434a2f4ca65c74bb1', + '0xf4129e092304d083ca7f31296aa296768f6dc51d85ed5f96278fb5be48ef2296', + '0xe622aefd42e8e3c70858da6eb29500bd1135f5670874140f308f435b454c015c', + '0x3d725b0ee40b0563332a6e09baa49ce777c90abe5404f77ada2429620f644e16', + '0xb8f3b35acd787284e4214eb1284517f197266733c0c5ea3baf6cf4aeb04be8a9', + '0x946424faab4e5c047ae9870236b641a73c8e1f42346d59e38cbb77f9f88dd6eb', + '0xac90de872f1f092a8258c35a2440c776c840e4ac0d8eed6a39920fe19a48d60d', + '0x9970cc53fef13e734266a0a00d43b8349addc442ef08b122c54e85c6efaec64d', + '0xbd94fea0a4c628a72474210bd0c01a89c2c9470a429e491190ecedbaeff03ea6', + '0xbe74b4ba0f91af15365258bf2384b2c1ec897c80aee856d40fb97b1057a3f9b0', + '0x53978685ca0b3a5123326756578ddd00b933cac87dc515b25994bbd0b6b11af2', + '0x5f9a348196532d8a0486f7a082071498016d2d1c90c6783fca22020265a3ceca', + '0x7ad296df98be10af7deeb1567847bea529f879829a7029d5d84084cb2ef06395', + '0xe6ac2082359d3cad3217b80b9d004236b64b78e0a9d6fbcd204b56dba360c003', + '0x075f089a2d8069c0062275ab8b20093dcb7a63577f0d367dfe3e5093126336e5', + '0x79cfc5cc08541d85ebe21cfa8d367aff7b4da0bbec3b48fb325be60f7c0cd4a3', + '0x7294fb60e3bcc9e719fd4e7822bb933702d65c53dfde1537e92e28d285bbdc25', + '0xb3e82cfa34dc502d28d412a59ea7e42bb96c2a24c1df16a414ebded848f0c5cc', + '0xc1fc69a88517e0585887f234b417c775c526da2af7956cdf9c81285e94d8797d', + '0xf9e7ddb3b78d0b71f829c3f56f99f6ab45232bce37b0f4a8350c58b55bf85c10', + '0xb6e1cca9693ac9488c6914b8d698fd490958c687fe6d047faa0e5fd76440da1c', + '0x8c880663bc3f919b0504a6a151d04e78f607532fafadfbcfc2388a6aab3fb717', + '0x22ae224e105d7a62afb5c3203975acd9d6e94aab2d1f0b5bbefe7ee6d57453d5', + '0xc938e0775050b42a324ec832cb945cb1a9782ef58295763533cf8fa89c0d8ab0', + '0x580abe3ff2494d245ff23c27483651b9cfdc51c011347d6dc34abfdff52ce03c', + '0x5242278356feddc272521712273f8083409a915cb40721e2690e14759a3ad20a', + '0x12cceef0598e018b4192f160c2f2175b67adeeffbfdf52a21fa646c6c0c9d24a', + '0xa18a1d47c2229c441ab2ff24d78a86b0d966ad8a2e92cbe3544773f032a295a2', + '0x5e97cd8b6401d254e100199bc523c41c2f313a6980cf33c3b86d195c71e68054', + '0xef3cb1ec905ff3c537f34be3008b3e98100c938bf9fe3f79a4854cbd90b2ff03', + '0x169675c35ffbacf44fd3c994a9d18452914cd8fdee7a7910386dc3505b9f1c5a', + '0x4c525e191c3a67aa3ece110b5571d9d6900c277867026379f9cdf8ba6ebe1024', + '0x806040ae46bc27b9f48ab2cd660e0f7c44a905d3f05a3b9f002c0766c354f876', + '0x1ed2be894a6bc85a74682c0c4045a15fa9b3e1671c30e67140de2fbb9fec4151', + '0x01de4f2f53213f326d7c774a6b3a26f3a5efcddb515053a530286632f7393806', + '0x60f5591e1459df0ac8b2496e92dd69bc3ef8a39ee00b49ccaf0fc7d725da9339', + '0x944be7472306ef5ac4f3ad63387ed1f56f586882fd1394ca88f8057b648d305e', + '0xf4aca036b84864b07d27d7445105613b616e321d815aa25a0083db7c7446881a', + '0xb4caeb6fcf9ac5694e8757353b5aa80f2e9b7c42968e7623f2eca1f41f97a343', + '0x082140ea627495e3a842fc7a047695ec237a332f5c937720ab852f01c33f5ff7', + '0xc04efa5c6944e4f27370a8afce7b56a61309495413139bc6e128c68b961386b4', + '0x7ee14aa819f0819a6ef4d22a57bd59c2322662c1f794fa125aea9b9256d48d9e', + '0x6101eb5459ee526c2195322882ea69b74685daa7c7f9d66d85489daf0303079f', + '0xd2ca717681d06630fc60b68ae31eb416e1afb5a70e1d45713247804bec96b198', + '0x473e6abec3c5945505958388bfc8202ed5e1dd9e52f0ab6489d595583c7ecca2', + '0x953ebdb1b2dbc473562b8739c203c49d692531bdc8fe57aba3e0b4cb42c2844e', + '0xb89f3d6cce3046b103b19cd5e023473391e76e61fbafc44b13a32de8c037f69b', + '0x50aebcd7137f166fa59a25bb0ba68fdb08a8c64d858b1106a6f67465dca8b49a', + '0xbba6adce21ae1d20d22e0dbc52127834756c571f0feb23748709f3336e299a04', + '0x28ea7221dfa4fc014023912b3a41db2d3a44be136872bb13eea86b28f4965605', + '0x49250acba166e802e2a4d2d11d1d7222d653f93d664e70d6f6bd6bcc88fff8c8', + '0x4a9f15b0b3c2eb034f4decf41ec6c64b6a4881fe8a637cbcb82e800e9dc32278', + '0x4160268dc31fb5eb3f522e6928c99f2ad1c9f920c5649001ca294aff3a109f3f', + '0xd55140cb0c8bed4ffb4170390bf2b3a1198b0a8cf69148d0c8bed0c3a5a440f5', + '0x06500da7b85c17a7d168fae170dbb70d962fbf587fc6cf79c160db453eed4afb', + '0x1ccd8fb070400f4ed2ccfb1876fc74d924eaf8738394d67cc0cce53ce5937b6c', + '0x55bd2acb82617f032145541ddcde126e5d86507bdf5e3aaef74fe908504dc50a', + '0x9312533bd8b9612bc0cfe113e669d187a8c0c2116ca4324da1524edbbe0b014e', + '0x854c4ae0a25dea79d1a3beef2f2b43ec9f88e664403a925b13f01a68cd725bc8', + '0x433b187b7e8cf4065ea15441c4bad8f886a23cbe553394fc69edca119d37c18b', + '0xd563637c6e5101a657915a418bc4825638132073e18d6fb79757a2d0e9912ced', + '0x634685fc6263a04971c12ca95eeb8ab718c19a3f2f4afc9afe69c7fa320ffb6a', + '0x71f1cd6ea922e3167b159f23f3d649f67697ec2d4cfce63bb08893147c95f0f8', + '0x0914090e5e2206661e66985f53901895c67e923c94a438c906e863b0ceb09e78', + '0x3caaf484ef19cbeefabcacc5478aa65ec4416e3c8f899d190333e45c0b1ea566', + '0xb3165c968851b62f318d1285571b0409e864be256f4817cae88672eeb31d32e1', + '0x41cfe1c698c9043be596a147aa418081b062401cc73bccaa0cfa933714c0f4b8', + '0xf05cbc59d191faeff9ed3d3f4cd690567f20792246f61059944c63770ebdc5d7', + '0x4c3b75eee105866f388ddc157535fd95be88031e1cb6ae39fce3f52b33158550', + '0x3e8da0723685545a8e2904865125c326eff75c3f41bb3c6ab2f799d51fd462e5', + '0xb27fe2aa2cf4563038a3cd41e43ad89797e9e6876e0b38b3a04023c588bb4683', + '0x14c6de0a761ae1d1d94007b4f1926a54f5c2afe5262abc7116d7602b9a39baf7', + '0xa025651ef54b13c36c12199b9bb770808fcbe2fbc06027dcb417864ea675f676', + '0x03aa2160fb1089fb3512cc62019ca94bd1ea909d3ebd2ab139a75618d2ef1031', + '0x1858cbcaa0bac301639154458a05d6e9e09179c82fe7c4e4d44294c734ba827b', + '0x17664f7bf31cfc1cbcc45e93a56e7506121005ab96c67ae4b2791386bb4e2e80', + '0x38d45c31af6fa5a8c9311ff1f5bafa5ffc3d83aee06e6984e4f4712592cbb962', + '0x8d4aa80fc5bbb3bfd4523199ee42bf9cc324185c5c39f25d58c4f3830ce2bec3', + '0x7e1cac47a243baed0360966304e5ab0433f1bd31baeea0c9f8ff3b0a194cf936', + '0x748763344f58c131c83f33f9f3ab67197ce538ea580b964c0891d3e93febb0b6', + '0x5d87cbd61f541df420f3d7182988ea3985392d6248145f75ed8c39c94bf78f8c', + '0xe42c765df85cf5a762766023d4f03f1957ba8c03ac8d0ec9ebf7d941e461d637', + '0xbe85b7eab113fd6c9dc5a79dc54919f320bb16a6c15ccad1bf4188b3a92605f8', + '0x5a0685fc3047a67e1001d9ca2b53a44651893318ba38c48a3537859aed33b7da', + '0x684984da2fa594aeabebf162dd378873d2c18fe7728aaaa0ba2bb8db5aa3d789', + '0xd1a8a85b78da62fcb74190ebe93e353b7952892adcb5f8eb3429e62636d4929c', + '0x557415000cc04400d649a4ed29d15277a7bbf8ac25921481ee54eb2fb9e7ed27', + '0x5cf33d6c47bcff8db958b614b06f05cf7fc03b26d3e80bf8a57d6a7669f198ea', + '0x837523a5308087bfdd8e902a70c65d21a7c369f18ffaea7ec4f11110ad6f8927', + '0x95d83e60206935310e0f544ba18a62af694800d978937ea53de10fa56844b7e9', + '0xc1b83eddf9d20e5506ac2740cd09fc5454fac62237a9840fe36e730af64ef4d5', + '0xb656d797a9778964ebb0ce04f4aee42f5cfb92a8c8180a2f224f2b8eae2a11e8', + '0x6a1032b0252df99a7d4fba20dcce566da57d9b8b2c6889b7544781bbb6b0dfbe', + '0x7832e17600e4410a5944a6b98e321378556487f7479c7a61d76745a73c128929', + '0x617296d4d696a5dcdb14316fccb5391a52a8ed132e686026cf1ef8afcc4dff23', + '0x134730ed4259cc79c2538c276085cd7b829bb0d578ccbc922b26ca944014ad67', + '0xfafe65deead69e2173d3eb1ab2384ff1cd3a814f5bb1f15c1082f4ae519d0f06', + '0x5ded236f4bed3f24e1b8f325f94ef363128519aa7d0248a250655bad8b52fc79', + '0x38f086a7cacf753a6d527cffe6c989fde983f43ce8ea37268dd70580f35d925a', + '0x6fffefc15f8e6055b510c366a9f9d2f080365eb11f0b6037cebb2b3304d30c87', + '0x90a8bf8e435b0290cc8ce073de91da37f7986d61d5f26c4af6c3e33782075a4b', + '0x132bcfef2e758dfbbfecf90cbd3ccd1eda75f0b409d80722970592ab22d5274c', + '0xb738d02ee2c884dcdbf76dda6f30805cefd86169157d34c3705c2ee6c1fe75cc', + '0xd477bd9ce819f3b98a22de0039e82bc95f660c7b50e4434f9ffef0181f48f829', + '0xaa55fcd3865134de79d0be329f17d4b9f50c1f208bdbd88f4a2c0009f39c28e1', + '0x9288ba8bb044603e8872c7ebd997fb15e07f7959c9c36f4d849d881b7f43fa53', + '0x6820043d6bbe9930568ad96d889b6ab5f2106fa210179717e25e9279766c7cda', + '0x5ed2932f5e1c7949218f27be45f17a745e1c1116e2b335c09bfc98f8bb79b97b', + '0x212c20f2421e5028316e736666271db96c85fdd2d908885999266aef098b6d03', + '0x6be230f8b0120d371cdd1b207505a1e2a947fc82da4cac7f6e0d577e58a2341e', + '0xdadc3db8486becc8327c1e80d77039a2d609237cdcf1e19bb25e2a7b23a9d0fc', + '0xe90d991aa875f6773fbb12337616303f972236aa71b7611885093bd22b53773e', + '0x77bf6c89399a8dd1612098a3b4b7823e61211f241bb3b6415fbd127abd20d4d9', + '0x2200b9cc7dfa84050dd2f14a7e8d52689790d8cb1bdc7f072b66b992aa74ca8b', + '0x2c371ccbbabdfb895e39a07257d73fa021fcdd94caadfa1d294c9a718b629f48', + '0x3cc465a2184f91c2014d07d99a00827709f996779d8d0d155be54e009fbf4d81', + '0x0455b8d543cd783b66be8ef7c9154b15a9df91d604ccd75e311acb7cd8aae713', + '0x105cedd0ff44c7c8f01c84c1dfe747f42043c7487e766a6b8f864c3d6fb8e056', + '0x5b957726b8b0e2824240925b28f1de33b0d26f6c3449378e0253bf2af12539b9', + '0x7a3b4851fab70861611ef909e00e07d3566789eb1215a0087b5cfaf9f054b0aa', + '0x39e6cd9b9efd3b84afab9b5d57a8f6b20de2213467ac007025bf49c3df1f58e9', + '0x257f57f2fee42172d1fb1593eecba42eac7c67f4162c7df35c72f623072133e1', + '0xa4e82c6255985d6adb6e5a7fa133b6b42a3d1216c2a942893e4ac03b9afe825d', + '0xd374f2538241162221855d3ff7a725562f36121f6ee916a71a02fb000bc76c3f', + '0x208b3c485daa20c13711ab86dec6e1fbd2084d3b4308c80c00e5639ac34b63e4', + '0x035d6d9f92078f590891318a462bf84168f4c0d9646d0388098aa40b3e26476b', + '0xd20b62daae2c307040fc6411729d272adc881fac68d58cb6ae1bed8ccd243ddc', + '0x1ebf18167807fd24564cbe8c0783d896b895038519a18d6b751011d7fc0e2982', + '0x90bf16c43f6b5010e835d39448151b1cb4a0c6431579d63451c3f88a33611490', + '0x79d7d15364e68c881ad6ae0b2f57b7d61e6ba09fce8e5b8b371695e73fa0ce38', + '0x93b4a9ad7be7f8b70f8f938c8e55bd221dd52230c754ac5fc7614a935159fe6c', + '0x0489f287085ce771aae94e269a9ba02f6a4903141d46784af7ab54c192b6fd08', + ], + root: '0x3ab1589b2853143f682dad19e443648489d318c8619e79f59a44d14a846fbb1a', +}; + +export const depositDataRootsFixture20k = { + events: [ + '0x01ba5818c17223777b13dd194f5e64ca92927af901d0fd1599e93c4554f17b00', + '0x48367a14c733b6b4907a2c60fd9b254bb6d7e4d00d0792544309f097d3b76a44', + '0xd0637c1d813bb304c3e955dc869e80be1447848f4e909d231d728543902858c7', + '0xf0232e374baa5d6ddfe7c9cb4fa092145133d094a8e2e09c10ef6029e7c76d22', + '0xafb4a665b1c72e2987e4d157a761cce824b6f7ed024914f9ebe0b7a034dcb08a', + '0xc23c024afec5f0a51d98678b9df5fc76a30b8822e0f5f8f28afb102ed47a427f', + '0x738aba50404c467b8a81ff6233d5d961ed1c560ed08ddbc1420d4a994c093d7f', + '0xa2dfd76a79a5f21ec8530436d63daeae105d86da535abdb3af14b8a322718a42', + '0x0afa5b014a0adc3aadf67fcc32825daac16a58458cd57949aa590241868d2723', + '0x9fe0af4b96294da5ec3fecc02a35207e04a3cc3fb4513991257414a8416a4927', + '0x6c769169d0cca53ca6e31e0ff4d79ed2768a9155b4fe2edb7507df3a26cc365a', + '0xf1d5421bd3410cf6838b797dbda63ff32f4f36795b0cb613e1cbcc7c04d8e000', + '0x2a174fd52565a39472a8a3397f0315e6a2a44d8d582b3fede3743d2e3e11b818', + '0xd21564ed94c14a109fe15b3ceb6a0896f75af33d34993827f484c9960306229b', + '0xbeb5ed5674e4ca6f8718fd7a6a1b9107cf46d84a6c382a39480ada92cc3fedc0', + '0x88b90504375f3cd5c8ba6162d27290de53935abca037ba75bc7303b869959347', + '0xfc044a27fb392dbe824ff903820c993b16f283d2fef0e1f997c8b0c0396452a0', + '0xb3e3e66f4bfd4b987396f18be8b479b14479658dea5c0e333d7324035df75618', + '0x3ae63505d86feb7b07cede59db3f3c3641fb9d8525db232fe29170f9b509ec11', + '0xb644322ce4a4f0dcaa755b6c893b34b69a65df7a3213d14bff1195c1b61a360e', + '0xf62ffe4b04d36639c9e370ba854c475657a704f4fd5ec94945cfc46b3f3b8433', + '0xf20e43a2bcc1230691b6d022b160847e95c2e99195631fe239eeb11c8aecabdf', + '0xab7ff6ad81b9eb26b24c89b179f5568df2eb90d4a9b68bc465615e5396000bd4', + '0x52a3df54c682c0d1100da3fb5385328ff73b4e257d324b568bccf541a6020ff3', + '0xac4d7a7baa5c8a485e696cb1c75ae184c042db1967a63e644f6deb28afc8bc9b', + '0x65ab1005b284b46ccee8b20922fba6d41bde3afaafc0ea98993c06e2eee3af79', + '0x4876db3d488827e29b6c63b8cd6b53f11f9237d7a3fc6d60770553018e963605', + '0xab74447c39a3c53740387f22a1220503004765130d5ebecde325d5a334ab3b04', + '0x74ff13436dbc3122b86b341a78ec7c5af02b756de29c1341004630078456ac5e', + '0x67b5c22da6a35a72fd3ddbde426a6b89a3c08a454a3900fe357197097c4e8e14', + '0x0879bce9478489f53033eaf052f5570d74db4570ea88f766bc900d432fbd6a4d', + '0xa57b65bb32e860f7c3064880c1ea13f5e76c1eb97ac241136f52bb6d4390380f', + '0xc90f7751c3b7d307e98bab47f01b3f2a70efd819036af31351ac5702c9e7de19', + '0x6ff19cb4dd3c57c3f2f1ce02ee04fdbdea80e51740f57b3080b6d913ad108426', + '0x186de53ae9e7f7f8315497c8730275b47eac51c7fd0972d638f30b1b0b77c930', + '0x5797aad88f16d359f6d33ab2d7f333fd7c6e361d5307bec9d09147e8f1523ed2', + '0x1c40c73618f726e5ba91732209819038aab6c25befb7e54f49f570944de8ef7d', + '0xa7aada7398b92b5259f13cbfd10e61baac13958e998f1e355af817eb7edf85fc', + '0xe3a50f5bb8741caa1253fd59be88c252bf80fce7115ca5fca87bcedc75010b62', + '0xb99385870b0334fab7b9ff08b6ccabc044900a2a431715ddb2c537a5c4e4d335', + '0x18bc4025754916d402bfcfd813ff77d440cb7ba3fb8400d11b3bf965e3755771', + '0x331859bea72554da55a5bf27370fb5d8ae2dd9b512bd47d6e2549a8a1e3f1d78', + '0x853bbdac6d5ed928dbf396ae2081ecf44f2b3056f0525f62eb9452182a3e7318', + '0x85c08d9bc962886c1c74240839f363f651d547f0546ff1a08e0a2125d1f5763f', + '0x13ac980d9789e45510751e6efe80e3c20f3135eddd3ba02ca12ce3b4a5cecbf1', + '0x6a404b40f37dd9fa0570cd3a82800ae5de48063bfc6d8532f567a3c9cb0366fd', + '0xe23a27f1edcad66dbef2f2eb6e8af9587899d8fa23099f9af8a5cff9bd5de24a', + '0x9dac244ef28797fddcc2e6b295af1d0e3921adadfeebc75c69e59a400ae0308b', + '0x2517dd29acb875e4f59e4b2ac3a37c63e29a580704ce270f9c727961fe161462', + '0x72272014309e0e3ab91bd3c41b4b75e904933a7193582746cd760a36606fd7ca', + '0xa9a87aff71782de400fde8c9de01558b8e02652b2e9d180709881d1729e6e95e', + '0x6adef730f475053eb8d43fd0ecb6565e66efbd37e4e3cd7a845cade93b05adc8', + '0x2042fd8e45f17fdd3c2636358c1ab247924f1c6945175a1f05a31fa1875ba522', + '0x52030e7190dee69cdfa87d2b57fe0d9e524455fd3a3bf04c6129a779ab28a9be', + '0x6bf1fb0df70bc1ccc67ed1166e1c62396ad4df5a2c0734176b343f156dbe5541', + '0x7c113edea9f06387f3bd2c902c26901f11f3e710372e52c0d5ab0ef224d981b0', + '0x38a3caf12eb5e7e97978787ea2e83139f13399fc84a686b83f553b9c9c4cae43', + '0x917520db093df70a06c2187f5ba77980d883b0472a2483d2f2986c11a3f2745f', + '0x39f70fca4d19206619693f1cbf8c140461bcd4cfd89a10bc7cbc55a9c12dd891', + '0x0f962d2a841e4ae490eb5d508618ec4efad7883992204e7e5cc59ca8e4b139c2', + '0x2d7fac279fe9edf0a3ed13ea309a6eae3cb137c9b035783decb69e53c44d8964', + '0x90c5f46d074ccae4c7865879fd4be382b4ac72b00286a248062d371e49a232fe', + '0x8fe6747d267438b03e3bdd62e98a0a9a0bc9b3e860a77be49c6b7410e7eb8cf4', + '0xe6da0c5e86660c0a6baef412b1f265035e8010dcd4f5fa6c700669fc729f6800', + '0xd6dcd3465d04ef1053e3f984eec7cb2815b49f7dd4dc4a07d221388770326856', + '0x79e4042cbc1bcddfbee3b0953679cfef3539d75e05458254f3414476fa8f55b4', + '0xb3f8096a92cccf01c0cc68b11c7b119fea7c2308662922d827d432975186bbeb', + '0xf85e15a50aafd4d7ff17821c301dc7b07249f6a4909917b18659c804ac944858', + '0xc1192656ba98c8be058d9976e95eb397d83326d8c36a762e12035eb5d865a5d7', + '0x3c5618f212ede686ecb55b6c634668478f4634ae37007387f64e39045c49eaf2', + '0xa282dfc3b8d9feb1ebacd29db524ae015bdc198263f81e9c466fa858c4ec1364', + '0xf18886da457750a2febcd2d45ed091ece920f7854dff4115ea88c841e0c25ac1', + '0x62c413646c7e7d85103063085a94e3285c7f1d34495eb1464c27aad095dd3c85', + '0xb2c9cd6dd0065a4427fea3b9b07bad1f2f6c767608adf62ea09edeb58695dc7d', + '0x601f17bf11fedc05f492999071ff3df8011263ca63da89ddb70b511f610e6dc7', + '0x6fc1fb78455e460c57808caa7f81aa462b056f5b45e6b89738efcddf7e55b42b', + '0x0a0dccb95d1b824a4bc4e9647bcfd3624c0e3390d7fde7b9985ef62f227d0719', + '0x04a28692d8688a08cc96c184f1ceef45de98ec83a74815f082835258067f9b0c', + '0x986b988e1ad8b9c69a79def36b62792506e828676b4374e178af74765aada323', + '0x2bd621c35b1dd9436e38ebdee320d766b6d660e90ae039cc09b24828239b23c0', + '0x450eda98c32e3f1eb2506a4bda2365945d2a2910b13967069ff83a1f150b7ee6', + '0x5fd11f8e5884e7e07bf3c84a9ecfe7e4872c4d81466b524b8d6c9cc4908ec8f1', + '0x33642611f89ce08c1190633e2d1d00a17612c4ac04ec21048b633a0c6b5d4ebd', + '0x3246f523d12f5b827eaae3c1046d1c39dd0bf96774b91e448426d57e0c3fd2b6', + '0x5748defe03171b500ca1c18fafaea82d0dc8a04a99e9107238624d038981ca23', + '0x7cef95703a90b54b8121ce514393f4fb087fc89e3bbae0ecbae9f4b6c32e155d', + '0xf438427d7f9e2fb898d0cef4ff5678f66a3566c9aecbbeadfb2c0283b6a8d33e', + '0x482334e2a16192dd58f5b62bab284b071022ee0d99ea5b119f00e4a1f4ceed0d', + '0x87956b1d1c44983869fe5006e34d1463ae98698c1c8b434e9ef90becd83cba13', + '0x53857c9faf5e4b1c7b879e98e90d3e71d8d84996da595723f2bfb15d4d3fd326', + '0x42f1d5c4c53dd02bacf00dda88a3f1e24afcb69c3072a2045010cf35eb68e621', + '0x9d7908cecb052e9a21852fe7e9157ccad594d79a051df9ea21d6f211b3b80c76', + '0x329206088ce91e857c700deb54ded677d74cafb55827148c3f3151a2ee066c10', + '0x36f7e5008d2ca8869c94d702bf72dcdf1d44b39551cf639fe2971c6e120899b3', + '0x23045aeffe2becce54a8e22a561353179a6a4a471e413d2d1b7304b5417bc825', + '0xdff2cf4e82b66c7707617b2d8affeb9ee8f6d6e18a1c35caf95b151eeb1a574c', + '0xe41ffcf451445333e5d92c742df40d3546e09891cb2899a81cadc2bdadeeaf9c', + '0x9f2341d3d22e01bed1bd677988c99f9436520be79c9d310f105c7e233be9d247', + '0xbcbcec8fb97015275f49ce9449ace992e55b6bcf21ed16250b10c73c37c13f0f', + '0xc21b334b44f6fa096601d72ec3b43097e9190fdd3e612b8829a539db261563f7', + '0x02bb11f61cf8a627b1ad416f29d4321db3427c6c2a864eb4d900cd52d4e9683c', + '0x986f49c10d61d9ab94213fb94e9aa22f41c3e4ceaf29d75ee33f6239d6b3a110', + '0x3ccc087186a668e8df2bdc666252350bc8422f70cd86b94ca4503b73d241c0ce', + '0x6f6a71e9a7c9499e483de6496310800d02d377370f29a717cd4b860e9bcc39e6', + '0xb848b6102fc24e04c186c7c00fa0c9bd4b5ca4d3b35607757fd9e994911a6c44', + '0x20ddaa225234e8e3215f5fb0586e0218f6586878741949c188bbe0ea3eeeabe6', + '0x69e517d5af6091e7499447d0d761712a3b2a6dc54a14331808decc96409d7093', + '0x7b8450572bf47ab26c84da93a09337a0e37fb36f082520358165d7e4cf2ada20', + '0x73a613b22efd940383171608b9ea99a9eb18019f97a91a4fff31574b938a40d2', + '0x2fe791820140ccfa102db44375531724ea10a8be45c18145d69a608b98a8ea8f', + '0x7ace10c6e817842388c9f6ac2fbf48d9fd29c70acec560bfe4a1df8ebf648d08', + '0x88b727e22e523a9fe07207b27439b9ce34c360a5c39ecb6102e4ae9c9ac3d152', + '0x1566cf193efd738df3cd06cdcd89b9fa16aa9ad72227d2916214a3e59bc2c145', + '0x6441bec229912f62abc1fdeb22c897aba2c435b6272f47ddd819a58cd36bdc0b', + '0x2386b9ff6dba153bba1fbf3b70fdfa70ba36ab0a62a8679679ec56dd8188f7e1', + '0x167917d2f2bca88f38504e5c8f3911ac62a3c2b2c6bf08810cae00859c113cca', + '0x10c51df5262296630f167b1b1c1a3815b09e00622a36340fae2cfe1909e6a618', + '0x7817ba36b0326cae32dfe1bfea673356aefa54ba729ed17ed0767662784008d3', + '0xeb1aa1ec67a3809cf9153a14effb03868984f82407916550b855bfe4438e24a9', + '0xfdf155771bf0a5f81340535d64b75938e031abdb23a4796ecbb07829d5e2a9b9', + '0xbe7e8ec078707cc91eed548b23b64657071c7ba0dce0eb69adbb465b115c2e72', + '0x03415d9e7db35ecb191884d8cdffb8ebef2ed9c05179c08210eee9d93f77f387', + '0x1b728ec9e4b399ba9141dc17ff999ec31a3942a20223570719c4819aed333248', + '0x251cc9f7b9c50c9e8fe3c7edc5910c5bf59c58db3fb766914f81f5ef405ff3e9', + '0x432a35fc9ce08a123b245eb0e3257caa9e9d1e4be0b593f00959c90b44efa9e8', + '0xe19d0b08d5ff0677b6f84096c10fc8b0e51a058961de65b7c0583da33f661204', + '0xb1c2fed86f8852b8fdc818767e95a55f717b8ce8a9825e45fa3afeba9b581d8b', + '0x00b4dcab8424fa02957f978f32d7af4131993b3950963b3648bee6dff27f6800', + '0x9fea6304431500e4b5fcd0dd0d87e772d9212a69f49597c84809e344fec2d765', + '0x89449b7170baa165bac7b1e75af24b86e35f6bc7c7665ead14ddf81a9f8a66c0', + '0x021017335ba6896cd5897962d227791ce359f1cb6b2d569da663b382b26b5650', + '0x23bdfec8c32ba1d06c194270d1f2191a2491fc71779cb7b5e68040e2b0b7f1ca', + '0x90bc9d019fee4cfcca225ea967ddd5ccfceb0743bd5361bb29d966caa2bca458', + '0xcfba13d576aa11faa82ec4d60ae9f5f59b1c512d910a9813c0bc8493a8ea75e5', + '0x1c56f5529138438edddcf4affac5469a4b87c242c2c36446b313b781e871d7ac', + '0x22bd4d68d340e059f70677c158e80f03e81860b26ba6b633ac25c1968112c32f', + '0x6d56426496814bf26e4a1f0ea51150f8ee1428033bf391632c7bd11abbfe77b3', + '0x44c595711d4e20f4448e120c8de48e20ecf07b808d04af7f98b9e089af3ceca6', + '0x6d0281cca8ecf1a9795f20db48e946b3e247257a41623235f1755f70f8714aa0', + '0x50157a4c71620a10063ca117e87903d205af0644f1870d22da47fd97e24476ab', + '0x93dce6cc88e79e424952f7f389c6b19ee0161c21abe2aaec3af4871c742118c1', + '0x1225c7d0d531140a050cf132fe72211e7e70bd93755760c5c0ebbf47bc8bac40', + '0xd1412fbd031c7e16f801e158fa7cae634c392a7ebdcf7cc7abc96fd372ac8906', + '0x7f7cce141b3d9067e9c3aeecaac0bb12da981c9b8011d584327ffd3c287e3152', + '0xa14ce909ae68614a293d5ed838fc015b6145180744a6b1b497b9a192f2f4e852', + '0x48e549eb1cd44747ce9566be2f697f3aba1b2356a6fbf7c42a787db86ead78da', + '0x001281502652fd04e520af9655ca7f3e2428557455c9adc0de65a16d1a5f60af', + '0xfa49ada4d99dc82782fff02d620db56ac8565e767f09220e8065710e0dfe5067', + '0x564852cca4c77607b179418c84f26d867773f6e11b5a76d1a14259e666236038', + '0x3b1afa4d71e3488d98263b0c3cef5cd0a563bf83b66d857dc6656a84e7740c42', + '0xde56fbb1b06ef7b9c0f4f92dca3df1cd4e2df509f317ceebee4e612568457247', + '0xe32ee7156484eec816317b51fd3b3f95094eee53416dec8c51fc84a4a537a26d', + '0xf5372d7996557762d83d9851f1676d12fcb4936dbb864fdc50c803b925c21933', + '0x6b17efbcfc7f307d1d3154dd8690a5d0c84bc2e435dddd321755ae51f352fef8', + '0x689e099f4e7a95c74d40f53e7ba3b4a5005251b81ebe306b8ecd498c9a16a068', + '0x56da4b7290fdbee62bf01b1f213ec37bb3a346771c55c6c2b9c9050fa168f88e', + '0xeb291218e2b6825a9d2b0389a769a9811c4712d07a3987ad007d7386c8353fb3', + '0xd0873af33e81a107a15a28484377fb972ea98f4c45e12ab57d5253d0acd0907c', + '0xd920cc5c4d514785f22399dbc03c1191e67133b2a960f2d60fbd0bdeba50e4d8', + '0x187b54e41f65300f6a6acb73527ab90cec410beba31bec62458832a3c5665c3a', + '0x23cc25e9c2de9d7b8805d17e27c874728f010bb4d4c58f8e6027cb0830ae9488', + '0x84b88e3186f32fae491c85d43ae14828fb8c3b2500af7c11fce5b2ecf3fc0a89', + '0xba71f251426c6301b4a8c940b3350abb71c5da0988d421461b3f9f414d8d1021', + '0x238424b33c91dce5400d0a5ed77f31c5ddd6575da680547bc4b571c7a38486f5', + '0x33e53d6b868c638401e0a0474423fb0ec53485592ed7a1b0ce9036e061053f5f', + '0x1ced92a780a18870e3fb3c11f18060647b1fb62283cee6f6ec2a0d715aa85046', + '0x329a9d3c223badac24243f8b32490d771cd888c6efe5c3f8dd34fff70ccda779', + '0x74d245812c705dd25b7dfb9c6726d9f680d3dc3c2ab86892b999078b1fe42cbe', + '0x67645ba66486de9fbc6d913ade0772332c0a8e6e3e82d8cccf3e6e14fc275dc7', + '0x957bca177575c8a9bca2766f30fd47764e59fa3c141d31f68f43530d0a558bbf', + '0x063056bd2748eb83f78ee7060abd21f112058fe13dea5f569d62cc0cab2510c8', + '0x1b17e80ba898ee84fdc99ddac942150e0fb43e9379d6ff1183864f73a8c9479d', + '0x7a22e38d2f06de2b23d18e1c8665f48c3c88d66df5198a800fc6a9407d0f2591', + '0xf23b50a0a7708326db1c31c8a577cf75f178a0e211db79cda07a00ae125d3920', + '0x53ece540693e08f24cd5d4aad1f071e25af158ebdcaf0d03946671b56105ca45', + '0x3bc672e957b8d5ece38f5198840919afb7ec7d67d4bab4684aaad862ef1f20b7', + '0x7cebefa8c96807531c67e3f25b6630d14f3758ccd990707d628a5d656d06f8df', + '0x0435f09c6578f07a311aa9c9877e603370e2ae2d23a5f0a31251a4d4c715cf7d', + '0xc4983a7925242a483db8e1c4a6b5baa6d6bfa8be53f07826fd26712b30a3a296', + '0x2f6e48ee4e307ea32ebf3d7ef151b50341b8f1578bf5b60e0c386b28d7f0ac5d', + '0x33082ac46884c24031d5802f82a0099e8b056fe2a7c54e0c52b089ce36bb8b9c', + '0xc43ca322036e9b40b55886a0fddae4cac639a46674851d678958bb6554db2011', + '0xbd694bbbf5fefa8203d0907313326aeab829d69b7a9c9cd7e881ab3a24ce406e', + '0x0933f8e6866dd3371c6a4d70b0205c3f30ad28a4c96f00510e0c48baf31c5a15', + '0x1c57f03b104b82e481c0e0d823117b930cf4ee338289d4426815c900d1861b28', + '0xf3ebee6f4a9481bf94e333e1eae7e56b073e13aa15028cb0004d661d4ced2875', + '0x43865ec25ad9986f1431da349be25e6d0852a56c4573a13f7b62602dc2428b5b', + '0xeaecf3719d9000fed03f8b82a69a7cf876ed9909f8d4a862b03398c9bced04fd', + '0x323ba57eb34834954f8265a0064d70cf61975428926e75dfd82315de10c5ac40', + '0x87f3ced240576df4bd495adadc600ae59cbce60e8a93bb972628cb66a308ce24', + '0x85262601962d57f29e068deddac0920d9b5a4f7dbe87f5c522d64c695300457a', + '0xba331943067c903d5add7ad72280f9a5b56ac4be656cc07c190d30f44a3543b4', + '0x0839ea04a6eec46a6f00f199f6985b80bd8f7eb17724aec3c1e423d1b0c4e441', + '0x45616d1f139e1ce704a264fa81f92fded8cafbf5feed808c99106e5c622f3743', + '0x0e42f380cb9f639383813fb3a992506b4050c857b9cc55f4b23cb39c83d5fbb1', + '0x995d3aa2167b104e45b8e27093f2fdfa909f6bd648fd67c3f585ee8030abcaec', + '0xb0972a362eadf19d23acc327262663e31dac0954d5dc5299742fc9908e19291e', + '0x5d648d205d43092e9b332020cde403001806548a95d6d89607a9009657c56cf9', + '0xa0c1e398eb5512ca64eff128dc9ad272350103f7dde31077fcc1e307053ad49a', + '0x9eb2d3bbfdea568805d65298f095abfc5d9e6e032e6924e78b00981a76368851', + '0x6a295653b7080d6da7782cb26fbcbca329b32218dd2a803bb69ca4c9894f6cc2', + '0x8be72a010583833277f8f14999d8cd72a645537c1417dee6fdf8385b61f3f45b', + '0x7950e1d8c047bfde4526581bae61a77882473fc98493cb6b5abe7de067013e84', + '0x515569b56641cac2810e9aae0b6d16cda4c7673b010f3caed77b799b3b0438b5', + '0x5c6b3db949b2806461cab20ca1d87137589d98511cba85fde8706652e84fb994', + '0x813465f31b90109542edf0d06cf4bf92460a3480479f2026f372118fe6950f43', + '0xcfefea30cda086ed77a85f51e07683ab3b262cc06eb9a191d640e5348c5df53a', + '0x5199fe02a183754e49dbf0e0908574d46ad07600a07aa2390f0f99c73a160654', + '0xd2c7475ae1a3aee23ea73b3f8107f84160b7306598af025cdde6fd2fcc557db8', + '0xbf895f7fe69f19007aa2304c75e1470efd7e3ceebe49828afc15b4257b88f425', + '0x33c93e7c7bf1fcbd8d921f9c16e6dced448e26909cf18e113327b473d595d7b8', + '0xdfa84b48ee49870a406794815f20d21204dd2caeeef711725d50799e68b132e3', + '0xd73d0fc34a37d798a7c77d182c8a498db8476d9eac3e6deeac95a4f36741968d', + '0x3d5046e0f0a1174ff1148e43cdaf8590860b340b319ca5ce06802eec8f2e6886', + '0x4ad4bf54cb585a53d6e60b5bd488105b835940fbe8a43ece880600bf893716d4', + '0xc71a899711b0209f0d505ff4dc3a09420f0545d7a824378bbed0d16b23e30ead', + '0x0d7ecde289d067baac71302fcf5ac5f4453ea5e99e5d65cf0d97d08a25ba2ec1', + '0x4251575a46dffde5a1360ddec2dcfd5c95d16dd607190ffce66df89be00c7291', + '0xaeb038e06220eb289c8757620d7774947eb6ad25a1992a6ae16ff5f8d91788ec', + '0x75a323aea4f405d1ac042f243cd52e7b22f65d6d90928de0348f1fe916358587', + '0x58cbdfa0b244e2faf0585e3e5d225de33510f9993c210bf42957505e8954116b', + '0xd25c524b53c4c95e811e73d9d2a9c4cda25b794647f4050130eca1800037d029', + '0x9a58dc64a5016ca083e17248b43fb574d7dbd20f1297eea0215b813369420fa1', + '0x74f9c299cba6845b73538058b442f90abbb9d8b89d25ee3c6e0062b7f2e7b5c2', + '0x7437fbfff9d8bb1ff83f89a1e3524c91218fa40a5de18f006b9ace7d9e565d62', + '0xb6006a14c0165415fedc88ba039168a8313c582caee7661212970209ba732faa', + '0xde75bc6f1ef4f15caa61d0f20be909d46a9a8f26d8a7df951030e3c981a6f03c', + '0x5ac38b99bf0f2ec09d80c40c03a26a61f12221c84bad39512a1785237ce0c461', + '0xa43c50803e6cb4426d0140d33ced0d29b67a7f37382ce642a5a082c39ec3ff93', + '0x411bc322c6c4aee920d1a63af845361f9e07098f38b6644ea7e159db6c5e6ceb', + '0xf9f34fcb57fe873083e2ef353dd98c5f04860db5b33a37d7666a2d757f35a1d0', + '0x456eed24e6c0b828e535953c7355eecb7e38566c3543045ef722fc613e6bab4b', + '0xa30545e45ee61794004110088cf7cc7c0ee252368c7b3f25f188f096b0808d77', + '0x4c2737003937f8e133bc3b03ab170129cfa73e531ed8a21c8aa6651a92cb3204', + '0x64f56f19ca2b90e6f4b098f803b7f29999cbcb111b4146b1afc3f89d21f7cde8', + '0xcb3aff7c8296ea2a06315be8e52d09d3a387ba7aee74e60b242795bb09bafe7b', + '0xe64416c5882f35180a4ce22d749029c9a3425887b6f5c50c3d25fd1532f413a3', + '0x917a849eb5db1b40ed8fab13dd437d25b317373c211edad9801f6ec73635acb5', + '0xebae06c87362c1968738fbc5ff21df9f5e5d434c53125a7b31f28120fa7be8ed', + '0x3300444acc27d479d8b23dbe39526bbfe3bf478377bf988b7d0d5bb95aa3c438', + '0x971a5fb1cebe5550ad65e7a54e7e2035ee4aff36f8870a3d411a555997dc96fc', + '0xbfb8d27108bddaf4afcd6d9fdad299bbb1daa2a06705832b7f8426290f12c83c', + '0x507865e60ab9aeb3c77da442eb0ca624dc7d00f3f640dfa6a9336125d0245dbb', + '0xc4776f8cc9d822922bceb1beaecb41e5b87fc49b395d1c35b3ad7174d0a04f28', + '0x10e53e31f92832eb537b29d9b999517b9ba84a20c1e1d7a99fe2684903f78f80', + '0x51396807cb41a2b659fe61ec5940ec5d37e33386e6fad3323b5da044dc7dbe57', + '0xd6cfc2c153f9873deb7a0927d720d6df7c0696e7d9974526841ce0ed1e34cf62', + '0x9b929ca3fe779e0994a4668a8bf6737d3faa808b5b67c050cc6c25f15c945932', + '0x21a8daad93d56c4877d976ba154179a7965e3b8fad12d97a5267de1558872bfe', + '0x7f69ad4abd54224ddb93eabef7d941d8d4b83176a4d523d67ad1901d33885036', + '0x9a3cb03d2579c5550741f7bda6ad83c48b60a912467758dcb61ea917282f9ed4', + '0x93f49248701de56083e70005a3e907c23c73d612ce00aa77e014fec40a95c201', + '0x70060481f4ba403ea54ce7722d00f37904e6b410e19adddd7dec83514fcfe771', + '0xf7aa235d2b78a5e72170c31042bc969f2b4f6be697714b5f9c6b6293838e7455', + '0xcfb5408341acc00e8732c31554e0dbf5e818c5d16e1bbc686f20248c7ba0faa3', + '0xb65cc497753387c0507796135b621e5fe3fda430769c749c0f8702e4fc08a093', + '0x5fe1265daef316a522bbbd3c11919a499eaba35fd43e679f65eff0850dd51389', + '0x2f62fc96d7f281f6c2485b3b04e48eb18edbe4ab65ad27d640133681b9624f04', + '0x455619dfb17f0a5141e2ce78caf45d96280b3c16c58d0206bb5835c8b7b2ad1c', + '0x298d9c05ba6c2fa323ed7e2c138ad6e7617f4a5361d85f048ece702eaa372995', + '0xd0addbaaed5c3ff61bc0b8b5ccda8b6a94b347423a4f84893e2529d51929bb76', + '0x2492b617ea9de5082e3522b7d0bff411618768089c3ea656627ad8986d7f4e01', + '0x54839878bf6f29c855e15c1762e2d8b298afd6bd032ea631db0083a79b99f6b4', + '0xdc5036cfe485b38beb5c3be2dacee3e96ce6fafd295b05111402ce24eee26e59', + '0x51b0d7db21369b00c7846f5f8baf2dc514028c06d44a190f46a26bf58f3e60fe', + '0x6ed56c96b3a3a0263b9d4e6f0f544535d7876245a2c718e83d9a2d965faf32cc', + '0x701182142765490d2050dcc502d69feb7a657fa44c6aaabe555881158aa6b486', + '0x28a4d3f1cc783e7cf62d3ef71d5636c5e1554747cb0611af1031dd136b7a269b', + '0x647d0f47feb079561fe21bb68bcff9a3f236bdbecac774a50f95d6af22b859c1', + '0x30b3ac6b2c8650eb9778938bc61d1b5e45a485a6d021f3114bf7963c0fec83b8', + '0x83c9e26fbc21cf0416489cabce48d0eaf8405eb59aeaea428fe79c5496549458', + '0xcee8df28d845c17e1dba7a4ac51578b648d9bcc6b37ee2821def5d62ad3c7541', + '0xaf47067a4baac7ff568dd8f6eb6abe7da640b0eecfc250e98e418da64a77eae8', + '0x213aab4fa9468dae77f68d701b1a32c5f6fc6d5c7c38bc930704965e8c430c7f', + '0x27b50f2d9afd82ee2a0d53e1430563f040f70ea84f2a7c2318b1d2a14a7dd690', + '0xde058ee2250cd7c6e82f0025d8e6aa6a24399f45bfdac5629704218a7c6b83ba', + '0xd6a09624c254efeacdbcfcdc5588a1bce9ab0778eb892369444f47cbf2c34717', + '0x9b515020b8c869ded9d6125402696ba62d243bee374cc3155c8dc619b1709e9e', + '0xc218e83841241b324f94f2e484c4d175a36611a0d70e0e17a1b01fd0644e62bf', + '0x979d145c9a20f9a29b7bd1aaf7765af4e25edaa36ab2282b05d4dd5079dce7d2', + '0xb5efe4927c8d96a7e0702364afc265293491842c0f61a7035af359fa7733c51e', + '0x9a6915160b0861917c894af196d04804fc186fcac250f411d3de9f50f4935ad0', + '0x15d1fc7fdb4a899eaf9f630433af38deb72a0c3da34b5a2cf7d1609963b6f12f', + '0x5f4c82ee68e98bfe2429e5e941e9b519541adfdb4ad2ccbf2ad4a4a28ec0df81', + '0x8c821a42ef5c0ca0e4f4fbb25fe1d125605b1948e58faeb366aa428147fa5564', + '0xda3115f56c99c7d765ed69cc189a8d5605768b07b935b7a0d86c4e10dc582829', + '0xf620f0c0e1a3984b0366ea6b65c89a9f263d707c8a27146577661229ed0523c8', + '0xb8aba00d6d5437834073e9eb9f5a114fdfc47e341fe0dc4ff0ebe96bb20f5d04', + '0x7276a939ede7733ec1297bb1ecadb4ac7569c8ecb9173cb9471687485a1c5451', + '0xff8129b10a3d7e4db5f9db4f69e82650ac4a6b1db845d1fe5fb620ee7ca113c4', + '0x7d23b7d69cdbb3af1a63737196583642f2cbafe35c8bf5a5b5f2f3fa7a4d83fc', + '0xe82b328245b6a554dceb7c928fc537ebc77a562b6196674736dc061b3a13b80e', + '0x93389f1da009d453f443aab01aa18c48cec628cd4ffebbb5a5f7a652f0b2a0e8', + '0x79c640ed577f4f00046cfe21e899133eeb87da71321ee688738867336e57a497', + '0xeb60bb68eafc01090d977cfeb90389df221de557748548e8d6754f10b8e65a81', + '0xd24214038947c3ae0f71709274dede7f896c2de8601b177ea681880ff224c42f', + '0x5342444e7e4039d135c669144caf63abfb4e67cc1d6a127bf66405a733afcff8', + '0x0834eaaacdf3e0e1298f37d8df2d940757402498c246cd5a50cc8666eebe607d', + '0x9e9199a285eba216db915bfabb8bda9f925f3026419e385261ca33028539a5e2', + '0x340ede2b359f116bd7dd820f717a6dc90a950368f438e374a49661fcdfaa3c57', + '0x1addb9d8cb77315e234110f73383c0aa13d95ec14aae1a5a30cd403b212ab9b8', + '0xf45b9b193301fe484d378d7a7639a8233f04db453b35298842a67db5da6937ad', + '0x27f75db4d6fcb21f4b052737d2af3164b01ac4f6bb58ea29f545aedbc5d2f2c1', + '0xef56737c32f2ffde7d20bab3fc8c76d36adb7b604cd4eef34af2070346216039', + '0x7fa6d52aaa05e6f42943e003c7d84614b695dd67353ec99b9817222453117bcf', + '0x718dbb1c8e144df01254b3b2e2f51ef04d77ac0ab0a29e2d6dbc83f1bfce7df3', + '0x794e230acb4f8513969a68c7cd8b0cfe77937936fa95c54fa6ef027bd659b022', + '0xc20b3a1c12fcab565fe4a9469a30a796c59c0d1fd45929a2b85c719707e8edfc', + '0xf96d0802bfbc4fc73195799067d43efae6868087f41367c38a4f798db465f091', + '0x148413f66a0d98d9f067c945c88ad8eae4727833e6270dbd94f48982f4acebe8', + '0x6df67aca084b26650991e69ee00e7e34e9bde608c5b3fb97664ff6eabbd638c8', + '0x03fa6633bcab2bbbcd2e8ffe59a76652c4e685e70f974d1024f396f819f3a8de', + '0x54a1543ad2e1d98b28610e9bd3ee62a9ea7f279305eeedb53c02a570a899c2b5', + '0x5ea5397ca8463115ff7baa0cd721b4b1bbfefd87d68228b5e7a7d13ec91386e1', + '0x4ae1c680c49e468c5018d207d0c61ca26b3d79b28673929c58739a465c49be3c', + '0xacf145138ca97e0b4105a926a3ef4851f1db30259510fdd3130a7c03e392b117', + '0xf617421d312e14ae42b90a13df69f09674e738fc3d297a5b560361a052f02e50', + '0x0db7c5b8c761cff909e75f96ee1e77037a0a7139ccb363bd92b9911571c13047', + '0x61b1f8a054369aad59ae194fb4c5586209d6d9c106c504091f1b7164ffa838b1', + '0x370d187e50d97ba31d0785d8f09dbfa2d3c95d15124456e478db86947fd68b59', + '0x3785e673a79b3d8d8f8c899a1aeda96506f4ddc7d10dedbeb126d5b1ee644b63', + '0xbcede27efcc8b2681abb8848fd3daad03f05d6c2198f94a800d79c03b4653ba3', + '0xc811b6d7553610d3d307e18fb52d3eb148e1177188be4c1c84fb35a454f41590', + '0x21dea2ceee271045cf6f5ee63757ab68280bfffe867e70ffec83f68138aca80a', + '0x87576e126ad47a5e4a04bc7f7a1de20f17d65214e2de4d98b1b54cd4565fc52e', + '0x3bf016a7d83f99d0d7d2fe06c8f50277ed2dde7fa964e0df99cbcb77c9e7d4a1', + '0xd9e4a95e341960dd1ff81c54d105c5ea948031f1fb1f361fd823107bf51de678', + '0x99531d33670a1b6d226a57b4679ecb53962bafd55894a208b3c1e5172e0dfd80', + '0x10a8a0e374e938a2542f2a5e993ff581a1f2537407dd79294b4897f72da8b086', + '0x9dcc02c38007b0f59ec74ad79901efec5081d4f75be082fc9de0203c758812a4', + '0x55768b6a767a6b86832e308590f52ef97e9ddbe11accff530b5669af0f744847', + '0x2a7323979d16e345e107775f174ee9e1ffa5ddf0ce15843bba858430629554ad', + '0x6f06a392e7b0e55d98468bf70e51f75e02a0305d7fcc49e5f7e370a3f406f569', + '0x6fd4c65461ab1682c8580b608d9c4cdeb1dd73961f4e8a28b0e14869985bc437', + '0xe821b5337d4fdc9557dcee5de278f8d83b276a9a044e2df694fbf9eb42bd4701', + '0x1bb1625a8d2262dde6bcd67bcfc604056275d1f77510344c27d3c58708ee8a49', + '0x982b8087c12fe879731ab8aef20fc6f57f71cccd34ce285856c39c898f7857bd', + '0xbf61b54b104c5ac678c2c503fac534f4ec9d60730a3fb8db9f04abaeb47fc009', + '0x49f1483a03e66eae9acb372a7670632970fafe6e812c1fca47c1985ec0047ff2', + '0xf4e2fca070f8e9b99314932db936d2444f3b5b2ef802f87c8627a9eedc52cb85', + '0x7b737c3142d82efe7f92cd3138f25fe7dbe69d84d8d90a6e2e268a1db7469195', + '0x9d306ffe94a6707057a4ed8746d9ce781d7a99a64378f281ed5c70b6f13a409a', + '0xae758e46e28732e046240c87eb6c38b1df5f970e476daa73df4b55e95af06b6f', + '0xa0e502e5d953773050810fda523c2f631acca8b9090778eb4ce6fde07213ddd1', + '0xede25ad1e027f9c19bb0d5432b2b320992bfc7cedc866e368f66a7b10202c050', + '0xeb751b6d66abee3b68aa09926625845e3da2ddc31d7fc36f8344929d9c0b677f', + '0xf5bd963625e431a178d7d4cc6a6fb625b0e9d0d92c26366ecd2d59623aeb51d7', + '0x0edcd0210ec5b256a40704c166a0f5ceef7b90af0198c2d6748e639cf63a1bfc', + '0xdaa30799780b9a581e3bae49b7da5d6f392bd77d2dbf80a18493ba0a690186e1', + '0x8fac00a6e858e845a5ab9c6396f24502f138468b579ef18946e72fa182bd931d', + '0x6a4f96677f758029d022b7d02663efbb1c17fe57fc60a96fc2c7e38ef1f574c3', + '0x223d16715efc95ac69b164dc4019c59a96581b33395d35177863f57d52f8130f', + '0x0c1f9077b3f5c33a1d0b85dd3e94a10ad0edb6502d77d6d71160f0d64a0ad9b9', + '0x1fb6d0f2a58ec0402f2e91763bb2d88dba52b748923dfc1de293dbc675e0da3a', + '0x396d727d78c719087431b343c0a0c33d4367807a4f8ad1f2e94947c35200e3f1', + '0x6188ece27cb7336ffd62490465df2d22e3124d2ba3340dad897353653e253377', + '0x9fb62b820ca6c09ff15c514c5a42fd89243c1cdeb959eaa8744aa04e737b2d8f', + '0xa45389202028f8a102df134091538a75df8b5a2306e8903519a6486ac6a8e5f8', + '0x89ad0db9cd37fa6c6b37da489486f50880df16be6aa702a75060f51090f8af28', + '0x784b8857c0c00a292b8d7fc82b84bbb06f941314797a49153394f7c2aebb2ee4', + '0x4ff707a3ed214b124b8e0e9067bfa7f8325ec041065c0fd8e67f19fb86f2dc6a', + '0xf1b9d58e05b9309874d3d288c118fd0926ae8a209b91bd7e06c1e5539d2b5c80', + '0x9c104856396f2608e8b4c65dcaadc28ac4673fe865d6b557b19ba72417f76c5b', + '0xe667641c0e7f8883dfddf71f8a36dfcacd8e8a201c7d417c63d22ebbe5f92c4f', + '0xec64f16579e9bc0be680ef019e6bbb9e20779e39dea018499ab06567d9a09117', + '0x34c0b7a4e6dc72fa27f6f1ff2752220fe8f45213cdb23acabfefb2c1dfcc8a95', + '0x62eeb213441d09380ef6153e09d5f3d01f7726a7b428fbc26c06c35de054cba2', + '0x322103b1063fe6a50ee9e454ad707f1735b04f2d57e868638390a30c360eae43', + '0x2272bb5acd29a8a41bf997e045d8ecaf2c877c39a2672efc0d7cbfccc2e523c4', + '0x82f29dc93e8192972425c891d8dbeb11332d99fde81e98ba55210c0d28f296f4', + '0x0ad30883b6e4256e8a2acb0e4330bd24a634be587dec4e607fc39700a29dd5a0', + '0xb9f02067057dab54febe78bfaed9e839a2a80a0897c676f678a74e1839a74502', + '0x989e5fe08599a3c43df3c6b4b65a06e46b9f5f89109b4accc4ed3bb0947416cd', + '0xff143fd7f3d278364d7f82cc3180b38d14c612ffcab82a120a341c76bbc781ec', + '0xc8c7e784af251f8a3d3c8838728c3631ec59e82aa45ecc818a823ec72a08b463', + '0x8a62815364cb918265d864820cbc59dda953212dd1b582bb9d51fe3ba821497b', + '0xafcc281833fa08279c2d0c354a6598cb42cbd860a27073507b5787b8b5792314', + '0x03c6917e2e5aa5aaf2b3caa46fd99964c4e5a9a2ef641d4dc9d7f1eb58bb525e', + '0xb33ed288d07b1e78320dd9c0d5e1a8c009e8990c99106df4f0232412fa41eaba', + '0x2ee253dc324194aea5ccb096967f709a06cfaaea68ebdcce5b4214bd540da830', + '0xd2fe9eb95731090f13ab828f7d8a98016ec67f760433bb10a5a592b1eabdbf1f', + '0x8396987d0ba5656b308eeadbbcf91e92d721075ba187bddb332126c2651a976b', + '0x5810b8e6bfa25221393c287cdc106b774f1029dbac256b2823bfcc558bdc0c0b', + '0xf34189ea4e617db50a08247a210795690b3ef45541c78dd19832cc8e2e76936e', + '0xfd81e724a707280bd6860e5951e7649cb170442f0f0f4b4a6abb913164cce4c5', + '0x4f81b4c5d9141e8f5926764edcbaf77fb889464b59cab961226f5a0565a3cf2a', + '0xa913b02bc404dc65ddbf550e1e971def05c509589ff3c5bfe8b116256b17f74b', + '0xd0bd339bb6e6d13568d7e347b3096a97ee2052e64fbe926da0c4aaff0ac75ece', + '0x7d5c263a9f55dde97b4d28f2e5b2c4935f602535aaab1ddc179f4e2c62be0474', + '0x79841f2b95a44c5e9a19e120fa97b62b1591c4cc1a997fe1a63863cb7200d565', + '0x6402a2586c2ee7237be93136db29a22cca83465e397335c9a5815ea041356c35', + '0xb38567c314c1a9331bb91745b0068576ebdaa399c1fa3ae746eb778ff88ef687', + '0x445d684a863a49519f536d990347d5947e6fa31c2b2191be1c49b1b2bd3a1d1b', + '0xd7b90d6b9de7cfe29e1d34c304f85a1abd795f0487f61f6b3b5b9ed5c212aa41', + '0xa1fc37acd45c40f1bc006c8d91669c2dc9fe2aff5b01eafd2910e8108f3d33a8', + '0xbc3dd887a69c9206789c12cd9e27236c2cecac8b3660a609a1207763cfc0089a', + '0xb7a27c4016714a1327cff46dbb54ef6e903f3931b54b9c68a6b0e773073cb7dd', + '0x9fbf72d3ce7d5cd8f407d5dbd65390b43f170adf4b6aec3f9329740f98a88bdb', + '0xcd63a65b84db283ccfcbeb114652f8fd069d446f2070202b8083ae5b5241dd45', + '0x1816185a5b64195b52f0fa724f73dd362eb72bb99bf9b401310e512e4f55fbe6', + '0xa5f54a4f0006fc26d8339deaa0ea36c347c0b5bbd8983ad147e48ed1e983885c', + '0x880acc62c41101c5131178620d6009eb6a65d29c1b40268b44e6634c18c24145', + '0xbc62e763426078ca4860822f9d0423d083cb77fff384acd623e77c4dbd32aa93', + '0x223b166edcc12d39357e9ee41564efe36cae401d647ac88c7d94a798a5179fec', + '0x1ea750272df98dac06e59e5fb033147b09a704846c4a48706a075a70860fa8a1', + '0x2eb04e64bea8c22f9ffe4d5512de246f2bde70a9542ec1e2db0ebcc162d06c6b', + '0x92005eda35c886e369b8bbd2452b9af41fc3f40858b840fa30382d2063069d19', + '0x8a7bbe702facd0dcd46acb5145b82785f0d0da59f1cb0420747e6a55cbac43f5', + '0xaa71f949e03a9dcc022b1b9dffafbbda42d15790c8af4f34291e84998d44d59f', + '0x07a94388f0535b2355df0aaf78bf150b86c362fc1927793557dd16df39856640', + '0xf8b916c7d90de8df713e8c3d26847bf4ec1a46feb7be65668b6bf3aa1c182ef3', + '0xd874dbd786b82ce452a2b9628f4c9f78e7f1804a1dc30b5a58d7eaa9345a9706', + '0x9e5b7188c281cc4c80275fe6d2b7390a8bc3de6f71cc65cbf6ab5a5216956e75', + '0x84f4084726ad896c737d9bb7ddb5ab97d4e25937604c391415b9fa8bf56bf4b3', + '0x5c0159ba7392fdad1a236a6a438c1f786870ee8702a2f1aa03ec1e7b4b287b11', + '0x4369001538aea0af15a0445cc61baa2c5f91db709cc6ae68e572b0ae5f61543e', + '0x55c991508bf5a76dc7fb9cf5e19a7b7a0dff4654a1495a1c4af456c88921fbd0', + '0xf1523208e6a6f6b95d89e2c7e8d15f222ef9e620ca04001a013b7a120e09ac91', + '0x50ab76e385c4ad491a49e936b12fc6bfcae60e58b30b5aeafe07cf9f477d4b1f', + '0xd1de3655da56bcbbd576f80e549a6c5580dcf9f3639b8040c294b6343435d86e', + '0x7081a20264908d5944f1fffb88b27ad9b27dacd09e788c6c18d64c02ac113131', + '0xfd786d1c20a090bb983c3fa2851aa7d08c72d6ab2b93fc8417e069130b5d4f3e', + '0x0657fd1c9e522f9db7ee4c39345dc8bcb7e5668e3e5dec61ce36691fae2be9e6', + '0x5118b7737de0e5a8a2ce6c986034725fe515a5c0b940a12ccca3a677bb777711', + '0xaae06bed83833933c78972b5fe0f04f65679a7c64d5480f5622817b5ae5a0fe1', + '0x6795eaeb9264267e89180196ff8b45f8b7772b8db3d78c670b3953dfb0261aa4', + '0xa750855000b79d6d8b026718f843e68cfc44b15be129cb38f518d786f9ce6870', + '0xa6672597d737e7123223180ee440fbfdd472c25633a14e9804f3b1e64f1cc1cd', + '0xcdcdd13a8bcc4598d41c3000c6fde4ea4e691ddee1322ce70137f805e88f6d43', + '0x9b50cd0563eff1a8245d42bc942481adec75745e49b2fc7e72c6e2c21d118199', + '0xb1ad2f18cabfdef0b9a176dcfbb24841cbe35c5411fd859055e9b72b671ab1bc', + '0xe5b1c747eb4f25f049bb2ab22840d86272c95d1cd691a2f2f765a1ce022bbf6b', + '0x5d0a063af7e0c0f7c4a05781602fd08ead4da56ace0b072f05bbeac79461932a', + '0x7af68574ac32a10096342fa1f42216a234b4296ac4e1516f445362e87d6e8d64', + '0xb5da98feb287465c6e1ab8fff301bd9cca43e93a660189f94127e38b4c43dad2', + '0xcf4d2ff970d771ebf62580a87bce5bcd88b436e35d993f8d031d91a20e9bf69c', + '0xd370696e2dac78a1697eb3fe6dc6c89a1b77ca8fe07bdde3dbda117bc60d9b58', + '0x06396a3b0be004ec093204e1e2de3a961eb006069b8933600e880366db32c7f4', + '0xddc325ee0450525864740c7a9e996d16d586d591b0c509d2c8c84357e72e2837', + '0x23070ecb04a71bdc3ef9272c611da71dfd688a213a7e15144e21f148790e6ea1', + '0xf584634834d6bd1a9f3d430b61b3c14c311699d105f762159c8400b09a62ecbd', + '0xa34727c8c242893511f60bbcf864779284a6ea513f6916c05de1be7956bc3bfa', + '0xecc2bcb630d0d261e0ad40db559a114d42f99bcb1eda05d9b3caf29ec55ec748', + '0x50bc14691ca9e2e25915e16d691cf686b4b58cee672e0efe6bd3398fa2321d62', + '0x2e5a7dbc0479e6f5222e4ad69e11636942fcb07218d3ce3c1d8e01f408709fbd', + '0xbcabe1a5f41a71cb422f5e121c9b7972adb942271ab166f77f15d6a71adfb89d', + '0x9c822497087d07d59c4de6479c0af84d62b3308f9c66ab6d00a8e598a0f9f601', + '0xae70369e455e741404df67500960eec4bec609580d88293e4a1060dedb0ae8cc', + '0x5bca2fdde7c8d8a09688681fc23c0c316e6d76b575f2e2c73a809bcdc225d4a2', + '0xca51aaff1f5cd5ad46591a49d542e196a6152245ceb9078a129c38eb2488ef4e', + '0x764c215ea6f8c73d7a9e6f4bf45ece5323cb0bf182f74e6c64f093d79291567f', + '0xc476aad91b49b35cbc0d5ef61d864c9127faac7ff9993909f546f9c94c504494', + '0xa8359d1e7d1bb019b9c7ff863f71943a35ad407c5f2932256f5aa2ccea3fc1e5', + '0x987577a001072e51258ad06fa78c7fb4d456daa3326290ad383b6166b3fef263', + '0x5932a8135faf71bc8194eab1d1e826d8b7bd90d6108b029361407f35abe0129f', + '0x93b40fe8441021f6fc5db8a1463eccdc503a3ce9b2923e53a514a4263b55e345', + '0xe742d7d066cb3d8bdc6957dea1894fa4e4c928e103aa18461646c9a93742411f', + '0x649b4bd5f4d35fc84b3d85a6e33340720b23d79218b8987d9d3a273161eb1ed7', + '0xc95af09762920b32d07adc05ea2dcfc08e7e42822f74b91f5ae9bd73f67f2db5', + '0x8514aed4a46491464d86adeb53d4da5e0874b3b0c56bdf1e8df9e6caacb8bfc3', + '0xa7cf5bc6e5fe5b6bcb34ff61cf65be22496c14bfb364885d7bba6fe6dcfc70e6', + '0xf32d8ae12aefa2f25d230b72c01a5b015cd4202e31d00c65917a88a286540706', + '0x774c3346ca6193c162045d27fa93e1480af18f6971c360fe358f3293bab8d26d', + '0xbffbb583273780af20b99f61177920062fa25389a7682fdce12c7fcc7be31b7f', + '0x08bc3cdb0cf555699cee480604ce490a5ddcec7b132c9dd1d60c90acb9477482', + '0xbdc96b252079ad9eec0fb5ebf72fd9679e751eaa0ee8a3b854f42ed2cfd09618', + '0xdb25fe48c93d8b85e4acd0870ea0aac0ed2c7888e04886760d62a6867612f8d8', + '0x035cff04ab39e9872e61511d5cc9ebbd4da87cfc28910316c855441b5bccc73b', + '0x9a38981f902f69ca224c17aa3f967d954b0fc7c1e576ff0555c04216e806caa7', + '0xe3f282c0f34dbdcdf5257b3b66f14b4d5c0d455fc47b530bae81f68635b3e3d8', + '0xde826de4ed3cbf98f6dda38adc09c9ab52a96119f592ac1d49f7c6df394de258', + '0xa74f8dbf5b996bec1e15d083082d9bfb51315481fd72bf71358903349d57340f', + '0xae6cd9c7a270cb409a5db27109252a37c53a47637e95f23826854e616e52a870', + '0x0322c82755bd4c6803270f541ef5f0ab92f19ab421056629d25702560679acf2', + '0x246c11c206ac1349356e5e2fd0ba517de3d17b21c3071b67773177fda4a2bfe6', + '0x3598c384e8e2f6b930051c3e4ce9a8158cb2a5d5dda05997c44bb8e3732caa1e', + '0xfd7407aab4bcc5be6473007ed5e348245693f10e38d85c6445149c8264b3ea61', + '0x2666c83b368b23f8874bcbaec8b3a5552cb21b753a59078774feb5bb607def7b', + '0x65165a4341b175a7e85052b33cbd44292c4ec481237565821ae10c92d8dc24d7', + '0x90b93b52d0d6f7efb959f57fd72dac8af235f9276530df95e2a0095be27fb8e1', + '0xdd757f99812cb76cd98eb7898be52ad5074001aa9458713b334f36909144f52c', + '0x70910d079da353935d7c22d1f38c023d53dadc37fa77d542afb588d68e5060fa', + '0xc0665e0cb520e96e10e73b2b3977eecd29a6e6acf8aa8172a84ba7daf57bfced', + '0x4f767d8dcd8c115d984ee878b9fd252f94293bbad82337c254eb1c03458c192d', + '0x338d0e5e92db4d9cf7cdfafdd0ed5c2790dce4bf1e6d2087ef97392abdd1aa5f', + '0xb3be5d559a1b9f85241c97587972b7f5202a939ab3e9921e0ccabd3e80a1e44a', + '0x5d384c2112ffbb4e48b6117bfd42659449e9f2cf025e1938513485aefbec33fb', + '0x28a4ccfe42262ab7f144063c638744d82ae3502cf2ece26ad2c349fd84492f9b', + '0x2ec8776ea96b12f64cadbff9a745eb01fb6cf2da2272871a167fb5f44c55dff8', + '0x52dc90f7bf6196ce3c7d7c1b18d58a8e3ca306b8d4a2416b148956c0285cc9c7', + '0xf375b028b9c00139523b4da818a826a36aec9c9b4c82c23f0feca6344509dfb0', + '0xe47f5c0fd50ea2ca81d95b4a7ef06e408df23ed0571576a863b1ace175ee841f', + '0x2e6a6919df3e97995f9f1f5bdf48147f8df1ecb02190a3e12c6bbdc2b9edbd2d', + '0x7705f24351df00e0fb058341fa86c3aac754a2725679863cd0b2cd512e9e0a79', + '0x7fe899fb43066b002b2950f59db15ff8d3051f2006699c3c6c5343294039c9d0', + '0x2f026ba0433a89fb5c19c519218f4fc28a054f0d7639285ae2194ee2fcdc53b7', + '0x555d51bd461cf9490f466cad3055a9d3221209b18479e594f7a9e456a2bedbd2', + '0xad32a923298511d8878051c307a3b054fa3895b1ae5566b251384b6a87d6b5c4', + '0x06bbfe2135d26115d8545854aefcab9c69d0eaee9e2e8ab9e492090badf06665', + '0xcefab5bd4bc710356a96fcf050805c0e8324532a08c1da3ef28d3b9822e1fe97', + '0xaafe9d1f090afb8537e7e25a60846dfe814e58a2b457b30ec5c2c030e87b1b78', + '0x28a5578009170f6ebeb68c66e027da76800a87ba332c0d0db06c229b09cf90b8', + '0x8a5b720af445936e4a1b42653fdbb659213c1a3580a5ee92a09831105fc9d00f', + '0xb110e55649ad87a16675769432c779080d535400ff0867b2c0e2b2cbb1b90449', + '0xd269d87c50fbf802d7848e94f608c7b543b0ed6049e9a4d915f38f1c7b091d0a', + '0xcf212a7c6b3fccac37bdee34f03894842e03bf0e3b4e4b378686241f2681b614', + '0x0adbb0afdb2b6c67f35f01f80a12da46968b5ade0da3695b7691ae6840078012', + '0xe2abfd10350daec9601d79ddef3ac7193e68baefc1b9911fba071713ffe14ea1', + '0x203f6f55c35c47b78f554a3abf5afbaf425688a1160661fe8b92a3535ebdf9b9', + '0xd64c9f20866f995f5746f4e703992f5c884ff4985ceafc370ea89801e380141f', + '0x20b696000c927144eb65689e07a3952ff968622ce653825fe7b7394e1a4e4e44', + '0x9dc2888b213712669a8b2151a4470d6f626f9ffef99653d2a225262ab4005a7f', + '0x8a2d9f3a52c60faea1dffc6493e50aa556f6741e5e699d0913a6723fce70e17a', + '0xa6a271eba297dd99505534cd6ab3475a896011c459defd18740b247bb570cfe0', + '0x67c67d22ea819afdbe41d2793ede49d5db33273540f0eda56ebce280c352698e', + '0xe984c837b893ae68de0d854b554197dc448a1572d2aaa9472de5c01590ab143f', + '0xfc8c2d77a3f1375d304886ebd4cef1d35a4de9d29f26bc131d2da36a334909ce', + '0xf2fa75a1d3e13ed4a5b1da58bb52f8078150feb0e3d0753ec43af6e792956f3f', + '0x1c3af2028778ec1686e7a24d2aa9c676d6d1a581ddb469c52eb1fdb0e99add67', + '0xaf340609676013fdb6a10c683440b451eccb4dc499e72e3135daf566a85b56c3', + '0xfd12f609c7f4a06296348e5d81906fb5ed32f8d07a02d7b672bda49dafffcac3', + '0xa9643f99820035ceb464b26c1a3093bfcf6c7fc022a63820b3e3602596922d26', + '0x5cd61b3083b7f0e4cef6e6104cdbdd2bd5dfe4fd2bdd21a372c6f8eeb9437aaf', + '0xc456158ad965afa71d7b62b857911a8e8a4e8a43e6beea09e384ddf4dd927b53', + '0x00c88df08fac1c02d3a0aeba0c1e70b8f3e8cbb46ec35f0ab18f4d095855186c', + '0x8809edfe1bb0d106b0a173f8c666b70bc90efb747d16bd92a7477fffad763651', + '0x9411f10193fd4b816de214bc314a2b875a9ec7a3bdc63fe3a1c4bcf56954efbd', + '0xaa4ae30d72b454588bc3269582c2d824748c9b55393601b82c2b07d5f89a1e4c', + '0x906a654056823b64a0c10793c5e679fc8efd9edf3b814f43001a993b095d3df9', + '0xf5ac12bff3f7fa2cf08bb0b32480774f5e341790171a16d4bd1a44d82fbf4c53', + '0xe8b3c343ea8a1504698147f40a451f9e150c5e28d7d43e8ad36c53c8430718f4', + '0xb788cf33c1dfe73c2f18ac6cacf5cd50f20e96aef61d798b71d9e8ff456ef69e', + '0xe3eb7bdfbb7efa2fbc056e1af3cbd6beb7523fc51339907a337272c83747103c', + '0x3c3b879ccd07e261fb7fedac668994f217722f4a01a0707267e7771b7a1c6a1e', + '0x086cf931389154fbd733fd6b782b873ab63484aaa36a0a21edb117426b4f45ef', + '0x280008a367aab7f2959b475ca06c3bb3b9245c85ba5bf9ee83193f17e6951eaa', + '0x0428cb0ad810a5b1380597cc4f84e035974891419eb2b558a7aaf67dd8844e59', + '0xc77aeffcf3083f010fd27e08cbe3af1f43aeb81dd13a8da9f81f52cff4c53924', + '0xe0aa2b3b3efdc3a99e1ca7372beed03b824f88d7f003c3b03fffdb1de178a05d', + '0x8a110d0260c6e46a78f6fc4d8622ba4b8c8f5e5f2cac9fecbe29be3345188682', + '0x193796a7054cbf06e0a4262e3e28e4fccf6a21a8b80075e4e692a3c8b36279e0', + '0x47bad9bd998731ef3c559dc63f95bd8f1c12d53d870d874982091ef1e80753cb', + '0xf7ee397971146aaaadc2575a10b5e978231e4457b70484ed0a12ba17763d142d', + '0x7d29ab857ea0d3ce32331073a1becf5e426dc03e9db480b9c9aa4ce3ba5529db', + '0x2747080aaa140c3d18d0351e922ed20ab63ebdbadf44026b4f69469c83c35ab8', + '0x10bca0dc250f16a175790e1b8307c991411d351532ed035539e34ad13f449445', + '0x7398f4dd903a5be0e1117c2433abd8477cc6e14a15d36c332722ee605d70d587', + '0x69f6cba9436e4f2f48129ec1a57325f46a8cb9c13ca6e81f7a29b667e1f700eb', + '0x7b15e945ed2226b486fa3da81090aa4f6e7296dbd799586643ff6437058bb7c1', + '0xd08e715819a94aa24e993e25151969b7c8f1f4e38fd35c30f9e31a30316941c4', + '0x09aab042b10398f4a3bb1bfd7b6b16567dd7bcd0e671a9a371cd59dd0ba704d9', + '0xb5db8739a0a6e97cb453d109e27f943353ccc865a916195e38cb25d60ecd8c61', + '0xfc7972d7d82c3dca25737b094b286e151548c5704122e7cb52ac45351ffc3cec', + '0xf7527d3be1a9354334e1264e530a2783fbc8daa92571b680636f9e50afde54d6', + '0x856a53df758186e954e657713793c8806561fb9e1033ead49639a19e16263342', + '0x052b227e1db5adce825e75c44b65a037d0a68d22f14273aa3178ee2b9bd9838a', + '0xcc7ed47dd46322ba4dd61f5c34b29a6932486d53a4a3f375ebb71ca54c2979dd', + '0xe962a94241620a73bd6557227002e22b2a20ed7ffbfd5ba0eb725c78e64b58a4', + '0xd4a3ab722b0c05cad0f4c4604cff9f35ea59cf8ed335777454af95f13a74f60e', + '0xe88592652967bc455de4294249ed76d427de85d01d64e001bd6990ad3620663b', + '0x48bc00f261aea636e95e8aea1d286d3b65cd5751b551ef8cf46a8bd3f8501667', + '0xf7e7e46654be9066c538cf7606c53f5fb0f5d7db9a8cc574af44709d53c10423', + '0x6b2e6b8222d126330c72dcc72a7b1866f9eede2809b79cf584527b4c81a301c5', + '0xf1cc2395a67a1ccf59ab0714eb66f4d707706a6613301acc43b505389d82f7ee', + '0x9f0eb633305494f773a558bd65f4f4b64e4564c8d81edd8bc6d738a29fd30c2e', + '0xdb08cc24e61b99da4ab7a510ea73b541cb48be6b48faeb0d4e0b7695dc9e0721', + '0xa68bba1ca522b9b05b191dea0e10340f1d48295a80c805c27200615962b4f1e7', + '0x19eccf6dcaf0eece2d597a4cb80bb4a21e5dff7c3ba47170ed225003a3816e89', + '0xe52217d4c4c1e164e262524fb35ef5b55664493e1dd57f6126234fe3f0cc1745', + '0x0d0e96c57eafbd95991101135eacc42cd1730e15e2184d24cd98edb78e382825', + '0x32035926d5ce8f7bd1f4a99d52d3c2587ded1194316caf9d1bf5d1c5b61e0bb8', + '0x9accdc963000adb5d008ee83c78c57a589003c7bef8cfc8df117e4ad83311740', + '0x7f4b91435297e8696cdd0f47cc9964edd7361d1b046658a9e439f7c42aacb248', + '0x725762297de1229711061522e58feb79ad2640f8ed04c1b46d08de75915df6de', + '0x9cea6085ee816562d64b44603162be193307b42708a4ff73f478106dc628bfd8', + '0xe272476bde4fdbb5ecc4b63844232e2fdd1fc47cd823780befb8aacf9223b56b', + '0x5f24f11ac42d09e4c36226052e96926250fb39af0f1a56285e4128b21203f7cc', + '0x3a9cefc0b74164bf5b86cc02ba3140116a86fcf5a801b11d732448c32903bb60', + '0x0bcd8f04122d9da6a2af177cef285048f67427e3d270b3f6b9727c1ccbd199c1', + '0xd8388989574a90579a16191f20409f823497808d3ff4aef1c8883fa95657f33b', + '0x814c6241e2ce1fca2aaa19b219421c9b0a42506204b06e790c65a730d7990758', + '0x05158805c28d50b451183ce02d0e648bf201cc23d249c7d018539c120c1f4d73', + '0x0574723db18d038f4e7a72af88bf78c015f349282e40d30b8d4706839415b246', + '0xb0f1d693f1b69649f0a09076d25a3ea75ec74d3cc8e2c1a1e9d7a2f4cabfac02', + '0x429e98b4b32321a40ffcf08ca0cfb18a5ce1b3e7e4f087f31aca3428c8e38e2a', + '0x06971a89dd3959947803403bee6a99950aecad0b1af4267c677db1b6aa9a034c', + '0xc4e75b9827971660d218e37f826ff1c9af300fd3fc04f05da701cbd319e0579f', + '0x3c2937c6efcba2e17ebe53197f34faca9561e89fbcf80fe710eaf9f00b3af2d5', + '0x09c10e10a865bbf7f7f77036f659d567eb03aa359cb3bd4c847e9386c1baaef5', + '0xba3524c519f6ac5ec90810d995e6115c47bff05cc31144ca04885eba8323f29f', + '0xc1e6facd3624368e16f8a4c124c5642f32aa39f6659397f352e72f3a873dea5f', + '0xef16b6e9f58ab9ce8448a31db347aef47abdd913aff9be134e858d33697f3ac5', + '0xcfd4bb8b7ed1e1c61553f49f5d818a0589e37b720aad8cb227fa615bc1c1b462', + '0xac241ca93c28e2cde02c3e0202bb970fc9b33e79ca49d1b723e1b69dfc001c69', + '0x09bcf479b6cfb96de663c8f0675912868bd81b685944432a9e173c606021ec64', + '0xae67c6d1b4fb378126b565a30a309b6d4e3889e65066cfbc36b278c9e77f062b', + '0xb9026abcc1cbd57615e05c4e4480a74fd6a0715d9a6ca781ad6e40e8a2b120ce', + '0x2f321a6e6560e052c3d6853f5e43446ed38e374b764d49f8715426762bf30a74', + '0x477966b45a4de0160b4d40c71f21c1d255afaab4a9dc182d8bf02e439e0e4292', + '0x6e69e6abea103589873cb985f3f0a46b3c3722e707bd44503edbfa92c023359e', + '0x57fa0fb9ed02f6639e39f8cd22d9bb40f9979b503bf2a934abe138f522e731a1', + '0xd12c73a5f8d32e85a46547ce7f6e7b9a4edd49f8623445710a54c38e47de77bd', + '0xa072dd42158364d80191fa0dcbcaf4a8b60644002f6d36eb90c0f47d726781a2', + '0x4d48f6750b38682d032055adbbe8bc776b075fef3759f74cadfefc43c8fa1fce', + '0xd340958601188c379b674e10b1fdc5eb4f80091d980bf9949ace31da7c09b97f', + '0xef91fc8b2eb06b2cc48f0b1b5bbe852aa403b86a6ce328a0649cbb50bf725987', + '0x3d9ee5cf464f2cfcb0bbf577f68e6687095834f8e30e3d2bd2d5d65493058d6a', + '0xc839fa7e818d90429a5466011a1f9143a31386d202dea3c04f33dd74d6af5a86', + '0x1068e479ca764893cc04758f5046f24285b2f197ba66e8abd148dc768e8bf7cb', + '0x4ecdb6bacc8de656c7324fbc4c0bcfceb68cc2af051c0039153d3c2859d3e536', + '0xb45f881916b9ab619d95f5445fbe0d967289d62816e43511cf64e3efea40e100', + '0xa83bfc3c79660b75ab5da1a73491077e889d6a2e409e845a43b1d73adc4a54fd', + '0xdac4afa56053527a664d7dc3ac911dcc257599cb14a8365a4b101c98e9989054', + '0x44e6d8a3046f2d5ab0eef4b7e9042973393ad731f9af7d2b6643f7c2e73a4551', + '0xedaddf0d415f2cf2fdf0d8ea7427fdc8f424df7aba5e400591ce09f9fb7e0e99', + '0xeaac1fba4f6fec3a2bb34a05f4222856f1f45566a0065075edba6c80b129494e', + '0x36c087c869e67167931d701c531aa36438e216c85519e69aad91313f77420608', + '0x5470bc2ebfacf5e247c05c73ed3a7e92f8668f946d6b20b47fe48e5643eb2e5d', + '0xd7f04c73fffe2f2b85f2a2f0939c638558dee6dad365798c481aed71ba4cfa5d', + '0x119de40139cb5ecc7f4735a5b4eb1f8855d1d08db54bd1583a16719eca2fddf4', + '0x5931a37f0a3e2e8e8faff34d5810ecfffb5b038f9fef5a942706723046fe5060', + '0x19b625cff7ae6fbd120bdf8702747aef144ecffc1e4f8788ecfa47c527bd9ea2', + '0x9accb92b1cadc8592f4c8dd7061b68ae27fa742a0679a49c42d2e68af9daa89f', + '0xd018632c946f26729f624162690c553dc38f03c772a8863d1c4b7d123eaa6950', + '0x6f223f98880538b2af75e4754a871e251f6a1ffe41fcd61346e58d3405239812', + '0x6e7c5c56bbf1cafc5d8e2242249c16a25fa725fc9e6f740135d3bb798f833b4d', + '0x9f3897eff0f8e01ca18187493bfeab1793b6f78a9c08af16e4ca98b9f0894710', + '0x32556cb742c4ca617d8eca345bb70ff0bfee095200893c1cd6d9478b2d4dd0ad', + '0xa9f03e79a7cd6e1b5c898fe2ecfb6cb5f5c1d05bd3a5ec0c6bcd83da66fe3c65', + '0x7a11513b5ee188b5c1adb59bec07153a6e1d3b323cfe823c8f6b39fdc85ddf24', + '0x9706adc411f056b1b4693b139bdb149cebe0bafd3e4896f8c5c56a620f6b5d3a', + '0x044bba94a72055cf10576c7c695c9c7d3305a28d545f03a9ba29d819730b1493', + '0x0551fd903c7b64e061041f3054f371c5df2157773bb1b6ddb4c247a8d6afcfe8', + '0xa00c4205dcb2e5c4dfb6b20fc81d6b0f4ec2e1d79598100d0d5f9dfd6a4dbaf3', + '0x82a944140e659ac2a06b5861a9257672ba2067f23bfe0ec4c5bebc0cd81f1f6b', + '0x2cf0c38c0a45f1ba5a6d677744747701d0d07e49dcf24e56dbfde92bfef68441', + '0xb28146d02fed330c74aaf8f9d4d182d9260de7971f33ecbf4a7c1a094d800886', + '0xa7aaa83cd091152a6b626493b74b873411453e8ef277445ba8d46862fac667f1', + '0xa99ac56d54ddf28c30ae3ef129369246cb2bae89fc54d943611e560ba63af475', + '0x1a362ca5ae721ffd33ad5d1c2494d95137d46f38e66d5de5b16786f5a2c2a664', + '0xbcce30923a817b2fd17200ba8a9a32d88a1ac55b64524dd2738e2799b60b7649', + '0x74f3691caddb42105cc7ffdfd79d506b2c73a7997647d0f377209f7f57a89ed6', + '0x0792576ff6a75321ab3448b9b6fbb85fcc2b53a0f797e5a5ea0a5ab79cba4806', + '0x55a8e154093a8140c9f01bc33b4e0ad8c497fdadcbdda464c261a4862fe3218c', + '0x44ab518257f27d6c9fbfd5dd8f127d335a9830dfcb84592f2c8efec709b9b2ef', + '0xd6216d02ac29224917cdc34e51eade6d00060c9ca073199ff1c42e29f99285b7', + '0x8fb9a01c83eba3f174910c330aadff7ff7b2e26a8c1bbbebe04489acf4913240', + '0xfc19372d1349fee34a5db2063f4618ef4ec3de1242eeba2d79153297d0449366', + '0x58f853d88f8f3fb250c9b8083002ee75b527c528cb9fa4fd11d07108c9720d7c', + '0xa85722e5bb4bf4e42dc47013b0c0837f6705830507207849a3624afd7f9f1832', + '0xac28a06292aad999f9c9de082e6b3c5c94bb9453fadfd03bbed57fb7ddd36aba', + '0xef385aa4da8085065941802f853c30de86bda7c0751c544e50163b9af744b562', + '0x5ab408f4c17762892004285fdb2095e2d97b50661fa770d0ba92bee534c92924', + '0x73bf6c09fa9f71001139184853eee6695e0b8660b4385cf44546669ac40ddb2b', + '0xbf08e2850e0800f24241167af75556b3de37552c6e812f6f88065eb248286386', + '0xdebfbf5d933deb047f9bb23f7f3f6984fa71c0310a0f6e93f25b0216c59bb126', + '0xbdb45a48f1badccb10496d9b1fd89cf2a5f127ba7fb51572d6d893202b0d22f9', + '0x41a0384444b998a2b067c0dec4d8b163649d2a73b28f3c87640aed1c027716e5', + '0x2bd541194105a0652287f4e5ab9896a3dbc95aeb9eae3a2604f8142385b6a7dc', + '0x58c0a2ee23f506f3cfe911009a316775037d68984da8256947d3611ed7429050', + '0x799f185a61133549f988bb545467d84f28afe144f8f867eec10dc09072dbaf03', + '0xaecc5d9770d2403e38b5dfcd646c434962fc8f35bf7d3a5cd721c688c92890aa', + '0xa34b345b4e8e424c14dccda06e2c303d2db72a4bdea434f794626767a4cdb1d3', + '0x3fb8ba04fc57038208eeae7b302a586319a29020f4daa6f27c61c2ff34e70b45', + '0x3a8bb258775e0370e7ecc30515106790b9dc47412bc2ce6c7961e79821832ce0', + '0x4149b4e2525af0ffd0c7d30e2676838353591abf011721158d78824a8d212964', + '0x0a5ec8da07f59df12f678a0259d165aa7c577e176eff9e4e3e7af104ea38b361', + '0xe01335e25f32678d411109e8c5d166bb9ac05f333987eda9a22b078f6cd79494', + '0xda1b062ab2f0746636af3e9ccffe8d87784835b99d1b7603d26e9f512470f50c', + '0xbf9b14b37570148cff331b946c402534dac5c2870b42b4e78b29787c659f1e01', + '0xb608e097e82ad20f0a0d2666b267ea467a7b2216b86bb50eea145ab4e7536426', + '0x578e3074e4dde36f81bf4f6ea524a5cf00dd20c2091463479d41ceea2096dec0', + '0xbad1c6ce85e7695cf7d6cd1c8b2e65a83cfe0adf37e9052559b4905c537a245d', + '0xe1a32fddb412ddea95850ad652cf644e137eede1d08a96ef10c1a26db8b6e22b', + '0x3ab89590df7e198ea14f96e71ad82eb50676c5d920a7c400f8dd957a554506a8', + '0x6dd0b06d8b9d6f89a78da4e10588b66eb0c28e8daf2b915c1d5ba15650b94a2e', + '0x8eb0ec04f2a0f8045cd987ab4aeb87c8ce3c2ec40042e385e36b92eefd7abf06', + '0x3e8a4061daf3d2496f5b2ceb7d05e2a12b327071f3ad28129e0dcdbec69a426f', + '0x177b7d7f9a2a0f9c448b135a205e7b688a430c88b333ba3300630473bdc1fcb9', + '0xfd8260a6cddb08ef3def644fd9d7eccd3805d0b91372745f28eff6b7cbb70483', + '0x030bb3bad5c6f66066a1ade5c5af0da7e74e2397757e407a3672160c6b511a12', + '0xda8f4909203991c657b95d1f5d2488a80844c2ef9d62127248bcf5a4ecc06545', + '0xe4261c194ba8eb412642f8f64c33d49de8c24bc54146a3c8f0585309a4d0c778', + '0xd7a1e40b425353d103b460c04c0b07367de146194ae04aa53b572a652824bbb5', + '0x7524d77df781970f29f481df2a2ee575205c7c7a6b336b9aaf8d2d04afefd4f4', + '0x89aadb2bc85a8ae14c309c8ccd8a00180d3d6244b03884fc7b983b164a109cab', + '0x4c56148f8c21bedeeb20f04a6af31945ff68290cc8d55ea86960f3de9faf09ea', + '0xf7fe0b7f4d8932de565e1742bbc5ad1496f32a6ec075837e3faa07bccdcd2479', + '0x2c479667bebdc88b8f349d3a3db7700a899170abb9155f4d526bc3b6521b32f3', + '0xf77a6ab9e4882f4be2b171e98c80835cb188b67425d1fc3ce73ad84375ad97f4', + '0x24b3c86cecb45ad7fc9bca0dcddba7cf41f6a3da7d64bfb85401f412ed4629d6', + '0xee231d36c90f6dabcd1ca51472ae392fb93918e4ce7394f4888a02f6850c6166', + '0x6ba4c80ab2b70146b6a5997b4922be4e83c5e028b0eb2dd431646754fb764b3d', + '0xa6f71d865e8d47fb804da2c3f2a42a8e6f517adab6ade593e2298c145603fc81', + '0x7369e84f16bd0afe51dc906a93fc5ab58b969d622929492b63c54a51d6eef36f', + '0x96a35de5c5ed9c7e2874d67d8717e90eddd65bef90250f79d890539e515e55de', + '0x3a5bd62ff9a0a0ec40f09d0020420978560472ff9ce83f409ed12ff10fa19a21', + '0x6d5c2be9fa4b0ba05ab8ed6b1921b29cd0f1882766af99d71e25c08320c54305', + '0x29d90a8a5ceafc749e99641cb88f7c1f83b9c7f95d0a0b64b474a1823d6ace07', + '0x36a8fbb1fb6baeb38b5606d3b2749ebadbe471927001b3e7d8256d073dc9b9f4', + '0xe31cee3cd95a961a7c7c71934efbf4c194de0a6789e56314865e4d181bc8d0ab', + '0xbf9377752ad013e8e5edab9ef391302eb7bda09ec84986ca27d41cedbb87d5ab', + '0x79c4f4d27994953ff8a57859f0dfdfacb092fef2e8d12fac3c4797b28084298f', + '0x22502748767ae69add877804f8c0b3b6284c381708ae2297a02b615b2487a6ce', + '0xf1c33ded14735e52d535858699949aad648c14abde589b53249e22483cd09279', + '0xc33e968c21ef92e26eb5a6014eca85b99bf8a57e1d6ee0a6d1ad4ce43b7c062d', + '0x3553e67b438920909537930fb72a22e1bc89b98ec1e90458fa544ba468534c15', + '0x0242bf1904dc28b2e785a6da65971a02fe23d91aee379d56cafdce4b9a8fea28', + '0xda2717d3e4c6f4465d55972a9641abbefb8963aab871582429c5fb8510042210', + '0xdafde42a0d5d24c04a44bc61da1f1ba97d41fad7207d3e2a280fd1f66cc61731', + '0x437d9c644114b1d9d6a7909e4fe92198b25697da177ec9d66517a07a82899e07', + '0xdc46ee3a5fbcacda94f6d134125860d53c8f91b34e2056bcb11ec92571761c6f', + '0xa629243ce5a3e2ba64b7b528acae3eac6ea3a0ff3b128d0a22f8975ce66dd410', + '0xe5cf7cd5da89a69aa9bba29019181b252f63d24bd71e310217339a0aaba1cca4', + '0x764d42f689832afc94d3a09ffcf19f0ca61b5fcfbf5444cd22574bfbb34eceeb', + '0x4b7d6d36cdce3f71d5f6e1be747adc31051539435bc60a22c6745e6b92efb9bc', + '0xfc1949a19004999e3d2dff0c2d40273f66fcb12252f62541978d70cb17d337d8', + '0x48f18510848adb85520f4d9556b2929043827037c229b752ce0fef1c085694d9', + '0x8f462ad4ab82152cb0ee0931e30835414c240ae5294c9a8ba219abc3c096a634', + '0x51ce6119a4537d645720c22cbf90a24b7297f564da95587af5c7b1a35299e1e5', + '0xa9eabc7aa5533b33a838aad2aeabf33efaa0cfc75d112316c30245a3ddc5dba1', + '0x45fc89947a696ca7d2fd7ff71cea6a1df18c9dc2f03f8eda245ce8ab7a35658a', + '0x2eb9a7ca384c0344f598fa90cacf3265ad1bec108241b58249252912e0408986', + '0x80b864e948b5bbc7e71e3d662bc3038bc37e6dca21c3777c1c511bbd6165181e', + '0xc07e39f09dded2e0caa1e8eabab200f98a43fd02ebe1ef3593c27875c53092fa', + '0xc58ed564835acfba5cdb526dc4773bff6c9ead95f9b7aa1760c46767f597fcc7', + '0x6f2e64d8c7a00d82721689f0621b806ef96a76293c42e0afaccdec5092bd2dd4', + '0xdf2560107d05033cd20145db53c9bd9cd38450d8c680b8f41ff4b03aa7acd49a', + '0xe13cb45fdca4db232c5c49dbec0557998870c1726c433e340e7e5c8a32f7d374', + '0x031296bcb01da02399e37cc381f87b14f1c6678273b5e5557315efa1e04753a7', + '0xca3b3f46079b8debbbcea54d38fd8b481671b1699fe6142d37852592c1c6db64', + '0x1bc7545eabd83cf0f59b848a2adf269bd13eb70e9ede9a38272ea89b1446be99', + '0xd0aee813335ddd258d591ebfe252d39d87be23d079aa37e80edbc1ee00e4eb0e', + '0x3d101ef1220b9fe9b5692e2fa67bcd5b578291481a598096f1fa99dc277755f2', + '0x665ad0fe4e94ea2d254be7e965adecb5d5b2dc170b37c8f74df7d6749d13fde0', + '0x627235f924b2d61d6c4eb9b4b0c48abdabce092f0cff368e2a3873096a5cd6ae', + '0xe960b6d5610e23d0585c6ddc515b9f6b9cd95337d9279a5821b60499edfdf1d7', + '0x39dbbb4c54dc114bf7f6dd1534c76359c8c33c925b37980f9d3a8b232aecf0d0', + '0x93306011c95061bbeb11de9e72d7add21b78e41b143d8136d63088c5673e15bf', + '0x4bb5a127686a32bccb354ed4b029e525ddcdb00a5eb5561285e19c8f3102f753', + '0xa00d35e61c5fb33333b9c7404a09c71d01694a9309a8b862ade4ecf55ed87f49', + '0x3bd582c7e75816329613a71c930af381be11761ba36d0e4e8251c5a0c4fe32a2', + '0xe8e1dab5cbbaf4000f4b203fe99145f61909d5feea32ab9cba1e300bae7fa99f', + '0x78dd130d7b6e0ff5b5a9bb11ca57dfaea7c6d11c13a54537592ac9d0ee1e1013', + '0x88e68d58e3f5917bb9592c1a7b89c9852361f9ffa0854dd0119775e4be8a739f', + '0x9e2b6c91c28d1b3f826da6936e9b38a90e82e17d9a4d31734a98a8e98b4a0d95', + '0xf2231dc224d9eb57f72eb7b198ab8c5be14a052723d64b91bd86f412b7fddc1f', + '0x62c5a27756665f34f3f88ee0c9d63866ce471a13e3dcf3a759db0dce9fdf143f', + '0x5b5d51ba64f42e6d60195e9cbe51363659cd84baf9865bbd7f027ab65f575c5e', + '0xaeeb5b59aa84f3c66e875809d24b62aad131d1013ac50b3fa89606f2473addee', + '0xca512c0cfbd93ed5c31aa09af1f2cf5edf075a90a5e058726f558d683d6d4ab1', + '0x9a6b45605c4625c8e5aca57e556890682944954ed1e4baf9293ce61a69df14c0', + '0x3c8a0d1742b4b49fe9e91256db8664ccc7e92f8e6d1ddc7c1b5ff6103f8cd9ac', + '0xe5d635d341fdb6dbec17afb5f0cca7a77e13e8430dd59a04aa4105bccde17cec', + '0xa5da3b4e409f69a7156258291da7952dbb9ba73231ee5158f51dee3446381e1f', + '0x6765b2c49263bad9a72866379f6da1942739f39a5fb70bb0b85c76c64443bc81', + '0xcc70556cc125837c2b60cb6109f0223c081240a070de9f70b203a81a56149877', + '0xd8d1634f52badd619adf824b5e9ecad0f152b6fde514fd30cd4a12ebf2c77344', + '0x4303a5e776df08cf2caf1cf001e809fa406abb8bd4e30efe315bb2ea9d75a590', + '0x31b5d1700b3ae3972685dfc6e85f0ae2ca5cafe451116aaa052fb8b23483ecec', + '0xa25f01d667d135d49263718f469691cb4030e85a065990cb0aa407a045447779', + '0x5ef4dfedc64cca17daf38cb0c34e38b02557672309f7136c0b75f2d6de3240c6', + '0x8461f9771a2affcf8b70fe6738c129b953ebc450329d49becc2de0ca4b23f750', + '0x9d8fbf1a43e3d59eb4bb99598b47bfdb81ef25dfe6c5d80cc44092345f338c11', + '0x434acecdd544aaf22dcad1f21bc60386e433187c71fa3e0381c32abab77186bb', + '0x2a77c426c095a341419146189f4deb2d74d15381f35df93e1f8448639ef18f03', + '0x8746be7d7de5e656a381d16b8a1cb0c2f45bcd514a02ac80884e1af6e91d5246', + '0x5edc07b6d5b5a57ac0faec9cb283954189565dce02c26ad08211d9ffa56c84bb', + '0x792b3dcc3094f7405ec61da02f977c2b2b5194bdfa62907a277923c855c2caca', + '0x19620712734dc5664fff560537d6f6df0a7a43cc8532bb396b1a2c74edc55cde', + '0x4158e643cebe417933e93ffb349c059c98438bddb71179055ef1e49ef4897edb', + '0xb980d5406ed9105c602c89c73f3d2de224ea6041240428135adfe5e1465e398e', + '0xf96a354b6f3ca498b8f25bf3c76bcfbbae78c051ad6596882811e4053406316c', + '0x3b2c58a1b3bb93c17331f9172b9f2b494d0c5d35b594d30323807aef5efd9d33', + '0xa12a581db2f673a7dce27ea514a182898e0329235a44387843a1e98dfac83a6a', + '0xc81db8366061da9c83c6522e6f57e450bc3136f72bb3cde175956cac5d3c5988', + '0xf8b4e0c0ad46c9c8bbe34d198faee7c37f6c8b4635ed7db14cb9084db85c5f3e', + '0x49fde0b19ce6eb03e3d592f8a599aed33ab3ae5811834af37bfe99ab9476d1ff', + '0x18555b454eb9e11538fc14049fcdb69445c5bdf25d350b4cc73b8418c89488a2', + '0x5a95135d540460d78e60234e4ea4a3fbfe0dc6530ed20bb6551f391c58364e26', + '0x83f3f224e2c77b457ceae501bb237727141feb03ecb34789a3decffa667b8f8e', + '0x53942c6b953f3903b1bae1e55131e2b9f409e2e900a919f4baee384471c5fb60', + '0x6b75634d1f134e27f87cf38db4442c5c19a037e00c1c687e125aed9e7f7a2467', + '0x0054e0f24689fe8758a821a00a05653acac76a92d3542d3f6fa3e01a7fd3d399', + '0x6c08147c1a4a4be7b8e689721589287b05cf161c62607e9c6bd91bf3c8cb249a', + '0x16276cc50334e3d3ede3d83282450ea5706de229f3a910d78b28e1d32fcfeba9', + '0x3a6084a56efe718c411f1cad1d19e9d477eda8091321f64112465e5164790279', + '0x07f1c9d7ee59bb31df91ebe5f5cb89284e23117b6e5d3648095b695cb77d92aa', + '0x3a177ce6a9854aca88e7f8fb3f3241f2fed96a8e3dfc58e48058e62e6fbf4dd5', + '0x3732818df6f504e949ac4111daebdf66df47d2dca4fcad53c3aa5d3828249d37', + '0x995ad10e9ff9e2c06064b2918d0f279981e4e6e60fc2a44062e290fdf6792b33', + '0xeb31b019502f21d165d30e95a3197a7bbbebb34780fbd165523e273500c470e1', + '0x4e82240bba88f57d595f0015cbf7b47c46a3f3976535e5638870763e7e469948', + '0xc05545c94df8c7b04e6bef4823a7d433691158dcb96924410a28c7deb3dd0708', + '0xa915c9bb26a053325d3aac7b238ce91706eb25bfe3dcfa38317534a32759dc22', + '0xbd9546a4b8104aa31f0cb13ca5c5908853f8dbf234eeac1ff046c8961dee9e98', + '0x4a26911c649c3de55373c48c9a48a848ac9fba67ebdffd307e45f991ddf40e6e', + '0xc5906a5edbc4d7e86199c5525600e939406b6f594b1020656f039fe0d8fc7f9c', + '0xdec58969142c3b766808ee5e47de94ec2bcbff37da8a06d971e20795006bc252', + '0x3a3b39dc3b38739e5571fa44c5280698458facc98a28bc96188e7c35e91a1a38', + '0x3aa93ba61124dcf6bd19c16c61b7c62711ce5955f001bbc67fe75a93e092e35e', + '0x067007537b503c3414f9f8e328a1b7353110b14861e374c6d42e432e5b5472a6', + '0x0f8c5a40023026f8e7a58c9d59ccc40e1a745c8732edcf1d5e2e1abaff7aca4e', + '0xe2761e88f38d536a7473ecf27998874f5857e44f3c06becf7747fea09216d5e7', + '0x1654b70c6582eb553f81f6c8d3917c0cf2bc6a1e06dde3ae64d966a58e13bce6', + '0xb5089766b015ca66263695ae50e705ee917ff93c8799a3db8338cb12ff84a3da', + '0xce001c26dc604b83a240fb194ca3435bf4b4825a31bba5183f1bfb77492003b9', + '0x50a9da4d14d8f97c6a166c5e0e256ecfed81a4afc005f67dffb52f85d3f528f3', + '0x967c021c09e23dd777a250f167518ebebd232ea2e2a63173075f631563a44978', + '0xa3f8fe7cab9ebf3bbf6711704f65afaec9cb75744bd098770a70835a6ba5eed8', + '0x44741405982c9a87d3758930b4b51a649700ceccfce0ff68642f810e280c3392', + '0xbc4777bdce57f572db079910be82cbe6cbb8a751c2f1eaf3ce8406678919f107', + '0x473325dd009f7ce64169c7b4d220dd473723f404a549ea2ea1fb19d540815a94', + '0xda841a8bf1d8505c585d045aff3c0f2e8bed6af0933bb820395c20942bbd30bf', + '0x7da907b1f5413c4a1ddaa76b4f52319f30978211002d26435eb042ba76f1276f', + '0x702053a4f7d00b5347d9e51fe2848fd1925d49c6ad85a06053ecac15dac6cdbe', + '0x9686c57251c11661439d2e6c1c8cbc07b043a1f82e0eff4fc02e191a7b62653a', + '0x224efd67d481074c83f05955c0a956b8f73de135b1b125d0d75b655cd94ff48b', + '0x9ee1cce0ad17812f92d4dbc5f6706514a80ab55d572d2dc26a3d3b052bfd8e46', + '0x5f808f1dea6a5c868b7d1351b443a6bbfdccdd507793fb95b1cee0f8e8f9aaa5', + '0xd2f672257722f5e42961663056c7b340430c37bf868fc440e81ff4594b8d4ea2', + '0x721f30048d29db878f3f43ae8ab5a67bfd5026e232a59d425d504a8f2b3ba8e1', + '0x75803567400aab20282d7c35c73f45716c526331a77cb49b77f5841e5d4e3e70', + '0x1e7a9e0492fe9a198047f02699eef05cb19641e9eaa0e95dd9206a91094ab12b', + '0xf3ae4b5bffbc8cb58315385d6f0fdc2d133a5bbc1676b9c9d19baf584f76e775', + '0x3d52c0c958ff238520aad6d62f44b45d4adb87db030e7df0068fddeb16b9f876', + '0x3033ef7c1df0025319d82fe1dbcf8ca2f2588e4f7a2fad524ad488f623e022ea', + '0x242dc765560a67f4e4044e08178e753d1902fb704ac7f1915d669868851446fa', + '0xaab360be2560e4fd5dd5e4e1e028c5c9bb6f7ac7c5acb01e0d022316c5eff493', + '0xaffebb9305c90e982801d9b90d15d88bdde28de49d9a9911d248255faf1702c9', + '0xa7fce71f799dffba8957d5be1908b598cad9edcc277b24a2dd79ee91425d20e1', + '0x637421d6411b1d2aaf7a5197b5ade4beec6b5beaf87bdfb15012133e3c5f1755', + '0x208fc72529cccdf41694ee6557c16f2b42b369f9cc9593504c0c30304a4c44d9', + '0x49f90e5f89f5e138e3257e76b3491d56d8c870952c2951f55e75ac6515a1034f', + '0x6a5fde5a6dd419c478ab08d090e29d6500ec00b7f966ce48f882d4784d7e4aba', + '0x3cfd5a8169d1b70d689aae73c32f632f77032cc1349850e720da703000a70106', + '0x8218739d7faf35439a3c424cdbd57566316c4cf18bad6fcb229cfcf2f0d280fe', + '0x11f998cc78b0a11eedec5c2721f719e5c4fa444a48deb43802bd80cf9e7ca1b6', + '0x378baca4e2d3b1772db988f4db5ef269535fd1134f606f35a98ad4a46ca2ecea', + '0xea2606990ac2f8d863ff49e9d58e25a158ef09bf6fe84bc9e3690d79c4550023', + '0x5412f1267dd06eea95f5d5917613c6e610870c2c65879ee2609eb13129448324', + '0x51696ea2dcbdb4de89f0db26c015c7e3c991d511faab126c5221177109f893fd', + '0x0415dde0c24a77334ad9c29821c3de0fe5859c61f09f413cc00f4212b10b4419', + '0xe2bb9751cb41fcc05ba942444073c341538701dc0ec1c233eb41b7b01d7bb1b0', + '0x87c44b99ed51fbdb22f0d529aeb45bcd40feac801811f5a73417f580dfa599f9', + '0x9400167d781eb70192983a003af8eda27c7abcf98e26d1999baf0b113717b031', + '0x07fab0635e408994ab7cbef0fd59a5a1f7c4dcfc11e04b469db040be565089e9', + '0x21129a4585401f3bbe29461e31493e403dcb1dc4b0f3d1616b30f96ed50dc072', + '0x76a682a8e2e55751de91635b6ae4aa16edcf5c92d9bff8ba38074bfe579f3d1b', + '0xce610adb504b9669f221ee951d7125c35a98955e935e8e8189aa097b4332fc74', + '0xc1da492066f1911ec123dcc3b90fd8742ae80629a8d2be1f071ee439df0fad36', + '0x0aeb5abc5ac25e53b4ce1acd8ee31944a671c0bf999788dfd66026bfd824c35c', + '0x0ddd552f60d99917bdb295310a07b18d4979b63be40babcc3882c509b89fcad5', + '0xe9fc62fde021320a620ea3cc229f6d81d7ef3bbdfac638e6a7fa5a9efa53f7fd', + '0x115d56f83d033d0342ad29c5e611f0aaf0fda946ea8df88557c54cc4c54d596d', + '0xe57a719daff2a6ad5cfa3cbcfbd6fa2c240451f03e31606bc1f447dc2e05a7c1', + '0x28edc89d848fb380f2d77ac24a5d51a3ffadbac384750c493dae7900d0408fa6', + '0x9d45ead11495b1dc370afa9437a9ec8cf04c668dbca71381cb0c17d8d413b7e3', + '0xa97f3acff860ff73cec3488946117cb6e7ff1582c715eab2b2ac784e76881007', + '0x188a67b979160d3c690aa44b2a9c61f3749866fa446a8eb08c5efad429328104', + '0xd5a127ff4828f9409819ef9d3c68ddb2542a86cb5e4b26ecb1ee3175f957f056', + '0xb1801e611dd2595d7eea1460a1987696af1acfcbe595e4e49683c02265360596', + '0xa2973d1088e1aeb252a119312149e165105eb2613da1373c2094bcfa84307cb7', + '0xdd9505703e13a93c326c1bcb985df44681f45edb36a28f2778a8dd19d3be4862', + '0x9d659b3d8cbb302a03407abc87816b9a35800f60d8ba57c1edd36bf5bea9a449', + '0x01d91571acfe7280b9c4fa250c1b17e56667342e8a7ef6a82ad30ae4a125a82d', + '0x52b5506e3ebde360635b1c2a8cef1a0ced68398867a9b775483097b25c70e900', + '0x366af927a2447f65d3eaaadb61d1db4aa066e8dcec0cd37a356f059b15104762', + '0x28225c34d9733d24ba72aa788de08f8c0aa455c5e332b242474ccac323172b92', + '0x2aef043594846f9575f4bf802de495a878b28858688b9b1db341e8cd8e536699', + '0x2fc3b1757b7c316ee2b59860b35101a129048b62c8a1d387451e46c612ad7259', + '0x69bffc6033df35d200d915fe4394381b855939aba7251edfeaadabb91767f1cb', + '0x4262bdd739dd0bc51cd2554482cde22d6e3a3440fc99392ed14a669b0854f4a9', + '0x0771289859d1f94591e4020ab7d8f02e7946800590934860230dcf59e581f9ee', + '0x8ff8a593d0a68428e66feaf570a22a36956ac285e2c688700e77a593a9edc61d', + '0x2a1af1a05d6a6c55e54e1a8329c4f91acb150025ba193d6da9f8188b2594947f', + '0x9d1f43be39fc57f261f1e27bdde1cec817eb8d4cb6b49d04a0920b08faf0aaeb', + '0x686e16c3cc1027bd7ed634c09c4f3605001d7aae32f38dec0fe42eb417d283ba', + '0x493d40f340fdf70a2d967f7af4d76bb3ce5b2166a70a7bc2bbf58d1fc461b686', + '0x7da118149f80b412dae2aeb1e23f975f128c299505da315c3f637ae88f01da7b', + '0x372705225cb4139530166ed9cd4c013b53ba8b4efcb7bb05a545e59ec634e962', + '0x69d7658d4a356090383cc706814156ca12d6fde655ff66ba87694f6ea314510b', + ], + root: '0xe1108bc2d965f0c397fa2a98a7e107a7a319ddc2f1c005f5a8f99ce598b34b58', +}; diff --git a/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.spec.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.spec.ts new file mode 100644 index 00000000..67152621 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.spec.ts @@ -0,0 +1,196 @@ +import { digest2Bytes32, fromHexString, toHexString } from '../../../crypto'; +import { DepositTree } from './deposit-tree'; +import { + depositDataRootsFixture20k, + depositDataRootsFixture10k, + dataTransformFixtures, +} from './deposit-tree.fixture'; +const MOCK_DEPOSIT_COUNT = 0n; +describe('DepositTree', () => { + let depositTree: DepositTree; + + beforeEach(() => { + depositTree = new DepositTree(); + }); + + test('should initialize zero hashes correctly', () => { + expect(depositTree.zeroHashes[0]).toEqual(DepositTree.ZERO_HASH); + for (let i = 1; i < DepositTree.DEPOSIT_CONTRACT_TREE_DEPTH; i++) { + expect(depositTree.zeroHashes[i]).not.toEqual(undefined); + } + }); + + test('should correctly insert a node and update the tree', () => { + const initialNodeCount = depositTree.nodeCount; + const node = new Uint8Array(32).fill(1); // Example node hash + depositTree.insert(node, MOCK_DEPOSIT_COUNT); + expect(depositTree.nodeCount).toBe(initialNodeCount + 1n); + }); + + test('should detect problem with deposit count while inserting new node', () => { + const initialNodeCount = depositTree.nodeCount; + const node = new Uint8Array(32).fill(1); + const SOME_UNREAL_DEPOSIT_COUNT = 100n; + const isInserted = depositTree.insert(node, SOME_UNREAL_DEPOSIT_COUNT); + expect(depositTree.nodeCount).toBe(initialNodeCount); + expect(isInserted).toBeFalsy(); + }); + + test('should handle detailed node data correctly', () => { + const originalTree = new DepositTree(); + const nodeData = { + wc: '0x123456789abcdef0', + pubkey: '0xabcdef1234567890', + signature: '0x987654321fedcba0', + amount: '0x0100000000000000', + }; + originalTree.insert(DepositTree.formDepositNode(nodeData), 0n); + expect(Number(originalTree.nodeCount)).toBe(1); + + const oldDepositRoot = originalTree.getRoot(); + const cloned = originalTree.clone(); + + cloned.insert( + DepositTree.formDepositNode({ ...nodeData, wc: '0x123456789abcdef1' }), + 1n, + ); + + expect(cloned.getRoot()).not.toEqual(oldDepositRoot); + expect(cloned.getRoot()).not.toEqual(originalTree.getRoot()); + expect(originalTree.getRoot()).toEqual(oldDepositRoot); + + const freshTree = new DepositTree(); + + freshTree.insert(DepositTree.formDepositNode(nodeData), 0n); + freshTree.insert( + DepositTree.formDepositNode({ ...nodeData, wc: '0x123456789abcdef1' }), + 1n, + ); + + expect(cloned.getRoot()).toEqual(freshTree.getRoot()); + }); + + test('branches from cloned tree do not linked with original tree', () => { + const originalTree = new DepositTree(); + const nodeData = { + wc: '0x123456789abcdef0', + pubkey: '0xabcdef1234567890', + signature: '0x987654321fedcba0', + amount: '0x0100000000000000', + }; + + originalTree.insert( + DepositTree.formDepositNode({ ...nodeData, wc: '0x123456789abcdef1' }), + 0n, + ); + originalTree.insert( + DepositTree.formDepositNode({ ...nodeData, wc: '0x123456789abcdef1' }), + 1n, + ); + + originalTree.branch[0][0] = 1; + const clone = originalTree.clone(); + originalTree.branch[0][1] = 1; + + expect(clone.branch[0][1]).toBe(142); + expect(originalTree.branch[0][1]).toBe(1); + }); + + test('clone works correctly', () => { + const nodeData = { + wc: '0x123456789abcdef0', + pubkey: '0xabcdef1234567890', + signature: '0x987654321fedcba0', + amount: '0x0100000000000000', + }; + depositTree.insert( + DepositTree.formDepositNode(nodeData), + MOCK_DEPOSIT_COUNT, + ); + expect(Number(depositTree.nodeCount)).toBe(1); + }); + + test('should clone the tree correctly', () => { + depositTree.insert(new Uint8Array(32).fill(1), MOCK_DEPOSIT_COUNT); + const clonedTree = depositTree.clone(); + expect(clonedTree.nodeCount).toEqual(depositTree.nodeCount); + expect(clonedTree.branch).toEqual(depositTree.branch); + expect(clonedTree).not.toBe(depositTree); + }); + + test('branch updates correctly after multiple insertions', () => { + const node1 = new Uint8Array(32).fill(1); // First example node + depositTree.insert(node1, MOCK_DEPOSIT_COUNT); // First insertion + + expect(depositTree.branch[0]).toEqual(node1); + + const node2 = new Uint8Array(32).fill(2); // Second example node + depositTree.insert(node2, MOCK_DEPOSIT_COUNT + 1n); // Second insertion + + // Now, we need to check the second level of the branch + // This should use the same hashing function as used in your actual code + const expectedHashAfterSecondInsert = digest2Bytes32( + depositTree.branch[0], + node2, + ); + expect(depositTree.branch[1]).toEqual(expectedHashAfterSecondInsert); + }); + + test('should throw error on invalid NodeData', () => { + const invalidNodeData = { + wc: 'xyz', + pubkey: 'abc', + signature: '123', + amount: 'not a number', + }; + expect(() => DepositTree.formDepositNode(invalidNodeData)).toThrowError(); + }); + + test.each(dataTransformFixtures)( + 'actual validation using data and hash from blockchain', + (event) => { + const depositDataRoot = DepositTree.formDepositNode({ + wc: event.wc, + pubkey: event.pubkey, + signature: event.signature, + amount: event.amount, + }); + + expect(toHexString(depositDataRoot)).toEqual(event.depositDataRoot); + }, + ); + + test('hashes should matches with fixtures (first 10k blocks from holesky)', () => { + depositDataRootsFixture10k.events.map((ev, index) => + depositTree.insert(fromHexString(ev), BigInt(index)), + ); + + expect(Number(depositTree.nodeCount)).toEqual( + depositDataRootsFixture10k.events.length, + ); + expect(depositTree.getRoot()).toEqual(depositDataRootsFixture10k.root); + }); + + test('hashes should matches with fixtures (second 10k blocks from holesky)', () => { + depositDataRootsFixture10k.events.map((ev, index) => + depositTree.insert(fromHexString(ev), BigInt(index)), + ); + + expect(Number(depositTree.nodeCount)).toEqual( + depositDataRootsFixture10k.events.length, + ); + expect(depositTree.getRoot()).toEqual(depositDataRootsFixture10k.root); + + depositDataRootsFixture20k.events.map((ev, index) => + depositTree.insert( + fromHexString(ev), + BigInt(depositDataRootsFixture10k.events.length + index), + ), + ); + expect(Number(depositTree.nodeCount)).toEqual( + depositDataRootsFixture10k.events.length + + depositDataRootsFixture20k.events.length, + ); + expect(depositTree.getRoot()).toEqual(depositDataRootsFixture20k.root); + }); +}); diff --git a/src/contracts/deposit/deposit-tree/deposit-tree.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.ts similarity index 62% rename from src/contracts/deposit/deposit-tree/deposit-tree.ts rename to src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.ts index 7bd4405c..94394b4e 100644 --- a/src/contracts/deposit/deposit-tree/deposit-tree.ts +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/deposit-tree.ts @@ -1,18 +1,23 @@ +import { + DepositData, + digest2Bytes32, + fromHexString, + parseLittleEndian64, + toLittleEndian64BigInt, +} from '../../../crypto'; import { ethers } from 'ethers'; -import { digest2Bytes32 } from '@chainsafe/as-sha256'; -import { fromHexString } from '@chainsafe/ssz'; -import { parseLittleEndian64, toLittleEndian64 } from '../deposit.utils'; -import { DepositData } from 'bls/bls.containers'; -import { NodeData } from '../interfaces'; +import { NodeData } from '../../../interfaces'; + +const ZERO_HASH_HEX = + '0x0000000000000000000000000000000000000000000000000000000000000000'; +const ZERO_HASH_ROOT_HEX = '0x000000000000000000000000000000000000000000000000'; export class DepositTree { static DEPOSIT_CONTRACT_TREE_DEPTH = 32; - static ZERO_HASH = fromHexString( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); + static ZERO_HASH = fromHexString(ZERO_HASH_HEX); zeroHashes: Uint8Array[] = new Array(DepositTree.DEPOSIT_CONTRACT_TREE_DEPTH); branch: Uint8Array[] = []; - nodeCount = 0; + nodeCount = 0n; constructor() { this.formZeroHashes(); @@ -38,12 +43,12 @@ export class DepositTree { /** * Forms the branch of the tree needed to update the root when a new node is inserted. * @param {Uint8Array} node - The node's data to be inserted. - * @param {number} depositCount - The sequential index of the deposit, representing the total deposits. + * @param {bigint} depositCount - The sequential index of the deposit, representing the total deposits. * @returns {Uint8Array[] | undefined} The updated branch of the tree after inserting the node. */ private formBranch( node: Uint8Array, - depositCount: number, + depositCount: bigint, ): Uint8Array[] | undefined { let size = depositCount; for ( @@ -51,38 +56,32 @@ export class DepositTree { height < DepositTree.DEPOSIT_CONTRACT_TREE_DEPTH; height++ ) { - if ((size & 1) == 1) { + if (size % 2n === 1n) { this.branch[height] = node; return this.branch; } node = digest2Bytes32(this.branch[height], node); - // Using size /= 2 is not a mistake. In JavaScript, when performing bitwise operations - // like & 1, floating-point numbers are implicitly converted to integers, discarding the fractional part. - // This ensures the algorithm works correctly and matches the logic of a Solidity smart contract. - // Solidity does not have floating-point numbers, and all division is performed as integer division, rounding down the result. - size /= 2; + size /= 2n; } } /** - * Inserts a new deposit into the tree using detailed node data. - * @param {NodeData} nodeData - The detailed data of the deposit to be inserted. - */ - public insert(nodeData: NodeData) { - const node = DepositTree.formDepositNode(nodeData); - this.nodeCount++; - this.formBranch(node, this.nodeCount); - } - - /** - * Inserts a new node into the tree using already computed node hash. - * @param {Uint8Array} node - The node's hash to be inserted. + * Inserts a new node into the tree using an already computed hash. The insertion only proceeds + * if the deposit count provided is the next sequential number expected (one more than the current node count). + * @param {Uint8Array} node - The hash of the node to be inserted, represented as a Uint8Array. + * @param {bigint} depositCount - The sequential count of the deposit event from the blockchain, + * expected to be one more than the current highest node count. + * @returns {boolean} Returns true if the node was successfully inserted, false otherwise. */ - public insertNode(node: Uint8Array) { + public insert(node: Uint8Array, depositCount: bigint): boolean { + if (depositCount !== this.nodeCount) { + return false; + } this.nodeCount++; this.formBranch(node, this.nodeCount); + return true; } /** @@ -97,20 +96,16 @@ export class DepositTree { height < DepositTree.DEPOSIT_CONTRACT_TREE_DEPTH; height++ ) { - if ((size & 1) == 1) { + if (size % 2n === 1n) { node = digest2Bytes32(this.branch[height], node); } else { node = digest2Bytes32(node, this.zeroHashes[height]); } - size /= 2; + size /= 2n; } const finalRoot = ethers.utils.soliditySha256( ['bytes', 'bytes', 'bytes'], - [ - node, - toLittleEndian64(this.nodeCount), - '0x000000000000000000000000000000000000000000000000', - ], + [node, toLittleEndian64BigInt(this.nodeCount), ZERO_HASH_ROOT_HEX], ); return finalRoot; } @@ -121,7 +116,7 @@ export class DepositTree { */ public clone() { const tree = new DepositTree(); - tree.branch = [...this.branch]; + tree.branch = this.branch.map((array) => Uint8Array.from(array)); tree.nodeCount = this.nodeCount; return tree; } diff --git a/src/contracts/deposit/deposit-tree/index.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/index.ts similarity index 100% rename from src/contracts/deposit/deposit-tree/index.ts rename to src/contracts/deposits-registry/sanity-checker/integrity-checker/deposit-tree/index.ts diff --git a/src/contracts/deposit/integrity-checker/index.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/index.ts similarity index 50% rename from src/contracts/deposit/integrity-checker/index.ts rename to src/contracts/deposits-registry/sanity-checker/integrity-checker/index.ts index f580e0ad..06b7860a 100644 --- a/src/contracts/deposit/integrity-checker/index.ts +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/index.ts @@ -1 +1,2 @@ export * from './integrity-checker.service'; +export * from './integrity-checker.module'; diff --git a/src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.module.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.module.ts new file mode 100644 index 00000000..cfa2c139 --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { DepositIntegrityCheckerService } from './integrity-checker.service'; + +@Module({ + providers: [DepositIntegrityCheckerService], + exports: [DepositIntegrityCheckerService], +}) +export class DepositIntegrityCheckerModule {} diff --git a/src/contracts/deposit/integrity-checker/integrity-checker.service.ts b/src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.service.ts similarity index 65% rename from src/contracts/deposit/integrity-checker/integrity-checker.service.ts rename to src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.service.ts index 68150ee4..752909c0 100644 --- a/src/contracts/deposit/integrity-checker/integrity-checker.service.ts +++ b/src/contracts/deposits-registry/sanity-checker/integrity-checker/integrity-checker.service.ts @@ -1,20 +1,17 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { RepositoryService } from 'contracts/repository'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { BlockTag } from 'provider'; -import { DepositTree } from '../deposit-tree'; +import { DepositTree } from './deposit-tree'; import { VerifiedDepositEvent, VerifiedDepositEventsCache, -} from '../interfaces'; -import { - DepositCacheIntegrityError, - DEPOSIT_TREE_STEP_SYNC, -} from './constants'; +} from '../../interfaces'; +import { DEPOSIT_TREE_STEP_SYNC } from './constants'; +import { toHexString } from 'contracts/deposits-registry/crypto'; @Injectable() export class DepositIntegrityCheckerService { - finalizedTree = new DepositTree(); + private finalizedTree = new DepositTree(); constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, private repositoryService: RepositoryService, @@ -54,46 +51,48 @@ export class DepositIntegrityCheckerService { } /** - * Checks the integrity of the latest root against the blockchain deposit root for a given block number. + * Checks the integrity of the latest deposit root against the blockchain deposit root for a given block number. + * latest is the tag against which the state relative to the blockchain is stored * @param {number} blockNumber - Block number to check the deposit root against. * @param {VerifiedDepositEvent[]} eventsCache - Latest events to verify against the deposit root. * @returns {Promise} A promise that resolves if the roots match, otherwise throws an error. */ public async checkLatestRoot( - blockNumber: number, + blockHash: string, eventsCache: VerifiedDepositEvent[], - ): Promise { + ): Promise { const tree = await this.putLatestEvents( eventsCache.sort((a, b) => a.depositCount - b.depositCount), ); - return this.checkRoot(blockNumber, tree); + return this.checkRoot(blockHash, tree); } /** - * Checks the integrity of the finalized root against the blockchain deposit root for a given block number. - * @param {number} blockNumber - Block number to check the deposit root against. + * Checks the integrity of the finalized deposit root against the blockchain deposit root for a given block number. + * finalized is the tag against which the state relative to the blockchain is stored. + * @param {string | number} tag - Block Tag to check the deposit root against. * @returns {Promise} A promise that resolves if the roots match, otherwise throws an error. */ - public async checkFinalizedRoot(blockNumber: number): Promise { - return this.checkRoot(blockNumber, this.finalizedTree); + public async checkFinalizedRoot(blockHash: string): Promise { + return this.checkRoot(blockHash, this.finalizedTree); } /** * A private helper method to compare the local deposit tree root with the remote deposit root from the blockchain. - * @param {number} blockNumber - Block number associated with the deposit root to verify. + * @param {string | number} tag - Block Tag associated with the deposit root to verify. * @param {DepositTree} tree - Deposit tree to use for comparison. * @returns {Promise} A promise that resolves if the roots match, otherwise logs an error and throws. */ - private async checkRoot(blockNumber: number, tree: DepositTree) { + private async checkRoot(blockHash: string, tree: DepositTree) { const localRoot = tree.getRoot(); - const remoteRoot = await this.getDepositRoot(blockNumber); + const remoteRoot = await this.getDepositRoot(blockHash); if (localRoot === remoteRoot) { this.logger.log('Integrity check successfully completed', { - blockNumber, + blockHash, }); - return; + return true; } this.logger.error( @@ -101,9 +100,7 @@ export class DepositIntegrityCheckerService { { localRoot, remoteRoot }, ); - throw new DepositCacheIntegrityError( - 'Deposit root is different from deposit root from the network', - ); + return false; } /** @@ -116,12 +113,39 @@ export class DepositIntegrityCheckerService { eventsCache: VerifiedDepositEvent[], ) { for (const [index, event] of eventsCache.entries()) { - tree.insertNode(event.depositDataRoot); + const insertionIsMade = tree.insert( + event.depositDataRoot, + BigInt(event.depositCount), + ); + + if (!insertionIsMade) { + const { + depositCount, + depositDataRoot, + index: eventIndex, + blockHash, + blockNumber, + } = event; + + this.logger.warn( + 'Problem found while forming deposit tree with event', + { + depositCount, + depositDataRoot: toHexString(depositDataRoot), + blockHash, + blockNumber, + eventIndex, + depositCountInTree: Number(tree.nodeCount), + }, + ); + + throw new Error('Problem found while forming deposit tree with event'); + } if (index % DEPOSIT_TREE_STEP_SYNC === 0) { await new Promise((res) => setTimeout(res, 1)); - this.logger.log('Checking integrity of saved deposit events', { + this.logger.log('Inserting verified deposit events', { processed: index, remaining: eventsCache.length - index, }); @@ -134,11 +158,10 @@ export class DepositIntegrityCheckerService { * @param {BlockTag | undefined} blockTag - Specific block number or tag to retrieve the deposit root for. * @returns {Promise} Promise that resolves with the deposit root. */ - public async getDepositRoot(blockTag?: BlockTag): Promise { + public async getDepositRoot(blockHash: string): Promise { const contract = await this.repositoryService.getCachedDepositContract(); - const depositRoot = await contract.get_deposit_root({ - blockTag: blockTag as any, - }); + const overrides = { blockTag: { blockHash } }; + const depositRoot = await contract.get_deposit_root(overrides as any); return depositRoot; } diff --git a/src/contracts/deposits-registry/sanity-checker/sanity-checker.module.ts b/src/contracts/deposits-registry/sanity-checker/sanity-checker.module.ts new file mode 100644 index 00000000..a1f0ab8d --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/sanity-checker.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { BlockchainCheckerModule } from './blockchain-checker'; +import { DepositIntegrityCheckerModule } from './integrity-checker'; +import { DepositRegistrySanityCheckerService } from './sanity-checker.service'; + +@Module({ + imports: [BlockchainCheckerModule, DepositIntegrityCheckerModule], + providers: [DepositRegistrySanityCheckerService], + exports: [DepositRegistrySanityCheckerService], +}) +export class DepositRegistrySanityCheckerModule {} diff --git a/src/contracts/deposits-registry/sanity-checker/sanity-checker.service.ts b/src/contracts/deposits-registry/sanity-checker/sanity-checker.service.ts new file mode 100644 index 00000000..207ffd1b --- /dev/null +++ b/src/contracts/deposits-registry/sanity-checker/sanity-checker.service.ts @@ -0,0 +1,147 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { + VerifiedDepositEvent, + VerifiedDepositEventsCache, +} from '../interfaces'; +import { BlockchainCheckerService } from './blockchain-checker/blockchain-checker.service'; +import { DepositIntegrityCheckerService } from './integrity-checker'; +import { toHexString } from '../crypto'; +@Injectable() +export class DepositRegistrySanityCheckerService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + private blockchainSanityChecker: BlockchainCheckerService, + private depositsIntegrityChecker: DepositIntegrityCheckerService, + ) {} + + public async initialize(initialEventsCache: VerifiedDepositEventsCache) { + await this.depositsIntegrityChecker.initialize(initialEventsCache); + } + + private async indexEventsChunk(events: VerifiedDepositEvent[]) { + return await this.depositsIntegrityChecker.putFinalizedEvents(events); + } + // putLatestEvents + private async checkFreshEvents( + blockHash: string, + events: VerifiedDepositEvent[], + ) { + return await this.depositsIntegrityChecker.checkLatestRoot( + blockHash, + events, + ); + } + + private findReorganization( + blockNumber: number, + blockHash: string, + events: VerifiedDepositEvent[], + ) { + const event = this.blockchainSanityChecker.findReorganizedEvent( + events, + blockNumber, + blockHash, + ); + + if (event) { + this.logger.error('Reorganization found in deposit event', { + blockHash: event.blockHash, + blockNumber: event.blockNumber, + depositDataRoot: toHexString(event.depositDataRoot), + }); + return true; + } + return false; + } + + public verifyCacheBlock( + cachedEvents: VerifiedDepositEventsCache, + currentBlock: number, + ) { + const isCacheValid = this.blockchainSanityChecker.validateCacheBlock( + cachedEvents, + currentBlock, + ); + + const blocks = { + cachedStartBlock: cachedEvents.headers.startBlock, + cachedEndBlock: cachedEvents.headers.endBlock, + currentBlock, + }; + + if (isCacheValid) { + this.logger.log('Deposit events cache has valid age', blocks); + } + + if (!isCacheValid) { + this.logger.error( + 'Deposit events cache is newer than the current block', + blocks, + ); + } + + return isCacheValid; + } + + public async addEventGroupToIndex( + chunkStartBlock: number, + chunkToBlock: number, + events: VerifiedDepositEvent[], + ) { + if (!events.length) return; + + const tree = await this.indexEventsChunk(events); + + this.logger.log('Deposit events chunk was indexed', { + chunkStartBlock, + chunkToBlock, + depositRoot: tree.getRoot(), + }); + } + + /** + * Verifies the integrity of the latest deposit events. If the last event is absent, + * it checks the validity of the last finalized root using the current block hash. + * Otherwise, it checks for reorganizations and matches the deposit root of the events. + * + * @param {string} currentBlockHash - The hash of the current block being processed. + * @param {VerifiedDepositEvent[]} freshEvents - Array of freshly verified deposit events. + * @returns {Promise} - Returns true if the deposit root matches and no reorganization is found, otherwise false. + */ + public async verifyFreshEvents( + currentBlockHash: string, + freshEvents: VerifiedDepositEvent[], + ) { + const lastEvent = freshEvents[freshEvents.length - 1]; + + // If events list is empty, there is no last event, so validate the finalized root for the current block hash. + if (!lastEvent) { + return this.depositsIntegrityChecker.checkFinalizedRoot(currentBlockHash); + } + + const { blockHash, blockNumber } = lastEvent; + + // Check for a reorganization in the blockchain that might affect the deposit events. + const isReorgFound = this.findReorganization( + blockNumber, + blockHash, + freshEvents, + ); + + // If a reorganization is found, return false as the events might not be in the correct state. + if (isReorgFound) return false; + + // Check if the deposit root of the events matches the expected values. + const isDepositRootMatches = await this.checkFreshEvents( + blockHash, + freshEvents, + ); + + return isDepositRootMatches; + } + + public async verifyUpdatedEvents(blockHash: string) { + return this.depositsIntegrityChecker.checkFinalizedRoot(blockHash); + } +} diff --git a/src/contracts/deposits-registry/store/index.ts b/src/contracts/deposits-registry/store/index.ts new file mode 100644 index 00000000..99322182 --- /dev/null +++ b/src/contracts/deposits-registry/store/index.ts @@ -0,0 +1,3 @@ +export * from './store.constants'; +export * from './store.module'; +export * from './store.service'; diff --git a/src/contracts/deposit/leveldb/leveldb.constants.ts b/src/contracts/deposits-registry/store/store.constants.ts similarity index 100% rename from src/contracts/deposit/leveldb/leveldb.constants.ts rename to src/contracts/deposits-registry/store/store.constants.ts diff --git a/src/contracts/deposit/leveldb/leveldb.fixtures.ts b/src/contracts/deposits-registry/store/store.fixtures.ts similarity index 91% rename from src/contracts/deposit/leveldb/leveldb.fixtures.ts rename to src/contracts/deposits-registry/store/store.fixtures.ts index bbca5314..10bda7c4 100644 --- a/src/contracts/deposit/leveldb/leveldb.fixtures.ts +++ b/src/contracts/deposits-registry/store/store.fixtures.ts @@ -1,4 +1,7 @@ -import { VerifiedDepositEvent, VerifiedDepositEventsCacheHeaders } from '..'; +import { + VerifiedDepositEvent, + VerifiedDepositEventsCacheHeaders, +} from '../interfaces'; // Mock for VerifiedDepositEventsCacheHeaders export const headersMock: VerifiedDepositEventsCacheHeaders = { diff --git a/src/contracts/deposit/leveldb/leveldb.module.ts b/src/contracts/deposits-registry/store/store.module.ts similarity index 65% rename from src/contracts/deposit/leveldb/leveldb.module.ts rename to src/contracts/deposits-registry/store/store.module.ts index 6c754d6e..0bf10e80 100644 --- a/src/contracts/deposit/leveldb/leveldb.module.ts +++ b/src/contracts/deposits-registry/store/store.module.ts @@ -1,20 +1,20 @@ import { DynamicModule, Module } from '@nestjs/common'; import { ProviderModule } from 'provider'; -import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './leveldb.constants'; -import { LevelDBService } from './leveldb.service'; +import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './store.constants'; +import { DepositsRegistryStoreService } from './store.service'; @Module({}) -export class LevelDBModule { +export class DepositsRegistryStoreModule { static register( defaultValue: unknown, cacheDir = 'cache', cacheLayerDir = 'deposit-cache', ): DynamicModule { return { - module: LevelDBModule, + module: DepositsRegistryStoreModule, imports: [ProviderModule], providers: [ - LevelDBService, + DepositsRegistryStoreService, { provide: DB_DIR, useValue: cacheDir, @@ -28,7 +28,7 @@ export class LevelDBModule { useValue: defaultValue, }, ], - exports: [LevelDBService], + exports: [DepositsRegistryStoreService], }; } } diff --git a/src/contracts/deposits-registry/store/store.service.spec.ts b/src/contracts/deposits-registry/store/store.service.spec.ts new file mode 100644 index 00000000..3751639b --- /dev/null +++ b/src/contracts/deposits-registry/store/store.service.spec.ts @@ -0,0 +1,134 @@ +import { Test } from '@nestjs/testing'; +import { MockProviderModule } from 'provider'; +import { ConfigModule } from 'common/config'; +import { LoggerModule } from 'common/logger'; +import { DepositsRegistryStoreModule } from './store.module'; +import { DepositsRegistryStoreService } from './store.service'; +import { cacheMock, eventMock1 } from './store.fixtures'; + +const getEventsDepositCount = async ( + dbService: DepositsRegistryStoreService, +) => { + const result = await dbService.getEventsCache(); + const expectedDeposits = result.data.map((event) => event.depositCount); + return expectedDeposits; +}; + +describe('dbService', () => { + const defaultCacheValue = { + headers: {}, + data: [] as any[], + }; + + let dbService: DepositsRegistryStoreService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot(), + MockProviderModule.forRoot(), + DepositsRegistryStoreModule.register(defaultCacheValue, 'leveldb-spec'), + LoggerModule, + ], + }).compile(); + + dbService = moduleRef.get(DepositsRegistryStoreService); + await dbService.initialize(); + }); + + afterEach(async () => { + try { + await dbService.deleteCache(); + await dbService.close(); + } catch (error) {} + }); + + it('should return default cache', async () => { + const result = await dbService.getEventsCache(); + expect(result).toEqual(defaultCacheValue); + }); + + it('should return saved cache', async () => { + const expected = cacheMock; + + await dbService.insertEventsCacheBatch(expected); + const result = await dbService.getEventsCache(); + + expect(result).toEqual(expected); + }); + + describe('deleteDepositsGreaterThanNBatch', () => { + const testCases = [ + { N: 10, deposits: [9, 10, 11, 12], expectedRemaining: [9, 10] }, + { N: 5, deposits: [3, 4, 5, 6], expectedRemaining: [3, 4, 5] }, + { N: 0, deposits: [0, 1, 2], expectedRemaining: [0] }, + ]; + + it.each(testCases)( + 'should delete deposits where deposit count is greater than %s', + async ({ N, deposits, expectedRemaining }) => { + await dbService.insertEventsCacheBatch({ + headers: { startBlock: 1, endBlock: 100 }, + data: deposits.map((count) => ({ + ...eventMock1, + depositCount: count, + })), + }); + + const insertedDeposits = await getEventsDepositCount(dbService); + expect(insertedDeposits).toEqual(expect.arrayContaining(deposits)); + expect(insertedDeposits.length).toBe(deposits.length); + + await dbService.deleteDepositsGreaterThanNBatch(N); + + const expectedDeposits = await getEventsDepositCount(dbService); + expect(expectedDeposits).toEqual( + expect.arrayContaining(expectedRemaining), + ); + expect(expectedDeposits.length).toBe(expectedRemaining.length); + }, + ); + }); + + describe('clearFromLastValidEvent', () => { + const testCases = [ + { lastValidCount: 5, deposits: [4, 5, 6], expectedRemaining: [4, 5] }, + { lastValidCount: 1, deposits: [1, 2, 3], expectedRemaining: [1] }, + { lastValidCount: 0, deposits: [0, 1], expectedRemaining: [0] }, + ]; + + it.each(testCases)( + 'should clear deposits starting from depositCount %s', + async ({ lastValidCount, deposits, expectedRemaining }) => { + await dbService.insertLastValidEvent({ + ...eventMock1, + depositCount: lastValidCount, + }); + + const lastEvent = await dbService.getLastValidEvent(); + expect(lastEvent).toBeDefined(); + expect(lastEvent?.depositCount).toBe(lastValidCount); + + await dbService.insertEventsCacheBatch({ + headers: { startBlock: 1, endBlock: 100 }, + data: deposits.map((count) => ({ + ...eventMock1, + depositCount: count, + })), + }); + + const insertedDeposits = await getEventsDepositCount(dbService); + expect(insertedDeposits).toEqual(expect.arrayContaining(deposits)); + expect(insertedDeposits.length).toBe(deposits.length); + + await dbService.clearFromLastValidEvent(); + + const expectedDeposits = await getEventsDepositCount(dbService); + expect(expectedDeposits).toEqual( + expect.arrayContaining(expectedRemaining), + ); + expect(expectedDeposits.length).toBe(expectedRemaining.length); + }, + ); + }); +}); diff --git a/src/contracts/deposit/leveldb/leveldb.service.ts b/src/contracts/deposits-registry/store/store.service.ts similarity index 60% rename from src/contracts/deposit/leveldb/leveldb.service.ts rename to src/contracts/deposits-registry/store/store.service.ts index d1e7ccbb..17723d12 100644 --- a/src/contracts/deposit/leveldb/leveldb.service.ts +++ b/src/contracts/deposits-registry/store/store.service.ts @@ -6,12 +6,16 @@ import { DB_DEFAULT_VALUE, MAX_DEPOSIT_COUNT, DB_LAYER_DIR, -} from './leveldb.constants'; +} from './store.constants'; import { ProviderService } from 'provider'; -import { VerifiedDepositEvent, VerifiedDepositEventsCacheHeaders } from '..'; +import { + VerifiedDepositEvent, + VerifiedDepositEventsCache, + VerifiedDepositEventsCacheHeaders, +} from '../interfaces'; @Injectable() -export class LevelDBService { +export class DepositsRegistryStoreService { private db!: Level; constructor( private providerService: ProviderService, @@ -65,6 +69,7 @@ export class LevelDBService { public async getEventsCache(): Promise<{ data: VerifiedDepositEvent[]; headers: VerifiedDepositEventsCacheHeaders; + lastValidEvent?: VerifiedDepositEvent; }> { try { const stream = this.db.iterator({ gte: 'deposit:', lte: 'deposit:\xFF' }); @@ -78,13 +83,82 @@ export class LevelDBService { await this.db.get('headers'), ); - return { data, headers }; + const lastValidEvent = await this.getLastValidEvent(); + + return { data, headers, lastValidEvent }; } catch (error: any) { if (error.code === 'LEVEL_NOT_FOUND') return this.cacheDefaultValue; throw error; } } + /** + * Retrieves the last valid deposit event from the database. + * This method queries the database for the 'last-valid-event' key to fetch the most recent + * valid event and parses it into a `VerifiedDepositEvent` object. + * + * @returns {Promise} A promise that resolves to the last valid `VerifiedDepositEvent` object + * or `undefined` if no event is found or if the event could not be retrieved (e.g., key does not exist). + * + * @throws {Error} Throws an error if there is a database access issue other than a 'LEVEL_NOT_FOUND' error code. + */ + public async getLastValidEvent(): Promise { + try { + const lastValidEvent = await this.db.get('last-valid-event'); + return this.parseDepositEvent(lastValidEvent); + } catch (error: any) { + if (error.code === 'LEVEL_NOT_FOUND') return undefined; + throw error; + } + } + + /** + * Clears all deposit records from the database starting from the deposit count of the last valid event. + * If no valid event is found, it will clear deposits greater than deposit count zero. + * This method leverages the `deleteDepositsGreaterThanNBatch` method for batch deletion. + * @returns {Promise} A promise that resolves when all appropriate deposits have been deleted. + */ + public async clearFromLastValidEvent(): Promise { + const lastValidEvent = await this.getLastValidEvent(); + + // Determine the starting index for deletion based on the last valid event's deposit count + const fromIndex = lastValidEvent ? lastValidEvent.depositCount : 0; + + // Delete all deposits from the determined index onwards + await this.deleteDepositsGreaterThanNBatch(fromIndex); + } + + /** + * Deletes all deposit records from the database with keys greater than a specified number. + * @param {number} depositCount - The number above which deposit keys will be deleted. + * @returns {Promise} A promise that resolves when the operation is complete. + */ + public async deleteDepositsGreaterThanNBatch( + depositCount: number, + ): Promise { + // Generate the upper boundary key for deletion + const upperBoundKey = this.generateDepositKey(depositCount); + + // Initialize the iterator starting from the upper boundary key + const stream = this.db.iterator({ gt: upperBoundKey, lte: 'deposit:\xFF' }); + + // Initialize an array to hold batch operations + const ops: { type: 'del'; key: string }[] = []; + + // Populate the batch operations array with delete operations + for await (const [key] of stream) { + ops.push({ + type: 'del', + key: key, + }); + } + + // Execute the batch operation if there are any operations to perform + if (ops.length > 0) { + await this.db.batch(ops); + } + } + /** * Generates a deposit key string based on a given number. * The number is checked to ensure it falls within a valid range (from 0 up to MAX_DEPOSIT_COUNT). @@ -165,6 +239,17 @@ export class LevelDBService { await this.db.batch(ops); } + /** + * Inserts a batch of deposit events and a header into the database. + * + * @param {VerifiedDepositEvent} event - Last valid and verified event. + * @returns {Promise} A promise that resolves when all operations have been successfully committed to the database. + * @public + */ + public async insertLastValidEvent(event: VerifiedDepositEvent) { + await this.db.put('last-valid-event', this.serializeDepositEvent(event)); + } + /** * Clears all entries from the database. * @@ -184,4 +269,19 @@ export class LevelDBService { public async close(): Promise { await this.db.close(); } + + /** + * Saves deposited events to cache + */ + public async setCachedEvents( + cachedEvents: VerifiedDepositEventsCache, + ): Promise { + await this.deleteCache(); + await this.insertEventsCacheBatch({ + ...cachedEvents, + headers: { + ...cachedEvents.headers, + }, + }); + } } diff --git a/src/contracts/lido/index.ts b/src/contracts/lido/index.ts deleted file mode 100644 index 9272f996..00000000 --- a/src/contracts/lido/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lido.module'; -export * from './lido.service'; diff --git a/src/contracts/lido/lido.module.ts b/src/contracts/lido/lido.module.ts deleted file mode 100644 index a430066d..00000000 --- a/src/contracts/lido/lido.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LidoService } from './lido.service'; - -@Module({ - providers: [LidoService], - exports: [LidoService], -}) -export class LidoModule {} diff --git a/src/contracts/lido/lido.service.ts b/src/contracts/lido/lido.service.ts deleted file mode 100644 index a7cd8c98..00000000 --- a/src/contracts/lido/lido.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { RepositoryService } from 'contracts/repository'; -import { BlockTag } from 'provider'; - -@Injectable() -export class LidoService { - constructor(private repositoryService: RepositoryService) {} - - /** - * Returns withdrawal credentials from the contract - */ - public async getWithdrawalCredentials(blockTag?: BlockTag): Promise { - const contract = await this.repositoryService.getCachedLidoContract(); - - return await contract.getWithdrawalCredentials({ - blockTag: blockTag as any, - }); - } -} diff --git a/src/contracts/repository/interfaces/staking-module.ts b/src/contracts/repository/interfaces/staking-module.ts deleted file mode 100644 index 413031f4..00000000 --- a/src/contracts/repository/interfaces/staking-module.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CsmAbi, SigningKeyAbi } from 'generated'; - -export interface StakingModule { - impl: SigningKeyAbi | CsmAbi; -} diff --git a/src/contracts/repository/repository.mock.ts b/src/contracts/repository/repository.mock.ts index 6f21f574..8b2118b5 100644 --- a/src/contracts/repository/repository.mock.ts +++ b/src/contracts/repository/repository.mock.ts @@ -1,4 +1,3 @@ -import { hexZeroPad } from '@ethersproject/bytes'; import { RepositoryService } from './repository.service'; export const mockRepository = async (repositoryService: RepositoryService) => { @@ -8,20 +7,10 @@ export const mockRepository = async (repositoryService: RepositoryService) => { .spyOn(repositoryService, 'getDepositAddress') .mockImplementation(async () => address1); - const mockGetPauseMessagePrefix = jest - .spyOn(repositoryService, 'getPauseMessagePrefix') - .mockImplementation(async () => hexZeroPad('0x2', 32)); - - const mockGetAttestMessagePrefix = jest - .spyOn(repositoryService, 'getAttestMessagePrefix') - .mockImplementation(async () => hexZeroPad('0x1', 32)); - - jest - .spyOn(repositoryService, 'getStakingModules') - .mockImplementation(async () => []); - await repositoryService.initCachedContracts('latest'); jest.spyOn(repositoryService, 'getCachedLidoContract'); - return { depositAddr, mockGetPauseMessagePrefix, mockGetAttestMessagePrefix }; + return { + depositAddr, + }; }; diff --git a/src/contracts/repository/repository.service.spec.ts b/src/contracts/repository/repository.service.spec.ts index 5813b609..2676b5d1 100644 --- a/src/contracts/repository/repository.service.spec.ts +++ b/src/contracts/repository/repository.service.spec.ts @@ -3,15 +3,13 @@ import { Test } from '@nestjs/testing'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; import { PrometheusModule } from 'common/prometheus'; -import { MockProviderModule, ProviderService } from 'provider'; +import { MockProviderModule } from 'provider'; import { RepositoryService } from 'contracts/repository'; import { RepositoryModule } from './repository.module'; import { LocatorService } from './locator/locator.service'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { mockLocator } from './locator/locator.mock'; import { mockRepository } from './repository.mock'; -import { SecurityAbi__factory } from 'generated'; -import { Interface } from '@ethersproject/abi'; describe('RepositoryService', () => { let repositoryService: RepositoryService; @@ -116,82 +114,27 @@ describe('RepositoryService', () => { }); }); - describe('messages prefixes', () => { - let repositoryService: RepositoryService; - let locatorService: LocatorService; - let providerService: ProviderService; + describe('staking router', () => { + let mockGetAddress; beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot(), - MockProviderModule.forRoot(), - LoggerModule, - PrometheusModule, - RepositoryModule, - ], - }).compile(); - - repositoryService = moduleRef.get(RepositoryService); - locatorService = moduleRef.get(LocatorService); - providerService = moduleRef.get(ProviderService); - jest - .spyOn(moduleRef.get(WINSTON_MODULE_NEST_PROVIDER), 'log') - .mockImplementation(() => undefined); + mockGetAddress = mockLocator(locatorService).SRAddr; + await mockRepository(repositoryService); }); - it('getAttestMessagePrefix', async () => { - const expected = '0x' + '1'.repeat(64); - - const mockProviderCall = jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(SecurityAbi__factory.abi); - const result = [expected]; - return iface.encodeFunctionResult('ATTEST_MESSAGE_PREFIX', result); - }); - - jest - .spyOn(repositoryService, 'getDepositAddress') - .mockImplementation(async () => '0x' + '5'.repeat(40)); - - jest - .spyOn(repositoryService, 'getStakingModules') - .mockImplementation(async () => []); - - mockLocator(locatorService); - - await repositoryService.initCachedContracts('latest'); - const prefix = await repositoryService.getAttestMessagePrefix(); - expect(prefix).toBe(expected); - expect(mockProviderCall).toBeCalledTimes(2); + it('should return contract instance', async () => { + const contract = await repositoryService.getCachedStakingRouterContract(); + expect(contract).toBeInstanceOf(Contract); }); - it('getPauseMessagePrefix', async () => { - const expected = '0x' + '1'.repeat(64); - - const mockProviderCall = jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(SecurityAbi__factory.abi); - const result = [expected]; - return iface.encodeFunctionResult('PAUSE_MESSAGE_PREFIX', result); - }); - - jest - .spyOn(repositoryService, 'getDepositAddress') - .mockImplementation(async () => '0x' + '5'.repeat(40)); - - jest - .spyOn(repositoryService, 'getStakingModules') - .mockImplementation(async () => []); - - mockLocator(locatorService); + it('should call getDepositAddress once and cache instance ', async () => { + const contract1 = + await repositoryService.getCachedStakingRouterContract(); + const contract2 = + await repositoryService.getCachedStakingRouterContract(); + expect(mockGetAddress).toBeCalledTimes(1); - await repositoryService.initCachedContracts('latest'); - const prefix = await repositoryService.getPauseMessagePrefix(); - expect(prefix).toBe(expected); - expect(mockProviderCall).toBeCalledTimes(2); + expect(contract1).toEqual(contract2); }); }); }); diff --git a/src/contracts/repository/repository.service.ts b/src/contracts/repository/repository.service.ts index c86e083d..32dc36be 100644 --- a/src/contracts/repository/repository.service.ts +++ b/src/contracts/repository/repository.service.ts @@ -1,10 +1,8 @@ +import { Block } from '@ethersproject/abstract-provider'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { LidoAbi, LidoAbi__factory, LocatorAbi } from 'generated'; import { SecurityAbi, SecurityAbi__factory } from 'generated'; import { DepositAbi, DepositAbi__factory } from 'generated'; -import { CsmAbi, CsmAbi__factory } from 'generated'; -import { SigningKeyAbi, SigningKeyAbi__factory } from 'generated'; -import { IStakingModuleAbi__factory } from 'generated'; import { StakingRouterAbi, StakingRouterAbi__factory } from 'generated'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { BlockTag, ProviderService } from 'provider'; @@ -16,12 +14,7 @@ import { INIT_CONTRACTS_TIMEOUT, LIDO_ABI, STAKING_ROUTER_ABI, - COMMUNITY_ONCHAIN_DEVNET0_V1_TYPE, - COMMUNITY_ONCHAIN_V1_TYPE, - CURATED_ONCHAIN_V1_TYPE, } from './repository.constants'; -import { ethers } from 'ethers'; -import { StakingModule } from './interfaces/staking-module'; @Injectable() export class RepositoryService { @@ -34,11 +27,7 @@ export class RepositoryService { string, LidoAbi | LocatorAbi | SecurityAbi | StakingRouterAbi > = {}; - // store prefixes on the current state of the contracts. - // if the contracts are updated we will change these addresses too - private cachedDSMPrefixes: Record = {}; private permanentContractsCache: Record = {}; - private stakingModulesCache: Record = {}; /** * Init cache for each contract @@ -48,14 +37,13 @@ export class RepositoryService { // order is important: deposit contract depends on dsm await this.initCachedDSMContract(blockTag); await this.initCachedDepositContract(blockTag); - await this.initCachedStakingRouterAbiContract(blockTag); - await this.initCachedStakingModulesContracts(blockTag); + await this.initCachedStakingRouterContract(blockTag); } /** * Init cache for each contract or wait if it makes some error */ - public async initOrWaitCachedContracts() { + public async initOrWaitCachedContracts(): Promise { const block = await this.providerService.getBlock(); try { await this.initCachedContracts({ blockHash: block.hash }); @@ -95,13 +83,6 @@ export class RepositoryService { return this.getFromCache(STAKING_ROUTER_ABI) as StakingRouterAbi; } - /** - * Get Node Operator Registry contract impl - */ - public getCachedStakingModulesContracts(): Record { - return this.stakingModulesCache; - } - /** * Get cached contract impl */ @@ -133,23 +114,6 @@ export class RepositoryService { this.tempContractsCache[contractKey] = impl; } - public setStakingModuleCache(address: string, impl: SigningKeyAbi | CsmAbi) { - if (!this.stakingModulesCache[address]) { - this.logger.log('Staking module contract initial address', { address }); - } - - if ( - this.stakingModulesCache[address] && - this.stakingModulesCache[address].impl.address !== address - ) { - this.logger.log('Staking module contract address was changed', { - address, - }); - } - - this.stakingModulesCache[address] = { impl }; - } - private setPermanentContractCache( address: string, contractKey: string, @@ -185,15 +149,6 @@ export class RepositoryService { DSM_ABI, SecurityAbi__factory.connect(address, provider), ); - - // prune dsm prefixes - this.cachedDSMPrefixes = {}; - - // re-init dsm prefixes - await Promise.all([ - this.getAttestMessagePrefix(), - this.getPauseMessagePrefix(), - ]); } /** @@ -214,7 +169,7 @@ export class RepositoryService { /** * Init cache for SR contract */ - private async initCachedStakingRouterAbiContract( + private async initCachedStakingRouterContract( blockTag: BlockTag, ): Promise { const stakingRouterAddress = @@ -228,104 +183,6 @@ export class RepositoryService { ); } - private async initCachedStakingModulesContracts( - blockTag: BlockTag, - ): Promise { - const stakingModules = await this.getStakingModules(blockTag); - await Promise.all( - stakingModules.map(async (stakingModule) => { - const type = await this.getStakingModuleType( - stakingModule.stakingModuleAddress, - blockTag, - ); - - const provider = this.providerService.provider; - - if (type === CURATED_ONCHAIN_V1_TYPE) { - this.setStakingModuleCache( - stakingModule.stakingModuleAddress, - SigningKeyAbi__factory.connect( - stakingModule.stakingModuleAddress, - provider, - ), - ); - return; - } - - if ( - type === COMMUNITY_ONCHAIN_V1_TYPE || - type === COMMUNITY_ONCHAIN_DEVNET0_V1_TYPE - ) { - this.setStakingModuleCache( - stakingModule.stakingModuleAddress, - CsmAbi__factory.connect( - stakingModule.stakingModuleAddress, - provider, - ), - ); - return; - } - - this.logger.error(new Error(`Staking Module type ${type} is unknown`)); - process.exit(1); - }), - ); - } - - public async getStakingModules(blockTag: BlockTag) { - const stakingRouter = await this.getCachedStakingRouterContract(); - const stakingModules = await stakingRouter.getStakingModules({ - blockTag: blockTag as any, - }); - - return stakingModules; - } - - public async getStakingModuleType( - contractAddress: string, - blockTag: BlockTag, - ): Promise { - const contract = IStakingModuleAbi__factory.connect( - contractAddress, - this.providerService.provider, - ); - - const type = await contract.getType({ blockTag } as any); - return ethers.utils.parseBytes32String(type); - } - - /** - * Returns a prefix from the contract with which the deposit message should be signed - */ - public async getAttestMessagePrefix(): Promise { - if (this.cachedDSMPrefixes.attest) return this.cachedDSMPrefixes.attest; - const contract = await this.getCachedDSMContract(); - this.cachedDSMPrefixes.attest = await contract.ATTEST_MESSAGE_PREFIX(); - return this.cachedDSMPrefixes.attest; - } - - /** - * Returns a prefix from the contract with which the pause message should be signed - */ - public async getPauseMessagePrefix(): Promise { - if (this.cachedDSMPrefixes.pause) return this.cachedDSMPrefixes.pause; - const contract = await this.getCachedDSMContract(); - this.cachedDSMPrefixes.pause = await contract.PAUSE_MESSAGE_PREFIX(); - - return this.cachedDSMPrefixes.pause; - } - - /** - * Returns a prefix from the contract with which the pause message should be signed - */ - public async getUnvetMessagePrefix(): Promise { - if (this.cachedDSMPrefixes.unvet) return this.cachedDSMPrefixes.unvet; - const contract = await this.getCachedDSMContract(); - this.cachedDSMPrefixes.unvet = await contract.UNVET_MESSAGE_PREFIX(); - - return this.cachedDSMPrefixes.unvet; - } - /** * Returns Deposit contract address */ diff --git a/src/contracts/security/security.service.spec.ts b/src/contracts/security/security.service.spec.ts index dad683f0..8e7620d3 100644 --- a/src/contracts/security/security.service.spec.ts +++ b/src/contracts/security/security.service.spec.ts @@ -4,7 +4,7 @@ import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; import { MockProviderModule, ProviderService } from 'provider'; import { WalletService } from 'wallet'; -import { SecurityAbi__factory, StakingRouterAbi__factory } from 'generated'; +import { SecurityAbi__factory } from 'generated'; import { RepositoryModule, RepositoryService } from 'contracts/repository'; import { LocatorService } from 'contracts/repository/locator/locator.service'; import { Interface } from '@ethersproject/abi'; @@ -31,8 +31,6 @@ describe('SecurityService', () => { let repositoryService: RepositoryService; let walletService: WalletService; let loggerService: LoggerService; - let mockGetAttestMessagePrefix: jest.SpyInstance, []>; - let mockGetPauseMessagePrefix: jest.SpyInstance, []>; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -57,9 +55,7 @@ describe('SecurityService', () => { mockLocator(moduleRef.get(LocatorService)); - const repo = await mockRepository(repositoryService); - mockGetAttestMessagePrefix = repo.mockGetAttestMessagePrefix; - mockGetPauseMessagePrefix = repo.mockGetPauseMessagePrefix; + await mockRepository(repositoryService); }); describe('getGuardians', () => { @@ -119,26 +115,30 @@ describe('SecurityService', () => { it('should add prefix', async () => { const prefix = hexZeroPad('0x1', 32); const depositRoot = hexZeroPad('0x2', 32); - const keysOpIndex = 1; + const nonce = 1; const blockNumber = 1; const blockHash = hexZeroPad('0x3', 32); const args = [ depositRoot, - keysOpIndex, + nonce, blockNumber, blockHash, TEST_MODULE_ID, ] as const; + const mockGetAttestMessagePrefix = jest + .spyOn(securityService, 'getAttestMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x1', 32)); + const signDepositData = jest.spyOn(walletService, 'signDepositData'); const signature = await securityService.signDepositData(...args); - // 1 — repository, 2 — signDepositData - expect(mockGetAttestMessagePrefix).toBeCalledTimes(2); + + expect(mockGetAttestMessagePrefix).toBeCalledTimes(1); expect(signDepositData).toBeCalledWith({ prefix, depositRoot, - keysOpIndex, + nonce, blockNumber, blockHash, stakingModuleId: TEST_MODULE_ID, @@ -157,15 +157,20 @@ describe('SecurityService', () => { describe('signPauseDataV2', () => { it('should add prefix', async () => { const blockNumber = 1; + const blockHash = '0x'; + + const mockGetPauseMessagePrefix = jest + .spyOn(securityService, 'getPauseMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); const signPauseData = jest.spyOn(walletService, 'signPauseDataV2'); const signature = await securityService.signPauseDataV2( blockNumber, + blockHash, TEST_MODULE_ID, ); - // 1 — repository, 2 — signDepositData - expect(mockGetPauseMessagePrefix).toBeCalledTimes(2); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); expect(signPauseData).toBeCalledWith({ blockNumber: 1, prefix: @@ -183,30 +188,42 @@ describe('SecurityService', () => { }); }); - describe('isDepositsPaused', () => { - it('should call contract method', async () => { - const expected = true; + describe('signPauseDataV3', () => { + it('should add prefix', async () => { + const blockNumber = 1; + const blockHash = '0x'; - const mockProviderCalla = jest - .spyOn(providerService.provider, 'call') - .mockImplementation(async () => { - const iface = new Interface(StakingRouterAbi__factory.abi); - return iface.encodeFunctionResult('getStakingModuleIsActive', [ - expected, - ]); - }); + const mockGetPauseMessagePrefix = jest + .spyOn(securityService, 'getPauseMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); - const isPaused = await securityService.isModuleDepositsPaused( - TEST_MODULE_ID, + const signPauseData = jest.spyOn(walletService, 'signPauseDataV3'); + + const signature = await securityService.signPauseDataV3( + blockNumber, + blockHash, + ); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); + expect(signPauseData).toBeCalledWith({ + blockNumber: 1, + prefix: + '0x0000000000000000000000000000000000000000000000000000000000000002', + }); + expect(signature).toEqual( + expect.objectContaining({ + _vs: expect.any(String), + r: expect.any(String), + s: expect.any(String), + v: expect.any(Number), + }), ); - expect(isPaused).toBe(!expected); - expect(mockProviderCalla).toBeCalledTimes(1); }); }); describe('pauseDepositsV2', () => { const hash = hexZeroPad('0x1', 32); const blockNumber = 10; + const blockHash = '0x'; let mockWait; let mockPauseDeposits; @@ -216,21 +233,24 @@ describe('SecurityService', () => { beforeEach(async () => { mockWait = jest.fn().mockImplementation(async () => undefined); - const repo = await mockRepository(repositoryService); - mockGetPauseMessagePrefix = repo.mockGetPauseMessagePrefix; + await mockRepository(repositoryService); + mockGetPauseMessagePrefix = jest + .spyOn(securityService, 'getPauseMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); mockPauseDeposits = jest .fn() .mockImplementation(async () => ({ wait: mockWait, hash })); mockGetContractWithSigner = jest - .spyOn(securityService, 'getContractV2WithSigner') + .spyOn(securityService, 'getContractWithSignerDeprecated') .mockImplementation( () => ({ pauseDeposits: mockPauseDeposits } as any), ); signature = await securityService.signPauseDataV2( blockNumber, + blockHash, TEST_MODULE_ID, ); }); @@ -244,14 +264,11 @@ describe('SecurityService', () => { expect(mockPauseDeposits).toBeCalledTimes(1); expect(mockWait).toBeCalledTimes(1); - // mockGetPauseMessagePrefix calls 3 times because - // we have more than one call under the hood - // 1 - repository, 2 — signPauseData, 3 — pauseDeposits - expect(mockGetPauseMessagePrefix).toBeCalledTimes(3); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); expect(mockGetContractWithSigner).toBeCalledTimes(1); }); - it('should exit if the previous call is not completed2', async () => { + it('should exit if the previous call is not completed', async () => { await Promise.all([ securityService.pauseDepositsV2(blockNumber, TEST_MODULE_ID, signature), securityService.pauseDepositsV2(blockNumber, TEST_MODULE_ID, signature), @@ -259,11 +276,255 @@ describe('SecurityService', () => { expect(mockPauseDeposits).toBeCalledTimes(1); expect(mockWait).toBeCalledTimes(1); - // mockGetPauseMessagePrefix calls 3 times because - // we have more than one call under the hood - // 1 - repository, 2 — signPauseData, 3 — pauseDeposits - expect(mockGetPauseMessagePrefix).toBeCalledTimes(3); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); + expect(mockGetContractWithSigner).toBeCalledTimes(1); + }); + }); + + describe('pauseDepositsV3', () => { + const hash = hexZeroPad('0x1', 32); + const blockNumber = 10; + const blockHash = '0x'; + + let mockWait; + let mockPauseDeposits; + let mockGetPauseMessagePrefix; + let mockGetContractWithSigner; + let signature; + + beforeEach(async () => { + mockWait = jest.fn().mockImplementation(async () => undefined); + await mockRepository(repositoryService); + mockGetPauseMessagePrefix = jest + .spyOn(securityService, 'getPauseMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); + + mockPauseDeposits = jest + .fn() + .mockImplementation(async () => ({ wait: mockWait, hash })); + + mockGetContractWithSigner = jest + .spyOn(securityService, 'getContractWithSigner') + .mockImplementation( + () => ({ pauseDeposits: mockPauseDeposits } as any), + ); + + signature = await securityService.signPauseDataV3(blockNumber, blockHash); + }); + + it('should call contract method', async () => { + await securityService.pauseDepositsV3(blockNumber, signature); + + expect(mockPauseDeposits).toBeCalledTimes(1); + expect(mockWait).toBeCalledTimes(1); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); + expect(mockGetContractWithSigner).toBeCalledTimes(1); + }); + + it('should exit if the previous call is not completed', async () => { + await Promise.all([ + securityService.pauseDepositsV3(blockNumber, signature), + securityService.pauseDepositsV3(blockNumber, signature), + ]); + + expect(mockPauseDeposits).toBeCalledTimes(1); + expect(mockWait).toBeCalledTimes(1); + expect(mockGetPauseMessagePrefix).toBeCalledTimes(1); expect(mockGetContractWithSigner).toBeCalledTimes(1); }); }); + + describe('signUnvetData', () => { + it('should add prefix', async () => { + const nonce = 1; + const blockNumber = 10; + const blockHash = hexZeroPad('0x3', 32); + const stakingModuleId = 1; + const operatorIds = '0x00000000000000010000000000000002'; + const vettedKeysByOperator = + '0x0000000000000000000000000000000000000000000000000000000000000002'; + + const mockGetUnvetMessagePrefix = jest + .spyOn(securityService, 'getUnvetMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); + + const signUnvetData = jest.spyOn(walletService, 'signUnvetData'); + + const signature = await securityService.signUnvetData( + nonce, + blockNumber, + blockHash, + stakingModuleId, + operatorIds, + vettedKeysByOperator, + ); + expect(mockGetUnvetMessagePrefix).toBeCalledTimes(1); + expect(signUnvetData).toBeCalledWith({ + blockNumber, + blockHash, + stakingModuleId, + nonce, + operatorIds, + vettedKeysByOperator, + prefix: + '0x0000000000000000000000000000000000000000000000000000000000000002', + }); + expect(signature).toEqual( + expect.objectContaining({ + _vs: expect.any(String), + r: expect.any(String), + s: expect.any(String), + v: expect.any(Number), + }), + ); + }); + }); + + describe('unvetSigningKeys', () => { + const hash = hexZeroPad('0x1', 32); + + const nonce = 1; + const blockNumber = 10; + const blockHash = hexZeroPad('0x3', 32); + const stakingModuleId = 1; + const operatorIds = '0x00000000000000010000000000000002'; + const vettedKeysByOperator = + '0x0000000000000000000000000000000000000000000000000000000000000002'; + + let mockWait; + let mockUnvetSigningKeys; + let mockGetUnvetMessagePrefix; + let mockGetContractWithSigner; + let signature; + + beforeEach(async () => { + mockWait = jest.fn().mockImplementation(async () => undefined); + await mockRepository(repositoryService); + mockGetUnvetMessagePrefix = jest + .spyOn(securityService, 'getUnvetMessagePrefix') + .mockImplementation(async () => hexZeroPad('0x2', 32)); + + mockUnvetSigningKeys = jest + .fn() + .mockImplementation(async () => ({ wait: mockWait, hash })); + + mockGetContractWithSigner = jest + .spyOn(securityService, 'getContractWithSigner') + .mockImplementation( + () => ({ unvetSigningKeys: mockUnvetSigningKeys } as any), + ); + + signature = await securityService.signUnvetData( + nonce, + blockNumber, + blockHash, + stakingModuleId, + operatorIds, + vettedKeysByOperator, + ); + }); + + it('should call contract method', async () => { + await securityService.unvetSigningKeys( + nonce, + blockNumber, + blockHash, + stakingModuleId, + operatorIds, + vettedKeysByOperator, + signature, + ); + + expect(mockUnvetSigningKeys).toBeCalledTimes(1); + expect(mockWait).toBeCalledTimes(1); + expect(mockGetUnvetMessagePrefix).toBeCalledTimes(1); + expect(mockGetContractWithSigner).toBeCalledTimes(1); + }); + + it('should exit if the previous call is not completed', async () => { + await Promise.all([ + securityService.unvetSigningKeys( + nonce, + blockNumber, + blockHash, + stakingModuleId, + operatorIds, + vettedKeysByOperator, + signature, + ), + securityService.unvetSigningKeys( + nonce, + blockNumber, + blockHash, + stakingModuleId, + operatorIds, + vettedKeysByOperator, + signature, + ), + ]); + + expect(mockUnvetSigningKeys).toBeCalledTimes(1); + expect(mockWait).toBeCalledTimes(1); + expect(mockGetUnvetMessagePrefix).toBeCalledTimes(1); + expect(mockGetContractWithSigner).toBeCalledTimes(1); + }); + }); + + describe('messages prefixes', () => { + const blockHash = '0x'; + + beforeEach(async () => { + jest + .spyOn(repositoryService, 'getDepositAddress') + .mockImplementation(async () => '0x' + '5'.repeat(40)); + }); + + it('getAttestMessagePrefix', async () => { + const expected = '0x' + '1'.repeat(64); + + const mockProviderCall = jest + .spyOn(providerService.provider, 'call') + .mockImplementation(async () => { + const iface = new Interface(SecurityAbi__factory.abi); + const result = [expected]; + return iface.encodeFunctionResult('ATTEST_MESSAGE_PREFIX', result); + }); + + const prefix = await securityService.getAttestMessagePrefix(blockHash); + expect(prefix).toBe(expected); + expect(mockProviderCall).toBeCalledTimes(1); + }); + + it('getPauseMessagePrefix', async () => { + const expected = '0x' + '1'.repeat(64); + + const mockProviderCall = jest + .spyOn(providerService.provider, 'call') + .mockImplementation(async () => { + const iface = new Interface(SecurityAbi__factory.abi); + const result = [expected]; + return iface.encodeFunctionResult('PAUSE_MESSAGE_PREFIX', result); + }); + + const prefix = await securityService.getPauseMessagePrefix(blockHash); + expect(prefix).toBe(expected); + expect(mockProviderCall).toBeCalledTimes(1); + }); + + it('getUnvetMessagePrefix', async () => { + const expected = '0x' + '1'.repeat(64); + + const mockProviderCall = jest + .spyOn(providerService.provider, 'call') + .mockImplementation(async () => { + const iface = new Interface(SecurityAbi__factory.abi); + const result = [expected]; + return iface.encodeFunctionResult('UNVET_MESSAGE_PREFIX', result); + }); + + const prefix = await securityService.getUnvetMessagePrefix(blockHash); + expect(prefix).toBe(expected); + expect(mockProviderCall).toBeCalledTimes(1); + }); + }); }); diff --git a/src/contracts/security/security.service.ts b/src/contracts/security/security.service.ts index 751f7bfd..78f1ac47 100644 --- a/src/contracts/security/security.service.ts +++ b/src/contracts/security/security.service.ts @@ -6,8 +6,12 @@ import { METRIC_PAUSE_ATTEMPTS, METRIC_UNVET_ATTEMPTS, } from 'common/prometheus'; -import { OneAtTime, StakingModuleId } from 'common/decorators'; -import { SecurityAbi, SecurityPauseV2Abi__factory } from 'generated'; +import { OneAtTime, OneAtTimeCallId } from 'common/decorators'; +import { SecurityAbi } from 'generated'; +import { + SecurityDeprecatedPauseAbi, + SecurityDeprecatedPauseAbi__factory, +} from 'generated'; import { RepositoryService } from 'contracts/repository'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Counter } from 'prom-client'; @@ -49,10 +53,13 @@ export class SecurityService { return contractWithSigner; } - public getContractV2WithSigner() { + /** + * Returns an instance of the deprecated v2 security contract with only the `pause` method. + */ + public getContractWithSignerDeprecated(): SecurityDeprecatedPauseAbi { const contract = this.repositoryService.getCachedDSMContract(); - const oldContract = SecurityPauseV2Abi__factory.connect( + const oldContract = SecurityDeprecatedPauseAbi__factory.connect( contract.address, this.providerService.provider, ); @@ -96,20 +103,27 @@ export class SecurityService { /** * Signs a message to deposit buffered ethers with the prefix from the contract + * + * @param depositRoot: Root of deposit contract + * @param nonce - Current index of keys operations from the registry contract + * @param blockNumber - The block number, included as part of the message for signing. + * @param blockHash - The block hash, included as part of the message for signing and is used to fetch the pause prefix + * @param stakingModuleId - The staking module ID, included as part of the message for signing. + * @returns Signature for deposit. */ public async signDepositData( depositRoot: string, - keysOpIndex: number, + nonce: number, blockNumber: number, blockHash: string, stakingModuleId: number, ): Promise { - const prefix = await this.repositoryService.getAttestMessagePrefix(); + const prefix = await this.getAttestMessagePrefix(blockHash); return await this.walletService.signDepositData({ prefix, depositRoot, - keysOpIndex, + nonce, blockNumber, blockHash, stakingModuleId, @@ -117,10 +131,17 @@ export class SecurityService { } /** - * Signs a message to pause deposits with the prefix from the contract + * Signs a message to pause deposits, including the pause prefix from the contract. + * + * @param blockNumber - The block number, included as part of the message for signing. + * @param blockHash - The block hash, used to fetch the pause prefix. + * @returns Signature for pausing deposits. */ - public async signPauseDataV3(blockNumber: number): Promise { - const prefix = await this.repositoryService.getPauseMessagePrefix(); + public async signPauseDataV3( + blockNumber: number, + blockHash: string, + ): Promise { + const prefix = await this.getPauseMessagePrefix(blockHash); return await this.walletService.signPauseDataV3({ prefix, @@ -135,41 +156,50 @@ export class SecurityService { */ @OneAtTime() public async pauseDepositsV3( - blockNumber: number, + pauseBlockNumber: number, signature: Signature, - ): Promise { - this.logger.warn('Try to pause deposits', { blockNumber }); + ): Promise { + this.logger.warn('Try to pause deposits', { pauseBlockNumber }); this.pauseAttempts.inc(); const contract = this.getContractWithSigner(); const { r, _vs: vs } = signature; - const tx = await contract.pauseDeposits(blockNumber, { + const tx = await contract.pauseDeposits(pauseBlockNumber, { r, vs, }); this.logger.warn('Pause transaction sent', { txHash: tx.hash, - blockNumber, + pauseBlockNumber, }); - this.logger.warn('Waiting for block confirmation', { blockNumber }); + this.logger.warn('Waiting for block confirmation', { pauseBlockNumber }); const receipt = await tx.wait(); - this.logger.warn('Block confirmation received', { blockNumber }); + this.logger.warn('Block confirmation received for the pause tx', { + pauseBlockNumber, + txHash: tx.hash, + }); return receipt; } /** - * Signs a message to pause deposits with the prefix from the contract + * Signs a message to pause deposits, including the pause prefix from the contract. + * + * @param blockNumber - The block number, included as part of the message for signing. + * @param blockHash - The block hash, used to fetch the pause prefix. + * @param stakingModuleId - The staking module ID, included as part of the message for signing. + * @returns Signature for pausing deposits. */ public async signPauseDataV2( blockNumber: number, + blockHash: string, stakingModuleId: number, ): Promise { - const prefix = await this.repositoryService.getPauseMessagePrefix(); + const prefix = await this.getPauseMessagePrefix(blockHash); return await this.walletService.signPauseDataV2({ prefix, @@ -187,13 +217,13 @@ export class SecurityService { @OneAtTime() public async pauseDepositsV2( blockNumber: number, - @StakingModuleId stakingModuleId: number, + @OneAtTimeCallId stakingModuleId: number, signature: Signature, - ): Promise { + ): Promise { this.logger.warn('Try to pause deposits', { stakingModuleId, blockNumber }); this.pauseAttempts.inc(); - const contract = this.getContractV2WithSigner(); + const contract = this.getContractWithSignerDeprecated(); const { r, _vs: vs } = signature; const tx = await contract.pauseDeposits(blockNumber, stakingModuleId, { @@ -226,7 +256,7 @@ export class SecurityService { * * @param nonce - The nonce for the staking module. * @param blockNumber - The block number at which the message is signed. - * @param blockHash - The hash of the block corresponding to the block number. + * @param blockHash - The hash of the block corresponding to the block number, used to fetch the pause prefix. * @param stakingModuleId - The ID of the target staking module. * @param operatorIds - A string containing the IDs of the operators whose keys are being unvetted. * @param vettedKeysByOperator - A string representing the new staking limit amount per operator. @@ -241,7 +271,7 @@ export class SecurityService { operatorIds: string, vettedKeysByOperator: string, ): Promise { - const prefix = await this.repositoryService.getUnvetMessagePrefix(); + const prefix = await this.getUnvetMessagePrefix(blockHash); return await this.walletService.signUnvetData({ prefix, @@ -272,11 +302,11 @@ export class SecurityService { nonce: number, blockNumber: number, blockHash: string, - @StakingModuleId stakingModuleId: number, + @OneAtTimeCallId stakingModuleId: number, operatorIds: string, vettedKeysByOperator: string, signature: Signature, - ): Promise { + ): Promise { this.logger.warn('Try to unvet keys for staking module', { stakingModuleId, blockNumber, @@ -355,29 +385,39 @@ export class SecurityService { /** * Check if deposits paused */ - public async isDepositContractPaused(blockTag?: BlockTag) { + public async isDepositsPaused(blockTag?: BlockTag) { const contract = await this.repositoryService.getCachedDSMContract(); return contract.isDepositsPaused({ blockTag: blockTag as any }); } /** - * Returns the current state of deposits for module + * Returns a prefix from the contract with which the deposit message should be signed */ - public async isModuleDepositsPaused( - stakingModuleId: number, - blockTag?: BlockTag, - ): Promise { - const stakingRouterContract = - await this.repositoryService.getCachedStakingRouterContract(); + public async getAttestMessagePrefix(blockHash: string): Promise { + const contract = await this.repositoryService.getCachedDSMContract(); + return await contract.ATTEST_MESSAGE_PREFIX({ + blockTag: { blockHash } as any, + }); + } - const isActive = await stakingRouterContract.getStakingModuleIsActive( - stakingModuleId, - { - blockTag: blockTag as any, - }, - ); + /** + * Returns a prefix from the contract with which the pause message should be signed + */ + public async getPauseMessagePrefix(blockHash: string): Promise { + const contract = await this.repositoryService.getCachedDSMContract(); + return await contract.PAUSE_MESSAGE_PREFIX({ + blockTag: { blockHash } as any, + }); + } - return !isActive; + /** + * Returns a prefix from the contract with which the pause message should be signed + */ + public async getUnvetMessagePrefix(blockHash: string): Promise { + const contract = await this.repositoryService.getCachedDSMContract(); + return await contract.UNVET_MESSAGE_PREFIX({ + blockTag: { blockHash } as any, + }); } } diff --git a/src/contracts/signing-key-events-cache/index.ts b/src/contracts/signing-key-events-cache/index.ts deleted file mode 100644 index baafb04b..00000000 --- a/src/contracts/signing-key-events-cache/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './signing-key-events-cache.module'; -export * from './signing-key-events-cache.service'; diff --git a/src/contracts/signing-key-events-cache/leveldb/index.ts b/src/contracts/signing-key-events-cache/leveldb/index.ts deleted file mode 100644 index eed6aa71..00000000 --- a/src/contracts/signing-key-events-cache/leveldb/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './leveldb.constants'; -export * from './leveldb.module'; -export * from './leveldb.service'; diff --git a/src/contracts/signing-key-events-cache/signing-key-events-cache.module.ts b/src/contracts/signing-key-events-cache/signing-key-events-cache.module.ts deleted file mode 100644 index 34c248f6..00000000 --- a/src/contracts/signing-key-events-cache/signing-key-events-cache.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LevelDBModule } from './leveldb'; -import { SigningKeyEventsCacheService } from './signing-key-events-cache.service'; -import { SIGNING_KEYS_CACHE_DEFAULT } from './constants'; -import { RepositoryModule } from 'contracts/repository'; - -@Module({ - imports: [ - RepositoryModule, - LevelDBModule.register(SIGNING_KEYS_CACHE_DEFAULT), - ], - providers: [SigningKeyEventsCacheService], - exports: [SigningKeyEventsCacheService], -}) -export class SigningKeyEventsCacheModule {} diff --git a/src/contracts/signing-keys-registry/fetcher/fetcher.module.ts b/src/contracts/signing-keys-registry/fetcher/fetcher.module.ts new file mode 100644 index 00000000..9b3831eb --- /dev/null +++ b/src/contracts/signing-keys-registry/fetcher/fetcher.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { StakingRouterModule } from 'contracts/staking-router'; +import { SigningKeysRegistryFetcherService } from './fetcher.service'; + +@Module({ + imports: [StakingRouterModule], + providers: [SigningKeysRegistryFetcherService], + exports: [SigningKeysRegistryFetcherService], +}) +export class SigningKeysRegistryFetcherModule {} diff --git a/src/contracts/signing-keys-registry/fetcher/fetcher.service.ts b/src/contracts/signing-keys-registry/fetcher/fetcher.service.ts new file mode 100644 index 00000000..78e6d994 --- /dev/null +++ b/src/contracts/signing-keys-registry/fetcher/fetcher.service.ts @@ -0,0 +1,86 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { StakingRouterService } from 'contracts/staking-router'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { ProviderService } from 'provider'; +import { + SigningKeyEvent, + SigningKeyEventsGroup, +} from '../interfaces/event.interface'; + +@Injectable() +export class SigningKeysRegistryFetcherService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + private providerService: ProviderService, + private stakingRouterService: StakingRouterService, + ) {} + + /** + * Fetches signing key events within a specified block range, with fallback mechanisms. + * If the request failed, it tries to repeat it or split it into two + * + * @param {number} startBlock - The starting block number of the range. + * @param {number} endBlock - The ending block number of the range. + * @returns {Promise} Events fetched within the specified block range + */ + public async fetchEventsFallOver( + startBlock: number, + endBlock: number, + stakingModulesAddresses: string[], + ): Promise { + const fetcherWrapper = (start: number, end: number) => + this.fetchEvents(start, end, stakingModulesAddresses); + + return await this.providerService.fetchEventsFallOver( + startBlock, + endBlock, + fetcherWrapper, + ); + } + + /** + * Fetches signing key events within a specified block range from staking module contracts. + * + * @param {number} startBlock - The starting block number of the range. + * @param {number} endBlock - The ending block number of the range. + * @returns {Promise} Events fetched within the specified block range. + */ + public async fetchEvents( + startBlock: number, + endBlock: number, + stakingModulesAddresses: string[], + ): Promise { + const events: SigningKeyEvent[] = []; + + await Promise.all( + stakingModulesAddresses.map(async (address) => { + const rawEvents = + await this.stakingRouterService.getSigningKeyAddedEvents( + startBlock, + endBlock, + address, + ); + + const moduleEvents: SigningKeyEvent[] = rawEvents.map((rawEvent) => { + return { + operatorIndex: rawEvent.args[0].toNumber(), + key: rawEvent.args[1], + moduleAddress: address, + blockNumber: rawEvent.blockNumber, + logIndex: rawEvent.logIndex, + blockHash: rawEvent.blockHash, + }; + }); + + events.push(...moduleEvents); + + this.logger.log('Fetched signing keys add events for staking module', { + count: moduleEvents.length, + address, + }); + }), + ); + + return { events, startBlock, endBlock }; + } +} diff --git a/src/contracts/signing-keys-registry/fetcher/index.ts b/src/contracts/signing-keys-registry/fetcher/index.ts new file mode 100644 index 00000000..128136bc --- /dev/null +++ b/src/contracts/signing-keys-registry/fetcher/index.ts @@ -0,0 +1,2 @@ +export * from './fetcher.module'; +export * from './fetcher.service'; diff --git a/src/contracts/signing-keys-registry/index.ts b/src/contracts/signing-keys-registry/index.ts new file mode 100644 index 00000000..a5530e09 --- /dev/null +++ b/src/contracts/signing-keys-registry/index.ts @@ -0,0 +1,2 @@ +export * from './signing-keys-registry.module'; +export * from './signing-keys-registry.service'; diff --git a/src/contracts/signing-key-events-cache/interfaces/cache.interface.ts b/src/contracts/signing-keys-registry/interfaces/cache.interface.ts similarity index 100% rename from src/contracts/signing-key-events-cache/interfaces/cache.interface.ts rename to src/contracts/signing-keys-registry/interfaces/cache.interface.ts diff --git a/src/contracts/signing-key-events-cache/interfaces/event.interface.ts b/src/contracts/signing-keys-registry/interfaces/event.interface.ts similarity index 100% rename from src/contracts/signing-key-events-cache/interfaces/event.interface.ts rename to src/contracts/signing-keys-registry/interfaces/event.interface.ts diff --git a/src/contracts/signing-keys-registry/sanity-checker/index.ts b/src/contracts/signing-keys-registry/sanity-checker/index.ts new file mode 100644 index 00000000..04e24741 --- /dev/null +++ b/src/contracts/signing-keys-registry/sanity-checker/index.ts @@ -0,0 +1,2 @@ +export * from './sanity-checker.module'; +export * from './sanity-checker.service'; diff --git a/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.module.ts b/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.module.ts new file mode 100644 index 00000000..8cbedd71 --- /dev/null +++ b/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SigningKeysRegistrySanityCheckerService } from './sanity-checker.service'; + +@Module({ + providers: [SigningKeysRegistrySanityCheckerService], + exports: [SigningKeysRegistrySanityCheckerService], +}) +export class SigningKeysRegistrySanityCheckerModule {} diff --git a/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.service.ts b/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.service.ts new file mode 100644 index 00000000..05f96ea0 --- /dev/null +++ b/src/contracts/signing-keys-registry/sanity-checker/sanity-checker.service.ts @@ -0,0 +1,93 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; + +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; + +import { SigningKeyEventsCache } from '../interfaces/cache.interface'; +import { SigningKeyEvent } from '../interfaces/event.interface'; + +@Injectable() +export class SigningKeysRegistrySanityCheckerService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + ) {} + + /** + * Validates the block number in the cached events against the current block number. + * + * This method checks if the cached events are up to date by comparing the current block number + * with the end block number in the cache. It logs a message if the cache is valid and a warning if it is not. + * + * @param {SigningKeyEventsCache} cachedEvents - The cached events containing block headers to validate. + * @param {number} currentBlock - The current block number to compare against the cached block. + * @returns {boolean} `true` if the cache is valid (i.e., the current block number is greater than or equal to the cached end block), `false` otherwise. + */ + public verifyCacheBlock( + cachedEvents: SigningKeyEventsCache, + currentBlock: number, + ): boolean { + const isCacheValid = currentBlock >= cachedEvents.headers.endBlock; + + const blocks = { + cachedStartBlock: cachedEvents.headers.startBlock, + cachedEndBlock: cachedEvents.headers.endBlock, + currentBlock, + }; + + if (isCacheValid) { + this.logger.log('Signing keys events cache has valid age', blocks); + } + + if (!isCacheValid) { + this.logger.warn( + 'Signing key events cache is newer than the current block', + blocks, + ); + } + + return isCacheValid; + } + + /** + * Validates the block hash of signing key events. + * + * This method checks each event's block hash against the provided block hash, but only if the event's block number + * matches the given `blockNumber`. This ensures that the events are not from an alternate chain (e.g., due to a chain reorganization). + * If a block number match is found but the block hashes do not match, an error is thrown. + * + * @param {SigningKeyEvent[]} events - The list of signing key events to be checked. + * @param {number} blockNumber - The block number to match against the events' block numbers. + * @param {string} blockHash - The block hash to match against the events' block hashes. + */ + public checkEventsBlockHash( + events: SigningKeyEvent[], + blockNumber: number, + blockHash: string, + ): boolean { + const event = this.findReorganizedEvent(events, blockNumber, blockHash); + if (event) { + this.logger.error('Reorganization found in signing key event', { + blockHash: event.blockHash, + blockNumber: event.blockNumber, + }); + return false; + } + return true; + } + + /** + * Checks events block hash + * An additional check to avoid events processing in an alternate chain + */ + private findReorganizedEvent( + events: SigningKeyEvent[], + blockNumber: number, + blockHash: string, + ): SigningKeyEvent | null { + return ( + events.find( + (event) => + event.blockNumber === blockNumber && event.blockHash !== blockHash, + ) || null + ); + } +} diff --git a/src/contracts/signing-key-events-cache/constants.ts b/src/contracts/signing-keys-registry/signing-keys-registry.constants.ts similarity index 78% rename from src/contracts/signing-key-events-cache/constants.ts rename to src/contracts/signing-keys-registry/signing-keys-registry.constants.ts index dd4ed80e..dd0829c6 100644 --- a/src/contracts/signing-key-events-cache/constants.ts +++ b/src/contracts/signing-keys-registry/signing-keys-registry.constants.ts @@ -9,7 +9,7 @@ export const SIGNING_KEYS_CACHE_DEFAULT = Object.freeze({ data: [], }); -export const CURATED_MODULE_DEPLOYMENT_BLOCK_NETWORK: { +export const EARLIEST_MODULE_DEPLOYMENT_BLOCK_NETWORK: { [key in CHAINS]?: number; } = { [CHAINS.Mainnet]: 11473216, @@ -18,5 +18,5 @@ export const CURATED_MODULE_DEPLOYMENT_BLOCK_NETWORK: { // will make a gap in case of reorganization export const SIGNING_KEYS_EVENTS_CACHE_LAG_BLOCKS = 100; -export const SIGNING_KEY_EVENTS_CACHE_UPDATE_BLOCK_RATE = 10; export const FETCHING_EVENTS_STEP = 10_000; +export const SIGNING_KEYS_REGISTRY_FINALIZED_TAG = 'finalized'; diff --git a/src/contracts/signing-keys-registry/signing-keys-registry.module.ts b/src/contracts/signing-keys-registry/signing-keys-registry.module.ts new file mode 100644 index 00000000..1bbbdb93 --- /dev/null +++ b/src/contracts/signing-keys-registry/signing-keys-registry.module.ts @@ -0,0 +1,41 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { SigningKeysStoreModule } from './store'; +import { SigningKeysRegistryService } from './signing-keys-registry.service'; +import { + SIGNING_KEYS_CACHE_DEFAULT, + SIGNING_KEYS_REGISTRY_FINALIZED_TAG, +} from './signing-keys-registry.constants'; +import { SigningKeysRegistryFetcherModule } from './fetcher'; +import { SigningKeysRegistrySanityCheckerModule } from './sanity-checker'; + +@Module({}) +export class SigningKeysRegistryModule { + /** + * Registers the signing keys module with a specific tag to handle block finality. + * The `finalizedTag` is primarily used to address issues with the Ganache handling of the 'finalized' tag, + * where it needs to be substituted with 'latest' for end-to-end tests. This tag is necessary only on a Ethereum node + * to avoid issues with blockchain reorganizations. + * In a production environment, this argument should either be empty or set to 'finalized'. + * + * @param {string} [finalizedTag='finalized'] - The tag to be used for identifying the status of blocks concerning finality. + * @returns {DynamicModule} - The dynamic module configuration for the Deposits Registry. + */ + static register(finalizedTag = 'finalized'): DynamicModule { + return { + module: SigningKeysRegistryModule, + imports: [ + SigningKeysRegistryFetcherModule, + SigningKeysRegistrySanityCheckerModule, + SigningKeysStoreModule.register(SIGNING_KEYS_CACHE_DEFAULT), + ], + providers: [ + SigningKeysRegistryService, + { + provide: SIGNING_KEYS_REGISTRY_FINALIZED_TAG, + useValue: finalizedTag, + }, + ], + exports: [SigningKeysRegistryService], + }; + } +} diff --git a/src/contracts/signing-key-events-cache/signing-key-events-cache.service.ts b/src/contracts/signing-keys-registry/signing-keys-registry.service.ts similarity index 53% rename from src/contracts/signing-key-events-cache/signing-key-events-cache.service.ts rename to src/contracts/signing-keys-registry/signing-keys-registry.service.ts index 18121215..67d4f697 100644 --- a/src/contracts/signing-key-events-cache/signing-key-events-cache.service.ts +++ b/src/contracts/signing-keys-registry/signing-keys-registry.service.ts @@ -1,29 +1,27 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { RepositoryService } from 'contracts/repository'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { ProviderService } from 'provider'; -import { - SigningKeyEvent, - SigningKeyEventsGroup, - SigningKeyEventsGroupWithStakingModules, -} from './interfaces/event.interface'; -import { LevelDBService } from './leveldb'; +import { SigningKeyEventsGroupWithStakingModules } from './interfaces/event.interface'; +import { SigningKeysStoreService } from './store'; import { SigningKeyEventsCache } from './interfaces/cache.interface'; import { - CURATED_MODULE_DEPLOYMENT_BLOCK_NETWORK, + EARLIEST_MODULE_DEPLOYMENT_BLOCK_NETWORK, FETCHING_EVENTS_STEP, - SIGNING_KEYS_EVENTS_CACHE_LAG_BLOCKS, - SIGNING_KEY_EVENTS_CACHE_UPDATE_BLOCK_RATE, -} from './constants'; + SIGNING_KEYS_REGISTRY_FINALIZED_TAG, +} from './signing-keys-registry.constants'; import { performance } from 'perf_hooks'; +import { SigningKeysRegistryFetcherService } from './fetcher'; +import { SigningKeysRegistrySanityCheckerService } from './sanity-checker/sanity-checker.service'; @Injectable() -export class SigningKeyEventsCacheService { +export class SigningKeysRegistryService { constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, private providerService: ProviderService, - private repositoryService: RepositoryService, - private levelDBCacheService: LevelDBService, + private store: SigningKeysStoreService, + private fetcher: SigningKeysRegistryFetcherService, + private sanityChecker: SigningKeysRegistrySanityCheckerService, + @Inject(SIGNING_KEYS_REGISTRY_FINALIZED_TAG) private finalizedTag: string, ) {} /** @@ -36,16 +34,10 @@ export class SigningKeyEventsCacheService { * @param {number} blockNumber - The block number of the newly processed block. * @returns {Promise} */ - public async handleNewBlock(blockNumber): Promise { - const wasUpdated = await this.stakingModuleListWasUpdated(); - if (wasUpdated) { - this.logger.log('Staking module list was updated. Deleting cache'); - await this.levelDBCacheService.deleteCache(); - await this.updateEventsCache(); - } else if (blockNumber % SIGNING_KEY_EVENTS_CACHE_UPDATE_BLOCK_RATE === 0) { - // update for every SIGNING_KEY_EVENTS_CACHE_UPDATE_BLOCK_RATE block - await this.updateEventsCache(); - } + public async handleNewBlock( + currentStakingModulesAddresses: string[], + ): Promise { + await this.updateEventsCache(currentStakingModulesAddresses); } /** @@ -53,25 +45,9 @@ export class SigningKeyEventsCacheService { * @param {number} blockNumber - The block number to validate the cache against. * @returns {Promise} */ - public async initialize(blockNumber) { - await this.levelDBCacheService.initialize(); - - const cachedEvents = await this.getCachedEvents(); - - // check age of cache - const isCacheValid = this.validateCacheBlock(cachedEvents, blockNumber); - - if (!isCacheValid) { - process.exit(1); - } - - const wasUpdated = await this.stakingModuleListWasUpdated(); - if (wasUpdated) { - this.logger.log('Staking module list was updated. Deleting cache'); - await this.levelDBCacheService.deleteCache(); - } - - await this.updateEventsCache(); + public async initialize(currentStakingModulesAddresses: string[]) { + await this.store.initialize(); + await this.updateEventsCache(currentStakingModulesAddresses); } /** @@ -80,40 +56,62 @@ export class SigningKeyEventsCacheService { * * @returns {Promise} The block number up to which the cache has been updated. */ - public async updateEventsCache(): Promise { + public async updateEventsCache( + currentStakingModulesAddresses: string[], + ): Promise { const fetchTimeStart = performance.now(); - const [latestBlock, initialCache] = await Promise.all([ - this.providerService.getBlockNumber(), + const wasUpdated = await this.stakingModuleListWasUpdated( + currentStakingModulesAddresses, + ); + + if (wasUpdated) { + this.logger.log('Staking module list was updated. Deleting cache'); + await this.store.deleteCache(); + } + + const [finalizedBlock, initialCache] = await Promise.all([ + this.providerService.getBlock(this.finalizedTag), this.getCachedEvents(), ]); + const { number: finalizedBlockNumber } = finalizedBlock; const firstNotCachedBlock = initialCache.headers.endBlock + 1; - const toBlock = latestBlock - SIGNING_KEYS_EVENTS_CACHE_LAG_BLOCKS; const totalEventsCount = initialCache.data.length; let newEventsCount = 0; - const stakingModulesAddresses = await this.getStakingModules(); + // check that the cache is written to a block less than or equal to the current block + // otherwise we consider that the Ethereum node has started sending incorrect data + const isCacheValid = this.sanityChecker.verifyCacheBlock( + initialCache, + finalizedBlockNumber, + ); + + if (!isCacheValid) return; for ( let block = firstNotCachedBlock; - block <= toBlock; + block <= finalizedBlockNumber; block += FETCHING_EVENTS_STEP ) { const chunkStartBlock = block; - const chunkToBlock = Math.min(toBlock, block + FETCHING_EVENTS_STEP - 1); + const chunkToBlock = Math.min( + finalizedBlockNumber, + block + FETCHING_EVENTS_STEP - 1, + ); - const chunkEventGroup = await this.fetchEventsFallOver( + const chunkEventGroup = await this.fetcher.fetchEventsFallOver( chunkStartBlock, chunkToBlock, + currentStakingModulesAddresses, ); - await this.levelDBCacheService.insertEventsCacheBatch({ + await this.store.insertEventsCacheBatch({ headers: { ...initialCache.headers, // as we update staking modules addresses always before run of this method, we can update value on every iteration - stakingModulesAddresses: stakingModulesAddresses, + stakingModulesAddresses: currentStakingModulesAddresses, endBlock: chunkEventGroup.endBlock, }, data: chunkEventGroup.events, @@ -122,7 +120,7 @@ export class SigningKeyEventsCacheService { newEventsCount += chunkEventGroup.events.length; this.logger.log('Historical signing key add events are fetched', { - toBlock, + finalizedBlockNumber, startBlock: chunkStartBlock, endBlock: chunkToBlock, }); @@ -137,8 +135,6 @@ export class SigningKeyEventsCacheService { totalEventsCount: totalEventsCount + newEventsCount, fetchTime, }); - - return toBlock; } /** @@ -149,12 +145,12 @@ export class SigningKeyEventsCacheService { * * @returns {Promise} Return `true` if the staking modules list was updated, `false` otherwise. */ - public async stakingModuleListWasUpdated(): Promise { + public async stakingModuleListWasUpdated( + currentModules: string[], + ): Promise { const { headers: { stakingModulesAddresses: previousModules }, - } = await this.levelDBCacheService.getHeader(); - - const currentModules = await this.getStakingModules(); + } = await this.store.getHeader(); const wasUpdated = this.wasStakingModulesListUpdated( previousModules, @@ -198,93 +194,6 @@ export class SigningKeyEventsCacheService { return modulesWereDeleted || modulesWereAdded; } - /** - * Retrieves the list of staking module addresses. - * - * This method fetches the cached staking modules contracts and returns the list of staking module addresses. - * - * @returns {Promise} Array of staking module addresses. - */ - public async getStakingModules(): Promise { - const stakingModulesContracts = - await this.repositoryService.getCachedStakingModulesContracts(); - - return Object.keys(stakingModulesContracts); - } - - /** - * Fetches signing key events within a specified block range, with fallback mechanisms. - * If the request failed, it tries to repeat it or split it into two - * - * @param {number} startBlock - The starting block number of the range. - * @param {number} endBlock - The ending block number of the range. - * @returns {Promise} Events fetched within the specified block range - */ - public async fetchEventsFallOver( - startBlock: number, - endBlock: number, - ): Promise { - return await this.providerService.fetchEventsFallOver( - startBlock, - endBlock, - this.fetchEvents.bind(this), - ); - } - - /** - * Fetches signing key events within a specified block range from staking module contracts. - * - * @param {number} startBlock - The starting block number of the range. - * @param {number} endBlock - The ending block number of the range. - * @returns {Promise} Events fetched within the specified block range. - */ - public async fetchEvents( - startBlock: number, - endBlock: number, - ): Promise { - const stakingModulesContracts = - await this.repositoryService.getCachedStakingModulesContracts(); - - const events: SigningKeyEvent[] = []; - - await Promise.all( - Object.entries(stakingModulesContracts).map( - async ([address, { impl }]) => { - const filter = impl.filters['SigningKeyAdded(uint256,bytes)'](); - - const rawEvents = await impl.queryFilter( - filter, - startBlock, - endBlock, - ); - - const moduleEvents: SigningKeyEvent[] = rawEvents.map((rawEvent) => { - return { - operatorIndex: rawEvent.args[0].toNumber(), - key: rawEvent.args[1], - moduleAddress: address, - blockNumber: rawEvent.blockNumber, - logIndex: rawEvent.logIndex, - blockHash: rawEvent.blockHash, - }; - }); - - events.push(...moduleEvents); - - this.logger.log( - 'Fetched signing keys add events for staking module', - { - count: moduleEvents.length, - address, - }, - ); - }, - ), - ); - - return { events, startBlock, endBlock }; - } - /** * Retrieves signing key events data from the cache. * @@ -296,7 +205,7 @@ export class SigningKeyEventsCacheService { * containing the cached signing key events and their metadata. */ public async getCachedEvents(): Promise { - const { headers, data } = await this.levelDBCacheService.getEventsCache(); + const { headers, data } = await this.store.getEventsCache(); // default values is startBlock: 0, endBlock: 0 const deploymentBlock = await this.getDeploymentBlockByNetwork(); @@ -324,7 +233,7 @@ export class SigningKeyEventsCacheService { keys: string[], ): Promise { const uniqueKeys = Array.from(new Set(keys)); - return await this.levelDBCacheService.getCachedEvents(uniqueKeys); + return await this.store.getCachedEvents(uniqueKeys); } /** @@ -346,20 +255,37 @@ export class SigningKeyEventsCacheService { const endBlock = blockNumber; const cachedEvents = await this.getEventsForOperatorsKeys([key]); - const isCacheValid = this.validateCacheBlock(cachedEvents, blockNumber); - if (!isCacheValid) process.exit(1); + const isCacheValid = this.sanityChecker.verifyCacheBlock( + cachedEvents, + blockNumber, + ); + + if (!isCacheValid) { + throw new Error( + `Signing key events cache is newer than the current block: ${blockNumber}`, + ); + } const firstNotCachedBlock = cachedEvents.headers.endBlock + 1; - const freshEventGroup = await this.fetchEventsFallOver( + const freshEventGroup = await this.fetcher.fetchEventsFallOver( firstNotCachedBlock, endBlock, + cachedEvents.headers.stakingModulesAddresses, ); const freshEvents = freshEventGroup.events; const lastEvent = freshEvents[freshEvents.length - 1]; const lastEventBlockHash = lastEvent?.blockHash; - this.checkEventsBlockHash(freshEvents, blockNumber, blockHash); + const isValid = this.sanityChecker.checkEventsBlockHash( + freshEvents, + blockNumber, + blockHash, + ); + + if (!isValid) { + throw new Error(`Reorganization found on block ${blockNumber}`); + } this.logger.debug?.('Fresh signing key add events are fetched', { events: freshEvents.length, @@ -403,75 +329,13 @@ export class SigningKeyEventsCacheService { public async setCachedEvents( cachedEvents: SigningKeyEventsCache, ): Promise { - await this.levelDBCacheService.deleteCache(); - await this.levelDBCacheService.insertEventsCacheBatch({ + await this.store.deleteCache(); + await this.store.insertEventsCacheBatch({ data: cachedEvents.data, headers: cachedEvents.headers, }); } - /** - * Validates the block number in the cached events against the current block number. - * - * This method checks if the cached events are up to date by comparing the current block number - * with the end block number in the cache. It logs a message if the cache is valid and a warning if it is not. - * - * @param {SigningKeyEventsCache} cachedEvents - The cached events containing block headers to validate. - * @param {number} currentBlock - The current block number to compare against the cached block. - * @returns {boolean} `true` if the cache is valid (i.e., the current block number is greater than or equal to the cached end block), `false` otherwise. - */ - public validateCacheBlock( - cachedEvents: SigningKeyEventsCache, - currentBlock: number, - ): boolean { - const isCacheValid = currentBlock >= cachedEvents.headers.endBlock; - - const blocks = { - cachedStartBlock: cachedEvents.headers.startBlock, - cachedEndBlock: cachedEvents.headers.endBlock, - currentBlock, - }; - - if (isCacheValid) { - this.logger.log('Signing keys events cache has valid age', blocks); - } - - if (!isCacheValid) { - this.logger.warn( - 'Signing key events cache is newer than the current block', - blocks, - ); - } - - return isCacheValid; - } - - /** - * Validates the block hash of signing key events. - * - * This method checks each event's block hash against the provided block hash, but only if the event's block number - * matches the given `blockNumber`. This ensures that the events are not from an alternate chain (e.g., due to a chain reorganization). - * If a block number match is found but the block hashes do not match, an error is thrown. - * - * @param {SigningKeyEvent[]} events - The list of signing key events to be checked. - * @param {number} blockNumber - The block number to match against the events' block numbers. - * @param {string} blockHash - The block hash to match against the events' block hashes. - * @throws {Error} If any event's block hash does not match the provided block hash for the specified block number. - */ - public checkEventsBlockHash( - events: SigningKeyEvent[], - blockNumber: number, - blockHash: string, - ): void { - events.forEach((event) => { - if (event.blockNumber === blockNumber && event.blockHash !== blockHash) { - throw new Error( - 'Blockhash of the received events does not match the current blockhash', - ); - } - }); - } - /** * Retrieves the block number when the curated module contract was deployed for the current network. * @@ -484,7 +348,7 @@ export class SigningKeyEventsCacheService { public async getDeploymentBlockByNetwork(): Promise { const chainId = await this.providerService.getChainId(); - const block = CURATED_MODULE_DEPLOYMENT_BLOCK_NETWORK[chainId]; + const block = EARLIEST_MODULE_DEPLOYMENT_BLOCK_NETWORK[chainId]; if (block == null) throw new Error(`Chain ${chainId} is not supported`); return block; diff --git a/src/contracts/signing-key-events-cache/signing-key-events-cache.spec.ts b/src/contracts/signing-keys-registry/signing-keys-registry.spec.ts similarity index 73% rename from src/contracts/signing-key-events-cache/signing-key-events-cache.spec.ts rename to src/contracts/signing-keys-registry/signing-keys-registry.spec.ts index e704dff1..9352293b 100644 --- a/src/contracts/signing-key-events-cache/signing-key-events-cache.spec.ts +++ b/src/contracts/signing-keys-registry/signing-keys-registry.spec.ts @@ -1,27 +1,30 @@ import { Test } from '@nestjs/testing'; +import { Block } from '@ethersproject/abstract-provider'; import { MockProviderModule, ProviderService } from 'provider'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; import { RepositoryModule, RepositoryService } from 'contracts/repository'; -import { LevelDBModule, LevelDBService } from './leveldb'; +import { SigningKeysStoreService, SigningKeysStoreModule } from './store'; import { mockRepository } from 'contracts/repository/repository.mock'; import { LocatorService } from 'contracts/repository/locator/locator.service'; import { mockLocator } from 'contracts/repository/locator/locator.mock'; -import { cacheMock, newEvent } from './leveldb/leveldb.fixtures'; -import { SigningKeyEventsCacheModule } from './signing-key-events-cache.module'; -import { SigningKeyEventsCacheService } from './signing-key-events-cache.service'; -import { StakingModule } from 'contracts/repository/interfaces/staking-module'; +import { cacheMock, newEvent } from './store/store.fixtures'; +import { SigningKeysRegistryModule } from './signing-keys-registry.module'; +import { SigningKeysRegistryService } from './signing-keys-registry.service'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { SigningKeysRegistryFetcherService } from './fetcher'; -describe('SigningKeyEventsCacheService', () => { +describe('SigningKeysRegistryService', () => { const defaultCacheValue = { headers: {}, data: [] as any[], }; - let dbService: LevelDBService; + let dbService: SigningKeysStoreService; let repositoryService: RepositoryService; let locatorService: LocatorService; - let signingkeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; + let signingKeysFetch: SigningKeysRegistryFetcherService; let providerService: ProviderService; beforeEach(async () => { @@ -30,22 +33,27 @@ describe('SigningKeyEventsCacheService', () => { ConfigModule.forRoot(), MockProviderModule.forRoot(), RepositoryModule, - LevelDBModule.register( + SigningKeysStoreModule.register( defaultCacheValue, 'leveldb-spec', 'signing-keys-spec', ), LoggerModule, - SigningKeyEventsCacheModule, + SigningKeysRegistryModule.register('latest'), ], }).compile(); - dbService = moduleRef.get(LevelDBService); + dbService = moduleRef.get(SigningKeysStoreService); repositoryService = moduleRef.get(RepositoryService); locatorService = moduleRef.get(LocatorService); - signingkeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); + signingKeysFetch = moduleRef.get(SigningKeysRegistryFetcherService); providerService = moduleRef.get(ProviderService); + const loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); + jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); + mockLocator(locatorService); await mockRepository(repositoryService); await dbService.initialize(); @@ -70,7 +78,7 @@ describe('SigningKeyEventsCacheService', () => { const endBlock = newEvent.blockNumber + 2000; // (10 - (newEvent.blockNumber % 10)); jest - .spyOn(signingkeyEventsCacheService, 'fetchEventsFallOver') + .spyOn(signingKeysFetch, 'fetchEventsFallOver') .mockImplementation(async () => { return { events: [...cacheMock.data, newEvent], @@ -83,36 +91,22 @@ describe('SigningKeyEventsCacheService', () => { }; }); - jest - .spyOn(providerService, 'getBlockNumber') - .mockImplementation(async () => { - return endBlock; - }); - - const record: Record = {}; - - [ - ...cacheMock.headers.stakingModulesAddresses, - newEvent.moduleAddress, - ].forEach((key) => { - record[key] = {} as StakingModule; + jest.spyOn(providerService, 'getBlock').mockImplementation(async () => { + return { number: endBlock } as Block; }); jest - .spyOn(repositoryService, 'getCachedStakingModulesContracts') - .mockImplementation(() => { - return record; - }); - - jest - .spyOn(signingkeyEventsCacheService, 'getDeploymentBlockByNetwork') + .spyOn(signingKeysRegistryService, 'getDeploymentBlockByNetwork') .mockImplementation(async () => { return expected.headers.startBlock; }); const deleteCache = jest.spyOn(dbService, 'deleteCache'); - await signingkeyEventsCacheService.handleNewBlock(endBlock); + await signingKeysRegistryService.handleNewBlock([ + ...cacheMock.headers.stakingModulesAddresses, + newEvent.moduleAddress, + ]); expect(deleteCache).toBeCalledTimes(1); @@ -170,11 +164,10 @@ describe('SigningKeyEventsCacheService', () => { )}, currentModules = ${JSON.stringify( testCase.currentModules, )}, expected = ${testCase.expected}`, () => { - const result = - signingkeyEventsCacheService.wasStakingModulesListUpdated( - testCase.previousModules, - testCase.currentModules, - ); + const result = signingKeysRegistryService.wasStakingModulesListUpdated( + testCase.previousModules, + testCase.currentModules, + ); expect(result).toEqual(testCase.expected); }); diff --git a/src/contracts/signing-keys-registry/store/index.ts b/src/contracts/signing-keys-registry/store/index.ts new file mode 100644 index 00000000..99322182 --- /dev/null +++ b/src/contracts/signing-keys-registry/store/index.ts @@ -0,0 +1,3 @@ +export * from './store.constants'; +export * from './store.module'; +export * from './store.service'; diff --git a/src/contracts/signing-key-events-cache/leveldb/leveldb.constants.ts b/src/contracts/signing-keys-registry/store/store.constants.ts similarity index 100% rename from src/contracts/signing-key-events-cache/leveldb/leveldb.constants.ts rename to src/contracts/signing-keys-registry/store/store.constants.ts diff --git a/src/contracts/signing-key-events-cache/leveldb/leveldb.fixtures.ts b/src/contracts/signing-keys-registry/store/store.fixtures.ts similarity index 100% rename from src/contracts/signing-key-events-cache/leveldb/leveldb.fixtures.ts rename to src/contracts/signing-keys-registry/store/store.fixtures.ts diff --git a/src/contracts/signing-key-events-cache/leveldb/leveldb.module.ts b/src/contracts/signing-keys-registry/store/store.module.ts similarity index 67% rename from src/contracts/signing-key-events-cache/leveldb/leveldb.module.ts rename to src/contracts/signing-keys-registry/store/store.module.ts index 629a7a88..8aef1fdc 100644 --- a/src/contracts/signing-key-events-cache/leveldb/leveldb.module.ts +++ b/src/contracts/signing-keys-registry/store/store.module.ts @@ -1,20 +1,20 @@ import { DynamicModule, Module } from '@nestjs/common'; import { ProviderModule } from 'provider'; -import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './leveldb.constants'; -import { LevelDBService } from './leveldb.service'; +import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './store.constants'; +import { SigningKeysStoreService } from './store.service'; @Module({}) -export class LevelDBModule { +export class SigningKeysStoreModule { static register( defaultValue: unknown, cacheDir = 'cache', cacheLayerDir = 'add-sign-keys-cache', ): DynamicModule { return { - module: LevelDBModule, + module: SigningKeysStoreModule, imports: [ProviderModule], providers: [ - LevelDBService, + SigningKeysStoreService, { provide: DB_DIR, useValue: cacheDir, @@ -28,7 +28,7 @@ export class LevelDBModule { useValue: cacheLayerDir, }, ], - exports: [LevelDBService], + exports: [SigningKeysStoreService], }; } } diff --git a/src/contracts/signing-key-events-cache/leveldb/leveldb.service.spec.ts b/src/contracts/signing-keys-registry/store/store.service.spec.ts similarity index 84% rename from src/contracts/signing-key-events-cache/leveldb/leveldb.service.spec.ts rename to src/contracts/signing-keys-registry/store/store.service.spec.ts index 7f874fcd..08cad728 100644 --- a/src/contracts/signing-key-events-cache/leveldb/leveldb.service.spec.ts +++ b/src/contracts/signing-keys-registry/store/store.service.spec.ts @@ -2,9 +2,9 @@ import { Test } from '@nestjs/testing'; import { MockProviderModule } from 'provider'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; -import { LevelDBModule } from './leveldb.module'; -import { LevelDBService } from './leveldb.service'; -import { cacheMock, eventsMock1, keyMock1 } from './leveldb.fixtures'; +import { SigningKeysStoreModule } from './store.module'; +import { SigningKeysStoreService } from './store.service'; +import { cacheMock, eventsMock1, keyMock1 } from './store.fixtures'; describe('dbService', () => { const defaultCacheValue = { @@ -12,14 +12,14 @@ describe('dbService', () => { data: [] as any[], }; - let dbService: LevelDBService; + let dbService: SigningKeysStoreService; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ imports: [ ConfigModule.forRoot(), MockProviderModule.forRoot(), - LevelDBModule.register( + SigningKeysStoreModule.register( defaultCacheValue, 'leveldb-spec', 'signing-keys-spec', @@ -28,7 +28,7 @@ describe('dbService', () => { ], }).compile(); - dbService = moduleRef.get(LevelDBService); + dbService = moduleRef.get(SigningKeysStoreService); await dbService.initialize(); }); diff --git a/src/contracts/signing-key-events-cache/leveldb/leveldb.service.ts b/src/contracts/signing-keys-registry/store/store.service.ts similarity index 96% rename from src/contracts/signing-key-events-cache/leveldb/leveldb.service.ts rename to src/contracts/signing-keys-registry/store/store.service.ts index 361d9e34..4a2755dd 100644 --- a/src/contracts/signing-key-events-cache/leveldb/leveldb.service.ts +++ b/src/contracts/signing-keys-registry/store/store.service.ts @@ -1,13 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { Level } from 'level'; import { join } from 'path'; -import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './leveldb.constants'; +import { DB_DIR, DB_DEFAULT_VALUE, DB_LAYER_DIR } from './store.constants'; import { ProviderService } from 'provider'; import { SigningKeyEvent } from '../interfaces/event.interface'; import { SigningKeyEventsCacheHeaders } from '../interfaces/cache.interface'; @Injectable() -export class LevelDBService { +export class SigningKeysStoreService { private db!: Level; constructor( private providerService: ProviderService, @@ -170,7 +170,7 @@ export class LevelDBService { * @returns {string} The serialized JSON string of the signing key event. * @public */ - public serializeEventDate(signingKeyEvent: SigningKeyEvent) { + public serializeEventData(signingKeyEvent: SigningKeyEvent) { return JSON.stringify(signingKeyEvent); } @@ -195,7 +195,7 @@ export class LevelDBService { const ops = records.data.map((event) => ({ type: 'put' as const, key: this.generateSigningKeyEventStorageKey(event), - value: this.serializeEventDate(event), + value: this.serializeEventData(event), })); ops.push({ type: 'put', diff --git a/src/staking-router/index.ts b/src/contracts/staking-router/index.ts similarity index 100% rename from src/staking-router/index.ts rename to src/contracts/staking-router/index.ts diff --git a/src/contracts/staking-router/staking-router.module.ts b/src/contracts/staking-router/staking-router.module.ts new file mode 100644 index 00000000..cf80647e --- /dev/null +++ b/src/contracts/staking-router/staking-router.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { StakingRouterService } from './staking-router.service'; + +@Module({ + providers: [StakingRouterService], + exports: [StakingRouterService], +}) +export class StakingRouterModule {} diff --git a/src/contracts/lido/lido.service.spec.ts b/src/contracts/staking-router/staking-router.service.spec.ts similarity index 63% rename from src/contracts/lido/lido.service.spec.ts rename to src/contracts/staking-router/staking-router.service.spec.ts index cf3f8df8..6271374d 100644 --- a/src/contracts/lido/lido.service.spec.ts +++ b/src/contracts/staking-router/staking-router.service.spec.ts @@ -2,22 +2,22 @@ import { Test } from '@nestjs/testing'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; import { MockProviderModule, ProviderService } from 'provider'; -import { LidoAbi__factory } from 'generated'; import { RepositoryModule, RepositoryService } from 'contracts/repository'; import { Interface } from '@ethersproject/abi'; -import { LidoService } from './lido.service'; -import { LidoModule } from './lido.module'; import { LocatorService } from 'contracts/repository/locator/locator.service'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { mockLocator } from 'contracts/repository/locator/locator.mock'; import { mockRepository } from 'contracts/repository/repository.mock'; +import { StakingRouterAbi__factory } from 'generated'; +import { StakingRouterModule, StakingRouterService } from '.'; + +const TEST_MODULE_ID = 1; describe('SecurityService', () => { - let lidoService: LidoService; let providerService: ProviderService; - let repositoryService: RepositoryService; let locatorService: LocatorService; + let stakingRouterService: StakingRouterService; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -25,16 +25,16 @@ describe('SecurityService', () => { ConfigModule.forRoot(), MockProviderModule.forRoot(), LoggerModule, - LidoModule, RepositoryModule, + StakingRouterModule, ], }).compile(); - lidoService = moduleRef.get(LidoService); providerService = moduleRef.get(ProviderService); - repositoryService = moduleRef.get(RepositoryService); locatorService = moduleRef.get(LocatorService); + stakingRouterService = moduleRef.get(StakingRouterService); + jest .spyOn(moduleRef.get(WINSTON_MODULE_NEST_PROVIDER), 'log') .mockImplementation(() => undefined); @@ -43,6 +43,27 @@ describe('SecurityService', () => { await mockRepository(repositoryService); }); + describe('isDepositsPaused', () => { + it('should call contract method', async () => { + const expected = true; + + const mockProviderCalla = jest + .spyOn(providerService.provider, 'call') + .mockImplementation(async () => { + const iface = new Interface(StakingRouterAbi__factory.abi); + return iface.encodeFunctionResult('getStakingModuleIsActive', [ + expected, + ]); + }); + + const isPaused = await stakingRouterService.isModuleDepositsPaused( + TEST_MODULE_ID, + ); + expect(isPaused).toBe(!expected); + expect(mockProviderCalla).toBeCalledTimes(1); + }); + }); + describe('getWithdrawalCredentials', () => { it('should return withdrawal credentials', async () => { const expected = '0x' + '1'.repeat(64); @@ -50,12 +71,12 @@ describe('SecurityService', () => { const mockProviderCall = jest .spyOn(providerService.provider, 'call') .mockImplementation(async () => { - const iface = new Interface(LidoAbi__factory.abi); + const iface = new Interface(StakingRouterAbi__factory.abi); const result = [expected]; return iface.encodeFunctionResult('getWithdrawalCredentials', result); }); - const wc = await lidoService.getWithdrawalCredentials(); + const wc = await await stakingRouterService.getWithdrawalCredentials(); expect(wc).toBe(expected); expect(mockProviderCall).toBeCalledTimes(1); }); diff --git a/src/contracts/staking-router/staking-router.service.ts b/src/contracts/staking-router/staking-router.service.ts new file mode 100644 index 00000000..94ac725f --- /dev/null +++ b/src/contracts/staking-router/staking-router.service.ts @@ -0,0 +1,103 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { RepositoryService } from 'contracts/repository'; +import { IStakingModuleAbi__factory } from 'generated'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { BlockTag, ProviderService } from 'provider'; + +@Injectable() +export class StakingRouterService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + private providerService: ProviderService, + private repositoryService: RepositoryService, + ) {} + + /** + * @param blockTag + * @returns List of staking modules fetched from the SR contract + */ + public async getStakingModules(blockTag: BlockTag) { + const stakingRouter = + await this.repositoryService.getCachedStakingRouterContract(); + const stakingModules = await stakingRouter.getStakingModules({ + blockTag: blockTag as any, + }); + + return stakingModules; + } + + /** + * Retrieves the list of staking module addresses. + * This method fetches the cached staking modules contracts and returns the list of staking module addresses. + * @param blockHash - Block hash + * @returns Array of staking module addresses. + */ + public async getStakingModulesAddresses( + blockHash: string, + ): Promise { + const stakingModules = await this.getStakingModules({ blockHash }); + + return stakingModules.map( + (stakingModule) => stakingModule.stakingModuleAddress, + ); + } + + /** + * Retrieves contract factory + * @param stakingModuleAddress Staking module address + * @returns Contract factory + */ + public async getStakingModule(stakingModuleAddress: string) { + return IStakingModuleAbi__factory.connect( + stakingModuleAddress, + this.providerService.provider, + ); + } + + /** + * Retrieves SigningKeyAdded events list + * @param startBlock - Start block for fetching events + * @param endBlock - End block for fetching events + * @param stakingModuleAddress - Staking module address + * @returns List of SigningKeyAdded events + */ + public async getSigningKeyAddedEvents( + startBlock: number, + endBlock: number, + stakingModuleAddress: string, + ) { + const contract = await this.getStakingModule(stakingModuleAddress); + const filter = contract.filters['SigningKeyAdded(uint256,bytes)'](); + + return await contract.queryFilter(filter, startBlock, endBlock); + } + + /** + * Returns the current state of deposits for module + */ + public async isModuleDepositsPaused( + stakingModuleId: number, + blockTag?: BlockTag, + ): Promise { + const stakingRouterContract = + await this.repositoryService.getCachedStakingRouterContract(); + + const isActive = await stakingRouterContract.getStakingModuleIsActive( + stakingModuleId, + { + blockTag: blockTag as any, + }, + ); + + return !isActive; + } + + public async getWithdrawalCredentials(blockTag?: BlockTag): Promise { + const stakingRouterContract = + await this.repositoryService.getCachedStakingRouterContract(); + + return await stakingRouterContract.getWithdrawalCredentials({ + blockTag: blockTag as any, + }); + } +} diff --git a/src/guardian/block-data-collector/block-data-collector.module.ts b/src/guardian/block-data-collector/block-data-collector.module.ts new file mode 100644 index 00000000..0e6291af --- /dev/null +++ b/src/guardian/block-data-collector/block-data-collector.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { DepositsRegistryModule } from 'contracts/deposits-registry'; +import { SecurityModule } from 'contracts/security'; +import { StakingModuleGuardModule } from 'guardian/staking-module-guard'; +import { WalletModule } from 'wallet'; +import { StakingRouterModule } from 'contracts/staking-router'; +import { BlockDataCollectorService } from './block-data-collector.service'; + +@Module({ + imports: [ + DepositsRegistryModule.register(), + SecurityModule, + StakingModuleGuardModule, + WalletModule, + StakingRouterModule, + ], + providers: [BlockDataCollectorService], + exports: [BlockDataCollectorService], +}) +export class BlockDataCollectorModule {} diff --git a/src/guardian/block-guard/block-guard.service.ts b/src/guardian/block-data-collector/block-data-collector.service.ts similarity index 73% rename from src/guardian/block-guard/block-guard.service.ts rename to src/guardian/block-data-collector/block-data-collector.service.ts index c15587ed..07aa0afd 100644 --- a/src/guardian/block-guard/block-guard.service.ts +++ b/src/guardian/block-data-collector/block-data-collector.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { DepositService } from 'contracts/deposit'; +import { DepositRegistryService } from 'contracts/deposits-registry'; import { SecurityService } from 'contracts/security'; import { BlockData } from '../interfaces'; @@ -12,14 +12,12 @@ import { METRIC_BLOCK_DATA_REQUEST_ERRORS, } from 'common/prometheus'; import { Counter, Histogram } from 'prom-client'; -import { LidoService } from 'contracts/lido'; import { StakingModuleGuardService } from 'guardian/staking-module-guard'; import { WalletService } from 'wallet'; +import { StakingRouterService } from 'contracts/staking-router'; @Injectable() -export class BlockGuardService { - protected lastProcessedStateMeta?: { blockHash: string; blockNumber: number }; - +export class BlockDataCollectorService { constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, @@ -32,41 +30,13 @@ export class BlockGuardService { private walletService: WalletService, - private depositService: DepositService, + private depositService: DepositRegistryService, private securityService: SecurityService, - private lidoService: LidoService, + private stakingRouterService: StakingRouterService, private stakingModuleGuardService: StakingModuleGuardService, ) {} - public isNeedToProcessNewState(newMeta: { - blockHash: string; - blockNumber: number; - }) { - const lastMeta = this.lastProcessedStateMeta; - if (!lastMeta) return true; - if (lastMeta.blockNumber > newMeta.blockNumber) { - this.logger.error('Keys-api returns old state', newMeta); - return false; - } - const isSameBlock = lastMeta.blockHash !== newMeta.blockHash; - - if (!isSameBlock) { - this.logger.log(`The block has not changed since the last cycle. Exit`, { - newMeta, - }); - } - - return isSameBlock; - } - - public setLastProcessedStateMeta(newMeta: { - blockHash: string; - blockNumber: number; - }) { - this.lastProcessedStateMeta = newMeta; - } - /** * Collects data from contracts in one place and by block hash, * to reduce the probability of getting data from different blocks @@ -88,14 +58,16 @@ export class BlockGuardService { guardianIndex, lidoWC, securityVersion, + walletBalanceCritical, ] = await Promise.all([ this.depositService.getDepositRoot({ blockHash }), this.depositService.getAllDepositedEvents(blockNumber, blockHash), this.securityService.getGuardianIndex({ blockHash }), - this.lidoService.getWithdrawalCredentials({ blockHash }), + this.stakingRouterService.getWithdrawalCredentials({ blockHash }), this.securityService.version({ blockHash, }), + this.walletService.isBalanceCritical(), ]); const theftHappened = @@ -116,9 +88,6 @@ export class BlockGuardService { }); } - const walletBalanceCritical = - await this.walletService.isBalanceCritical(); - return { blockNumber, blockHash, @@ -146,7 +115,7 @@ export class BlockGuardService { securityVersion: number, ) { if (securityVersion === 3) { - const alreadyPaused = await this.securityService.isDepositContractPaused({ + const alreadyPaused = await this.securityService.isDepositsPaused({ blockHash, }); diff --git a/src/guardian/block-data-collector/index.ts b/src/guardian/block-data-collector/index.ts new file mode 100644 index 00000000..4013ae8e --- /dev/null +++ b/src/guardian/block-data-collector/index.ts @@ -0,0 +1,2 @@ +export * from './block-data-collector.module'; +export * from './block-data-collector.service'; diff --git a/src/guardian/block-guard/block-guard.module.ts b/src/guardian/block-guard/block-guard.module.ts deleted file mode 100644 index 2a3d51e0..00000000 --- a/src/guardian/block-guard/block-guard.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from '@nestjs/common'; -import { DepositModule } from 'contracts/deposit'; -import { SecurityModule } from 'contracts/security'; -import { BlockGuardService } from './block-guard.service'; -import { LidoModule } from 'contracts/lido'; -import { StakingModuleGuardModule } from 'guardian/staking-module-guard'; -import { WalletModule } from 'wallet'; - -@Module({ - imports: [ - LidoModule, - DepositModule, - SecurityModule, - StakingModuleGuardModule, - WalletModule, - ], - providers: [BlockGuardService], - exports: [BlockGuardService], -}) -export class BlockGuardModule {} diff --git a/src/guardian/block-guard/index.ts b/src/guardian/block-guard/index.ts deleted file mode 100644 index 78e3eaf6..00000000 --- a/src/guardian/block-guard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './block-guard.module'; -export * from './block-guard.service'; diff --git a/src/guardian/duplicates/keys-duplication-checker.module.ts b/src/guardian/duplicates/keys-duplication-checker.module.ts index 3580014b..a5ba4528 100644 --- a/src/guardian/duplicates/keys-duplication-checker.module.ts +++ b/src/guardian/duplicates/keys-duplication-checker.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { SigningKeyEventsCacheModule } from 'contracts/signing-key-events-cache'; +import { SigningKeysRegistryModule } from 'contracts/signing-keys-registry'; import { KeysDuplicationCheckerService } from './keys-duplication-checker.service'; @Module({ - imports: [SigningKeyEventsCacheModule], + imports: [SigningKeysRegistryModule.register()], providers: [KeysDuplicationCheckerService], exports: [KeysDuplicationCheckerService], }) diff --git a/src/guardian/duplicates/keys-duplication-checker.service.spec.ts b/src/guardian/duplicates/keys-duplication-checker.service.spec.ts index 92059bb4..ccd25324 100644 --- a/src/guardian/duplicates/keys-duplication-checker.service.spec.ts +++ b/src/guardian/duplicates/keys-duplication-checker.service.spec.ts @@ -1,262 +1,431 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LoggerModule } from 'common/logger'; import { - SigningKeyEventsCacheModule, - SigningKeyEventsCacheService, -} from 'contracts/signing-key-events-cache'; + SigningKeysRegistryModule, + SigningKeysRegistryService, +} from 'contracts/signing-keys-registry'; import { KeysDuplicationCheckerModule } from './keys-duplication-checker.module'; import { KeysDuplicationCheckerService } from './keys-duplication-checker.service'; -import { - eventMock, - keyMock1, - keyMock1Duplicate, - keysMock, -} from './keys.fixtures'; +import { eventMock1, keyMock1, keyMock2 } from './keys.fixtures'; import { ConfigModule } from 'common/config'; import { MockProviderModule } from 'provider'; import { BlockData } from 'guardian/interfaces'; +import { StakingRouterModule } from 'contracts/staking-router'; +import { RepositoryModule } from 'contracts/repository'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; describe('KeysDuplicationCheckerService', () => { let service: KeysDuplicationCheckerService; - const mockSigningKeyEventsCacheService = { + const mockSigningKeysRegistryService = { getUpdatedSigningKeyEvents: jest.fn(), }; + const emptyBlockData = {} as BlockData; + beforeEach(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ imports: [ + StakingRouterModule, + RepositoryModule, ConfigModule.forRoot(), MockProviderModule.forRoot(), LoggerModule, KeysDuplicationCheckerModule, - SigningKeyEventsCacheModule, + SigningKeysRegistryModule.register('latest'), ], }) - .overrideProvider(SigningKeyEventsCacheService) - .useValue(mockSigningKeyEventsCacheService) + .overrideProvider(SigningKeysRegistryService) + .useValue(mockSigningKeysRegistryService) .compile(); service = moduleRef.get( KeysDuplicationCheckerService, ); + + const loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); + jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); }); - describe('findDuplicateKeys', () => { + describe('getDuplicateKeyGroups', () => { it('should identify and return tuples of duplicated keys along with their occurrences', () => { - const result = service.findDuplicateKeys(keysMock); - const expectedKey = - '0xb3c90525010a5710d43acbea46047fc37ed55306d032527fa15dd7e8cd8a9a5fa490347cc5fce59936fb8300683cd9f3'; - const expectedOccurrences = keysMock.filter( - (key) => key.key === expectedKey, - ); + const result = service.getDuplicateKeyGroups([ + { ...keyMock1, index: 1 }, + { ...keyMock1, index: 2 }, + { ...keyMock2, index: 3 }, + { ...keyMock2, index: 4 }, + ]); // Check the number of groups of duplicated keys identified - expect(result.length).toEqual(1); + expect(result.length).toEqual(2); - const [key, occurrences] = result[0]; - expect(key).toEqual(expectedKey); - expect(occurrences.length).toEqual(2); - expect(occurrences).toEqual(expect.arrayContaining(expectedOccurrences)); + expect(result[0][0]).toEqual(keyMock1.key); + expect(result[0][1].length).toEqual(2); + expect(result[0][1]).toEqual([ + { ...keyMock1, index: 1 }, + { ...keyMock1, index: 2 }, + ]); + + expect(result[1][0]).toEqual(keyMock2.key); + expect(result[1][1].length).toEqual(2); + expect(result[1][1]).toEqual([ + { ...keyMock2, index: 3 }, + { ...keyMock2, index: 4 }, + ]); }); }); describe('getDuplicatedKeys', () => { - it('duplicates across one operator', async () => { - const result = await service.getDuplicatedKeys(keysMock, {} as BlockData); + describe('Detect duplicates within a single operator', () => { + it('Returns unused keys as duplicates if the list contains a deposited key', async () => { + // will be return key with smallest index + // deposited keys has a smallest index + const unusedKey = { ...keyMock1, index: 2, used: false }; + const usedKey = { ...keyMock1, index: 1, used: true }; + const duplicatedKeysAmongSingleOperator = [unusedKey, usedKey]; - const expected = { duplicates: [keyMock1Duplicate], unresolved: [] }; + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongSingleOperator, + emptyBlockData, + ); - expect(result.duplicates.length).toEqual(1); - expect(result.duplicates).toEqual( - expect.arrayContaining(expected.duplicates), - ); - expect(result.unresolved).toEqual([]); - }); + expect(result.duplicates).toEqual([unusedKey]); + expect(result.unresolved).toEqual([]); + }); - it('original key is deposited', async () => { - const result = await service.getDuplicatedKeys( - [ - ...keysMock, - { - ...keyMock1Duplicate, - used: true, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - }, - ], - {} as BlockData, - ); - - const expected = { - duplicates: [keyMock1, keyMock1Duplicate], - unresolved: [], - }; - - expect(result.duplicates).toEqual( - expect.arrayContaining(expected.duplicates), - ); - expect(result.duplicates[0].used).toBeFalsy(); - expect(result.duplicates[1].used).toBeFalsy(); - - expect(result.unresolved).toEqual([]); - }); + it('Identifies the key with the smallest index as the earliest and returns the others as duplicates', async () => { + const unusedKey1 = { ...keyMock1, index: 1, used: false }; + const unusedKey2 = { ...keyMock1, index: 2, used: false }; + const duplicatedKeysAmongSingleOperator = [unusedKey1, unusedKey2]; - it('original key is deposited and from another module', async () => { - const result = await service.getDuplicatedKeys( - [ - ...keysMock, - { - ...keyMock1Duplicate, - used: true, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - moduleAddress: '0x12344556', - }, - ], - {} as BlockData, - ); - - const expected = { - duplicates: [keyMock1, keyMock1Duplicate], - unresolved: [], - }; - - expect(result.duplicates).toEqual( - expect.arrayContaining(expected.duplicates), - ); - expect(result.duplicates[0].used).toBeFalsy(); - expect(result.duplicates[1].used).toBeFalsy(); - - expect(result.unresolved).toEqual([]); + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongSingleOperator, + emptyBlockData, + ); + + expect(result.duplicates).toEqual([unusedKey2]); + expect(result.unresolved).toEqual([]); + }); }); - describe('duplicate across two operators', () => { - it('keys were added in different blocks', async () => { - mockSigningKeyEventsCacheService.getUpdatedSigningKeyEvents.mockImplementationOnce( - async () => { - return { - events: [ - eventMock, - { ...eventMock, logIndex: eventMock.logIndex + 1 }, - { - ...eventMock, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - blockNumber: eventMock.blockNumber + 1, - }, - ], - }; - }, - ); + describe('Detect duplicates across multiple operators within the same module', () => { + it('Returns unused keys as duplicates if the list contains a deposited key', async () => { + const unusedKey = { ...keyMock1, used: false, operatorIndex: 1 }; + const usedKey = { ...keyMock1, used: true, operatorIndex: 2 }; + const duplicatedKeysAmongMultipleOperator = [unusedKey, usedKey]; const result = await service.getDuplicatedKeys( - [ - ...keysMock, - { - ...keyMock1Duplicate, - used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - }, - ], - {} as BlockData, + duplicatedKeysAmongMultipleOperator, + emptyBlockData, ); - const expected = { - duplicates: [ - keyMock1Duplicate, - { - ...keyMock1Duplicate, - used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, + expect(result.duplicates).toEqual([unusedKey]); + expect(result.unresolved).toEqual([]); + }); + + describe('Detect duplicates based on SigningKeyAdded events', () => { + it('Returns all keys as unresolved if there is no event for operator', async () => { + const unusedKey1 = { ...keyMock1, used: false, operatorIndex: 1 }; + const unusedKey2 = { ...keyMock1, used: false, operatorIndex: 2 }; + + // unresolved will not influence detection of other keys duplicates + const unusedKey3 = { ...keyMock2, used: false, operatorIndex: 1 }; + const usedKeys = { ...keyMock2, used: true, operatorIndex: 2 }; + + const keyMock1Event = { + ...eventMock1, + operatorIndex: 1, + logIndex: 1, + blockNumber: 1, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event], + isValid: true, + }; }, - ], - unresolved: [], + ); + + const duplicatedKeysAmongMultipleOperators = [ + unusedKey1, + unusedKey2, + unusedKey3, + usedKeys, + ]; + + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleOperators, + emptyBlockData, + ); + + expect(result.duplicates).toEqual([unusedKey3]); + expect(result.unresolved).toEqual([unusedKey1, unusedKey2]); + }); + + it('Returns all keys as duplicates if multiple events occur in the smallest block', async () => { + const unusedKey1 = { ...keyMock1, used: false, operatorIndex: 1 }; + const unusedKey2 = { ...keyMock1, used: false, operatorIndex: 2 }; + + const keyMock1Event = { + ...eventMock1, + operatorIndex: 1, + logIndex: 1, + blockNumber: 1, + }; + + const keyMock2Event = { + ...eventMock1, + operatorIndex: 2, + logIndex: 2, + blockNumber: 1, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event, keyMock2Event], + isValid: true, + }; + }, + ); + + const duplicatedKeysAmongMultipleOperators = [unusedKey1, unusedKey2]; + + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleOperators, + emptyBlockData, + ); + + expect(result.duplicates).toEqual([unusedKey1, unusedKey2]); + expect(result.unresolved).toEqual([]); + }); + + it('Returns all keys as duplicates except the one with the smallest block number and key index', async () => { + const unusedKey1 = { + ...keyMock1, + index: 1, + used: false, + operatorIndex: 1, + }; + const unusedKey2 = { + ...keyMock1, + index: 2, + used: false, + operatorIndex: 1, + }; + const unusedKey3 = { ...keyMock1, used: false, operatorIndex: 2 }; + + const keyMock1Event = { + ...eventMock1, + operatorIndex: 1, + logIndex: 1, + blockNumber: 1, + }; + + const keyMock2Event = { + ...eventMock1, + operatorIndex: 2, + logIndex: 1, + blockNumber: 2, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event, keyMock2Event], + isValid: true, + }; + }, + ); + + const duplicatedKeysAmongMultipleOperators = [ + unusedKey1, + unusedKey2, + unusedKey3, + ]; + + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleOperators, + emptyBlockData, + ); + + expect(result.duplicates).toEqual([unusedKey2, unusedKey3]); + expect(result.unresolved).toEqual([]); + }); + }); + }); + + describe('Detect duplicates across multiple operators in different modules', () => { + it('Returns unused keys as duplicates if the list contains a deposited key', async () => { + const unusedKey = { + ...keyMock1, + used: false, + moduleAddress: 'address1', }; + const usedKey = { ...keyMock1, used: true, moduleAddress: 'address2' }; + const duplicatedKeysAmongMultipleModules = [unusedKey, usedKey]; - expect(result.duplicates).toEqual( - expect.arrayContaining(expected.duplicates), + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleModules, + emptyBlockData, ); + + expect(result.duplicates).toEqual([unusedKey]); expect(result.unresolved).toEqual([]); }); - it('keys were added in the same block', async () => { - mockSigningKeyEventsCacheService.getUpdatedSigningKeyEvents.mockImplementationOnce( - async () => { - return { - events: [ - eventMock, - { ...eventMock, logIndex: eventMock.logIndex + 1 }, - { - ...eventMock, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - logIndex: eventMock.logIndex + 2, - }, - ], - }; - }, - ); + describe('Detect duplicates based on SigningKeyAdded events', () => { + it('Return all keys as unresolved if there are no event for operator', async () => { + const unusedKey1 = { + ...keyMock1, + used: false, + moduleAddress: 'address1', + }; + const unusedKey2 = { + ...keyMock1, + used: false, + moduleAddress: 'address2', + }; - const result = await service.getDuplicatedKeys( - [ - ...keysMock, - { - ...keyMock1Duplicate, - used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, + // unresolved will not influence detection of other keys duplicates + const unusedKey3 = { ...keyMock2, used: false, operatorIndex: 1 }; + const usedKeys = { ...keyMock2, used: true, operatorIndex: 2 }; + + const keyMock1Event = { + ...eventMock1, + moduleAddress: 'address1', + logIndex: 1, + blockNumber: 1, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event], + isValid: true, + }; }, - ], - {} as BlockData, - ); + ); + + const duplicatedKeysAmongMultipleModules = [ + unusedKey1, + unusedKey2, + unusedKey3, + usedKeys, + ]; + + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleModules, + emptyBlockData, + ); - const expected = { - duplicates: [ - keyMock1Duplicate, - { - ...keyMock1Duplicate, - used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, + expect(result.duplicates).toEqual([unusedKey3]); + expect(result.unresolved).toEqual([unusedKey1, unusedKey2]); + }); + + it('Returns all keys as duplicates if multiple events occur in the smallest block', async () => { + const unusedKey1 = { + ...keyMock1, + used: false, + moduleAddress: 'address1', + }; + const unusedKey2 = { + ...keyMock1, + used: false, + moduleAddress: 'address2', + }; + + const keyMock1Event = { + ...eventMock1, + moduleAddress: 'address1', + logIndex: 1, + blockNumber: 1, + }; + + const keyMock2Event = { + ...eventMock1, + moduleAddress: 'address2', + logIndex: 2, + blockNumber: 1, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event, keyMock2Event], + isValid: true, + }; }, - ], - unresolved: [], - }; + ); - expect(result.duplicates).toEqual( - expect.arrayContaining(expected.duplicates), - ); - }); + const duplicatedKeysAmongMultipleModules = [unusedKey1, unusedKey2]; - it('should return unresolved keys list if no event for operator', async () => { - mockSigningKeyEventsCacheService.getUpdatedSigningKeyEvents.mockImplementationOnce( - async () => { - return { - events: [ - eventMock, - { ...eventMock, logIndex: eventMock.logIndex + 1 }, - ], - }; - }, - ); + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleModules, + emptyBlockData, + ); - const expected = [ - keyMock1, - keyMock1Duplicate, - { - ...keyMock1Duplicate, + expect(result.duplicates).toEqual([unusedKey1, unusedKey2]); + expect(result.unresolved).toEqual([]); + }); + + it('Returns all keys as duplicates except the one with the smallest block number and key index', async () => { + const unusedKey1 = { + ...keyMock1, + index: 1, + used: false, + moduleAddress: 'address1', + }; + const unusedKey2 = { + ...keyMock1, + index: 2, + used: false, + moduleAddress: 'address1', + }; + const unusedKey3 = { + ...keyMock1, used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, - }, - ]; - - const { duplicates, unresolved } = await service.getDuplicatedKeys( - [ - ...keysMock, - { - ...keyMock1Duplicate, - used: false, - operatorIndex: keyMock1Duplicate.operatorIndex + 1, + moduleAddress: 'address2', + }; + + const keyMock1Event = { + ...eventMock1, + moduleAddress: 'address1', + logIndex: 1, + blockNumber: 1, + }; + + const keyMock2Event = { + ...eventMock1, + moduleAddress: 'address2', + logIndex: 1, + blockNumber: 2, + }; + + mockSigningKeysRegistryService.getUpdatedSigningKeyEvents.mockImplementationOnce( + async () => { + return { + events: [keyMock1Event, keyMock2Event], + isValid: true, + }; }, - ], - {} as BlockData, - ); + ); + + const duplicatedKeysAmongMultipleModules = [ + unusedKey1, + unusedKey2, + unusedKey3, + ]; + + const result = await service.getDuplicatedKeys( + duplicatedKeysAmongMultipleModules, + emptyBlockData, + ); - expect(duplicates).toEqual([]); - expect(unresolved).toEqual(expect.arrayContaining(expected)); + expect(result.duplicates).toEqual([unusedKey2, unusedKey3]); + expect(result.unresolved).toEqual([]); + }); }); }); }); diff --git a/src/guardian/duplicates/keys-duplication-checker.service.ts b/src/guardian/duplicates/keys-duplication-checker.service.ts index 37272e37..a909b850 100644 --- a/src/guardian/duplicates/keys-duplication-checker.service.ts +++ b/src/guardian/duplicates/keys-duplication-checker.service.ts @@ -1,15 +1,22 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; -import { SigningKeyEvent } from 'contracts/signing-key-events-cache/interfaces/event.interface'; +import { + SigningKeyEvent, + SigningKeyEventsGroupWithStakingModules, +} from 'contracts/signing-keys-registry/interfaces/event.interface'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry/signing-keys-registry.service'; + import { BlockData } from 'guardian/interfaces'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { rangePromise } from 'utils'; + +const BATCH_SIZE = 10; @Injectable() export class KeysDuplicationCheckerService { constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, - private signingKeyEventsCacheService: SigningKeyEventsCacheService, + private signingKeysRegistryService: SigningKeysRegistryService, ) {} /** @@ -19,56 +26,40 @@ export class KeysDuplicationCheckerService { * 1. If there are duplicates within one operator, the key with the lowest index is considered the original, and the others are considered duplicates. * 2. If there are duplicates between different operators, check if a deposited key exists in the duplicates list; all others are considered duplicates. * 3. If there is no deposited key, check the SigningKeyAdded events for operators. - * 4. Sort events by block number and logIndex. The earliest event is considered the original, and the others are marked as duplicates. + * 4. Sort events by block number. The earliest event is considered the original, and the others are marked as duplicates. * * If there is no event for the key it will return list of unresolved keys. + * + * @param key public key + * @param blockData - collected data from the current block + * @returns An object containing two properties: + * - `duplicates`: An array of `RegistryKey` objects that are identified as duplicates. + * - `unresolved`: An array of `RegistryKey` objects for which no corresponding events were found. */ - async getDuplicatedKeys( + public async getDuplicatedKeys( keys: RegistryKey[], blockData: BlockData, ): Promise<{ duplicates: RegistryKey[]; unresolved: RegistryKey[] }> { - // List of all duplicates + if (keys.length === 0) { + return { duplicates: [], unresolved: [] }; + } // First element of sub-arrays is a key, second - all it's occurrences - const duplicatedKeys = this.findDuplicateKeys(keys); - - // async function that identify duplicates across list of duplicates - const getDuplicatedAndUnresolvedKeys = async ([key, occurrences]) => { - const operators = this.extractOperators(occurrences); - - // Function for identify duplicates across one operator - const duplicatesWithinOperator = () => - this.findDuplicatesWithinOperator(occurrences); - - // Function for identify duplicates if list contains deposited key - const duplicatesForDepositedKeys = () => - occurrences.filter((key) => !key.used); - - // Function for identify duplicates across multiple operators - const duplicatesAcrossOperators = async () => { - const { duplicateKeys, missingEvents } = - await this.getDuplicatesAcrossOperators( - key, - occurrences, - operators, - blockData, - ); - return { duplicates: duplicateKeys, unresolved: missingEvents }; - }; - - // if list contains only 1 operator - if (operators.size == 1) { - return { duplicates: duplicatesWithinOperator(), unresolved: [] }; - } else if (occurrences.some((key) => key.used)) { - // if list contains deposited key - return { duplicates: duplicatesForDepositedKeys(), unresolved: [] }; - } else { - // if list contain multiple operators and doesn't contain deposited keys - return await duplicatesAcrossOperators(); - } + const suspectedDuplicateKeyGroups = this.getDuplicateKeyGroups(keys); + + const processDuplicateGroup = async (index) => { + const [key, suspectedDuplicateKeys] = suspectedDuplicateKeyGroups[index]; + return await this.processDuplicateKeyGroup( + key, + suspectedDuplicateKeys, + blockData, + ); }; - const result = await Promise.all( - duplicatedKeys.map(getDuplicatedAndUnresolvedKeys), + const result = await rangePromise( + processDuplicateGroup, + 0, + suspectedDuplicateKeyGroups.length, + BATCH_SIZE, ); const duplicates = result.flatMap(({ duplicates }) => duplicates); @@ -77,35 +68,109 @@ export class KeysDuplicationCheckerService { return { duplicates, unresolved }; } - public findDuplicateKeys(keys: RegistryKey[]): [string, RegistryKey[]][] { - const keyOccurrencesMap = keys.reduce((acc, key) => { - const occurrences = acc.get(key.key) || []; - occurrences.push(key); - acc.set(key.key, occurrences); + /** + * Groups keys by their pubkey and returns a list of those with duplicates. + * + * This method iterates over the provided keys and groups them by their unique pubkey. + * It then filters out any groups that do not have duplicates, returning only the groups + * that contain more than one instance of the pubkey. + * + * @param keys - An array of `RegistryKey` objects to be checked for duplicates. + * @returns An array of tuples where each tuple contains a pubkey string and an array of + * `RegistryKey` objects that share that pubkey. Only keys with duplicates are included. + */ + public getDuplicateKeyGroups(keys: RegistryKey[]): [string, RegistryKey[]][] { + const keyMap = keys.reduce((acc, key) => { + const duplicateKeys = acc.get(key.key) || []; + duplicateKeys.push(key); + acc.set(key.key, duplicateKeys); return acc; }, new Map()); - return Array.from(keyOccurrencesMap.entries()).filter( - ([, occurrences]) => occurrences.length > 1, + return Array.from(keyMap.entries()).filter( + ([, duplicateKeys]) => duplicateKeys.length > 1, ); } - private extractOperators(occurrences: RegistryKey[]): Set { - return new Set( - occurrences.map((key) => `${key.moduleAddress}-${key.operatorIndex}`), + private async processDuplicateKeyGroup( + key: string, + suspectedDuplicateKeys: RegistryKey[], + blockData: BlockData, + ): Promise<{ duplicates: RegistryKey[]; unresolved: RegistryKey[] }> { + const uniqueOperatorIdentifiers = this.getUniqueIdentifiersForOperators( + suspectedDuplicateKeys, ); + + if (uniqueOperatorIdentifiers.length === 1) { + return this.handleSingleOperatorDuplicates(suspectedDuplicateKeys); + } + + if (this.hasDepositedKey(suspectedDuplicateKeys)) { + return this.handleDepositedKeyDuplicates(suspectedDuplicateKeys); + } + + return await this.handleMultiOperatorDuplicates( + key, + suspectedDuplicateKeys, + uniqueOperatorIdentifiers, + blockData, + ); + } + + private getUniqueIdentifiersForOperators(keys: RegistryKey[]): string[] { + return [...new Set(keys.map((key) => this.getKeyOperatorIdentifier(key)))]; + } + + private getKeyOperatorIdentifier(key: RegistryKey): string { + return `${key.moduleAddress}-${key.operatorIndex}`; + } + + private handleSingleOperatorDuplicates( + suspectedDuplicateKeys: RegistryKey[], + ): { + duplicates: RegistryKey[]; + unresolved: RegistryKey[]; + } { + const duplicates = this.findDuplicatesWithinOperator( + suspectedDuplicateKeys, + ); + return { duplicates, unresolved: [] }; + } + + private handleDepositedKeyDuplicates(suspectedDuplicateKeys: RegistryKey[]): { + duplicates: RegistryKey[]; + unresolved: RegistryKey[]; + } { + const duplicates = suspectedDuplicateKeys.filter((key) => !key.used); + return { duplicates, unresolved: [] }; + } + + private async handleMultiOperatorDuplicates( + key: string, + suspectedDuplicateKeys: RegistryKey[], + uniqueOperatorIdentifiers: string[], + blockData: BlockData, + ) { + const { duplicateKeys, unresolvedKeys } = + await this.getDuplicatesAcrossOperators( + key, + suspectedDuplicateKeys, + uniqueOperatorIdentifiers, + blockData, + ); + return { duplicates: duplicateKeys, unresolved: unresolvedKeys }; } private findDuplicatesWithinOperator( operatorKeys: RegistryKey[], ): RegistryKey[] { // Assuming keys belong to a single operator - const originalKey = this.findOriginalKeyWithinOperator(operatorKeys); - return operatorKeys.filter((key) => key.index !== originalKey.index); + const earliestKey = this.findEarliestKeyWithinOperator(operatorKeys); + return operatorKeys.filter((key) => key.index !== earliestKey.index); } - private findOriginalKeyWithinOperator( + private findEarliestKeyWithinOperator( operatorKeys: RegistryKey[], ): RegistryKey { return operatorKeys.reduce( @@ -114,90 +179,160 @@ export class KeysDuplicationCheckerService { ); } + private hasDepositedKey(keys: RegistryKey[]): boolean { + return keys.some((key) => key.used); + } + private async getDuplicatesAcrossOperators( key: string, - occurrences: RegistryKey[], - operators: Set, + suspectedDuplicateKeys: RegistryKey[], + uniqueOperatorIdentifiers: string[], blockData: BlockData, ) { - const events = await this.fetchSigningKeyEvents(key, blockData); + const { events } = await this.fetchSigningKeyEvents(key, blockData); - const missingOperators = this.findMissingOperators(operators, events); + const operatorsWithoutEvents = this.getOperatorsWithoutEvents( + uniqueOperatorIdentifiers, + events, + ); - if (missingOperators.length) { + if (operatorsWithoutEvents.length) { this.logger.error('Missing events for operators', { - missingOperators, + operatorsWithoutEvents, currentBlockNumber: blockData.blockNumber, currentBlockHash: blockData.blockHash, }); - // Return the entire occurrence set as unresolved - return { duplicateKeys: [], missingEvents: occurrences }; + // Return the entire list of duplicates as unresolved + return { duplicateKeys: [], unresolvedKeys: suspectedDuplicateKeys }; } - const originalEvent = this.findOriginalEvent(events); - const originalKey = this.findOriginalKey(occurrences, originalEvent); - - this.logger.log('Original key is', { - ...{ - originalKey, - createBlockNumber: originalEvent.blockNumber, - createBlockHash: originalEvent.blockHash, - createLogIndex: originalEvent.logIndex, - }, - currentBlockNumber: blockData.blockNumber, - currentBlockHash: blockData.blockHash, - }); - const duplicateKeys = occurrences.filter( - (k) => !this.isSameKey(k, originalKey), + return this.handleEventsForDuplicates( + events, + suspectedDuplicateKeys, + blockData, ); + } + + private handleEventsForDuplicates( + events: SigningKeyEvent[], + suspectedDuplicateKeys: RegistryKey[], + blockData: BlockData, + ) { + const earliestEvents = this.findEarliestEvents(events); + + // have only one event + if (earliestEvents.length === 1) { + const earliestEvent = earliestEvents[0]; + + const duplicateKeys = this.filterNonEarliestKeys( + earliestEvent, + suspectedDuplicateKeys, + blockData, + ); + + return { duplicateKeys, unresolvedKeys: [] }; + } - return { duplicateKeys, missingEvents: [] }; + // If there are few events at the same block + // There can be an attempt to front-run the key submission transaction, + // in this case, it's difficult to determine who was first, + // therefore it is proposed to unvet the entire set of duplicates. + // If trying to look at the log index, then a malicious actor can make a back-run + return { duplicateKeys: suspectedDuplicateKeys, unresolvedKeys: [] }; } private async fetchSigningKeyEvents( key: string, blockData: BlockData, - ): Promise { - const { events } = - await this.signingKeyEventsCacheService.getUpdatedSigningKeyEvents( + ): Promise { + const eventsGroup = + await this.signingKeysRegistryService.getUpdatedSigningKeyEvents( key, blockData.blockNumber, blockData.blockHash, ); - return events; + + return eventsGroup; } - private findOriginalEvent(events: SigningKeyEvent[]): SigningKeyEvent { - return events.reduce( - (prev, curr) => - prev.blockNumber < curr.blockNumber || - (prev.blockNumber === curr.blockNumber && prev.logIndex < curr.logIndex) - ? prev - : curr, - events[0], + private getOperatorsWithoutEvents( + uniqueOperatorIdentifiers: string[], + events: SigningKeyEvent[], + ): string[] { + const eventOperators = new Set( + events.map((event) => `${event.moduleAddress}-${event.operatorIndex}`), + ); + return uniqueOperatorIdentifiers.filter( + (operatorIdentifier) => !eventOperators.has(operatorIdentifier), ); } - private findOriginalKey( - occurrences: RegistryKey[], - originalEvent: SigningKeyEvent, - ): RegistryKey { - const keyOwnerKeys = occurrences.filter( - (key) => - key.moduleAddress === originalEvent.moduleAddress && - key.operatorIndex === originalEvent.operatorIndex, + private filterNonEarliestKeys( + earliestEvent: SigningKeyEvent, + suspectedDuplicateKeys: RegistryKey[], + blockData: BlockData, + ) { + const operatorKeys = this.findOperatorKeys( + suspectedDuplicateKeys, + earliestEvent.moduleAddress, + earliestEvent.operatorIndex, + ); + + const earliestKey = this.findEarliestKeyWithinOperator(operatorKeys); + + this.logger.log('Earliest key is', { + earliestKey, + createBlockNumber: earliestEvent.blockNumber, + createBlockHash: earliestEvent.blockHash, + currentBlockNumber: blockData.blockNumber, + currentBlockHash: blockData.blockHash, + }); + return suspectedDuplicateKeys.filter( + (key) => !this.isSameKey(key, earliestKey), ); - return this.findOriginalKeyWithinOperator(keyOwnerKeys); } - private findMissingOperators( - operators: Set, - events: SigningKeyEvent[], - ): string[] { - const eventOperators = new Set( - events.map((event) => `${event.moduleAddress}-${event.operatorIndex}`), + private findEarliestEvents(events: SigningKeyEvent[]): SigningKeyEvent[] { + if (events.length <= 1) return events; + + const { blockEvents } = events.reduce( + ({ earliestBlockNumber, blockEvents }, currEvent) => { + if (earliestBlockNumber === currEvent.blockNumber) { + blockEvents.push(currEvent); + return { + earliestBlockNumber, + blockEvents, + }; + } + + if (earliestBlockNumber > currEvent.blockNumber) { + return { + earliestBlockNumber: currEvent.blockNumber, + blockEvents: [currEvent], + }; + } + + return { earliestBlockNumber, blockEvents }; + }, + { + earliestBlockNumber: events[0].blockNumber, + blockEvents: [], + } as { earliestBlockNumber: number; blockEvents: SigningKeyEvent[] }, + ); + + return blockEvents; + } + + private findOperatorKeys( + keys: RegistryKey[], + moduleAddress: string, + operatorIndex: number, + ): RegistryKey[] { + return keys.filter( + (key) => + key.moduleAddress === moduleAddress && + key.operatorIndex === operatorIndex, ); - return [...operators].filter((op) => !eventOperators.has(op)); } private isSameKey(key1: RegistryKey, key2: RegistryKey): boolean { diff --git a/src/guardian/duplicates/keys.fixtures.ts b/src/guardian/duplicates/keys.fixtures.ts index d23109ab..af6683a7 100644 --- a/src/guardian/duplicates/keys.fixtures.ts +++ b/src/guardian/duplicates/keys.fixtures.ts @@ -1,7 +1,7 @@ -import { SigningKeyEvent } from 'contracts/signing-key-events-cache/interfaces/event.interface'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { SigningKeyEvent } from 'contracts/signing-keys-registry/interfaces/event.interface'; -export const keyMock1 = { +export const keyMock1: RegistryKey = { key: '0xb3c90525010a5710d43acbea46047fc37ed55306d032527fa15dd7e8cd8a9a5fa490347cc5fce59936fb8300683cd9f3', depositSignature: '0x8a77d9411781360cc107344a99f6660b206d2c708ae7fa35565b76ec661a0b86b6c78f5b5691d2cf469c27d0655dfc6311451a9e0501f3c19c6f7e35a770d1a908bfec7cba2e07339dc633b8b6626216ce76ec0fa48ee56aaaf2f9dc7ccb2fe2', @@ -9,19 +9,10 @@ export const keyMock1 = { used: false, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', index: 52, + vetted: true, }; -export const keyMock1Duplicate = { - key: '0xb3c90525010a5710d43acbea46047fc37ed55306d032527fa15dd7e8cd8a9a5fa490347cc5fce59936fb8300683cd9f3', - depositSignature: - '0x8a77d9411781360cc107344a99f6660b206d2c708ae7fa35565b76ec661a0b86b6c78f5b5691d2cf469c27d0655dfc6311451a9e0501f3c19c6f7e35a770d1a908bfec7cba2e07339dc633b8b6626216ce76ec0fa48ee56aaaf2f9dc7ccb2fe2', - operatorIndex: 1, - used: false, - moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', - index: 53, -}; - -export const keyMock2 = { +export const keyMock2: RegistryKey = { key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', depositSignature: '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', @@ -29,14 +20,22 @@ export const keyMock2 = { used: false, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', index: 51, + vetted: true, }; -export const keysMock: RegistryKey[] = [keyMock1, keyMock1Duplicate, keyMock2]; +export const eventMock1: SigningKeyEvent = { + operatorIndex: keyMock1.operatorIndex, + key: keyMock1.key, + moduleAddress: keyMock1.moduleAddress, + logIndex: 1, + blockNumber: 1, + blockHash: '0x', +}; -export const eventMock: SigningKeyEvent = { - operatorIndex: keyMock1Duplicate.operatorIndex, - key: keyMock1Duplicate.key, - moduleAddress: keyMock1Duplicate.moduleAddress, +export const eventMock2: SigningKeyEvent = { + operatorIndex: keyMock2.operatorIndex, + key: keyMock2.key, + moduleAddress: keyMock2.moduleAddress, logIndex: 1, blockNumber: 1, blockHash: '0x', diff --git a/src/guardian/guardian-metrics/guardian-metrics.service.ts b/src/guardian/guardian-metrics/guardian-metrics.service.ts index 78685bf4..1d0506ec 100644 --- a/src/guardian/guardian-metrics/guardian-metrics.service.ts +++ b/src/guardian/guardian-metrics/guardian-metrics.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { VerifiedDepositEvent } from 'contracts/deposit'; +import { VerifiedDepositEvent } from 'contracts/deposits-registry'; import { BlockData, StakingModuleData } from '../interfaces'; import { InjectMetric } from '@willsoto/nestjs-prometheus'; import { diff --git a/src/guardian/guardian.constants.ts b/src/guardian/guardian.constants.ts index 50cf550e..459da204 100644 --- a/src/guardian/guardian.constants.ts +++ b/src/guardian/guardian.constants.ts @@ -3,4 +3,4 @@ import { CronExpression } from '@nestjs/schedule'; export const GUARDIAN_DEPOSIT_RESIGNING_BLOCKS = 10; export const GUARDIAN_DEPOSIT_JOB_NAME = 'guardian-deposit-job'; export const GUARDIAN_DEPOSIT_JOB_DURATION = CronExpression.EVERY_5_SECONDS; -export const MIN_KAPI_VERSION = '1.5.0'; +export const MIN_KAPI_VERSION = '2.1.0'; diff --git a/src/guardian/guardian.module.ts b/src/guardian/guardian.module.ts index 68cd36d2..509b4b66 100644 --- a/src/guardian/guardian.module.ts +++ b/src/guardian/guardian.module.ts @@ -1,34 +1,34 @@ import { Module } from '@nestjs/common'; -import { DepositModule } from 'contracts/deposit'; +import { DepositsRegistryModule } from 'contracts/deposits-registry'; import { SecurityModule } from 'contracts/security'; -import { LidoModule } from 'contracts/lido'; import { MessagesModule } from 'messages'; import { GuardianService } from './guardian.service'; -import { StakingRouterModule } from 'staking-router'; import { ScheduleModule } from 'common/schedule'; -import { BlockGuardModule } from './block-guard/block-guard.module'; +import { BlockDataCollectorModule } from './block-data-collector/block-data-collector.module'; import { StakingModuleGuardModule } from './staking-module-guard'; import { GuardianMessageModule } from './guardian-message'; import { GuardianMetricsModule } from './guardian-metrics'; import { KeysApiModule } from 'keys-api/keys-api.module'; -import { SigningKeyEventsCacheModule } from 'contracts/signing-key-events-cache'; +import { SigningKeysRegistryModule } from 'contracts/signing-keys-registry'; import { UnvettingModule } from './unvetting/unvetting.module'; +import { StakingModuleDataCollectorModule } from 'staking-module-data-collector'; +import { StakingRouterModule } from 'contracts/staking-router'; @Module({ imports: [ - DepositModule, + DepositsRegistryModule.register(), SecurityModule, - LidoModule, MessagesModule, - StakingRouterModule, + StakingModuleDataCollectorModule, ScheduleModule, - BlockGuardModule, + BlockDataCollectorModule, StakingModuleGuardModule, UnvettingModule, GuardianMessageModule, GuardianMetricsModule, KeysApiModule, - SigningKeyEventsCacheModule, + SigningKeysRegistryModule.register(), + StakingRouterModule, ], providers: [GuardianService], exports: [GuardianService], diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index d314cb55..46d52c82 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -7,16 +7,15 @@ import { LoggerService } from '@nestjs/common'; import { ConfigModule } from 'common/config'; import { PrometheusModule } from 'common/prometheus'; import { GuardianModule } from 'guardian'; -import { DepositModule } from 'contracts/deposit'; +import { DepositsRegistryModule } from 'contracts/deposits-registry'; import { SecurityModule } from 'contracts/security'; import { RepositoryModule, RepositoryService } from 'contracts/repository'; -import { LidoModule } from 'contracts/lido'; import { MessagesModule } from 'messages'; -import { StakingRouterModule } from 'staking-router'; +import { StakingModuleDataCollectorModule } from 'staking-module-data-collector'; import { GuardianMetricsModule } from './guardian-metrics'; import { GuardianMessageModule } from './guardian-message'; import { StakingModuleGuardModule } from './staking-module-guard'; -import { BlockGuardModule, BlockGuardService } from './block-guard'; +import { BlockDataCollectorModule } from './block-data-collector'; import { ScheduleModule } from 'common/schedule'; import { LocatorService } from 'contracts/repository/locator/locator.service'; import { mockLocator } from 'contracts/repository/locator/locator.mock'; @@ -28,8 +27,6 @@ jest.mock('../transport/stomp/stomp.client'); describe('GuardianService', () => { let keysApiService: KeysApiService; - let blockGuardService: BlockGuardService; - let guardianService: GuardianService; let loggerService: LoggerService; @@ -45,13 +42,12 @@ describe('GuardianService', () => { PrometheusModule, GuardianModule, RepositoryModule, - DepositModule, + DepositsRegistryModule.register('latest'), SecurityModule, - LidoModule, MessagesModule, - StakingRouterModule, + StakingModuleDataCollectorModule, ScheduleModule, - BlockGuardModule, + BlockDataCollectorModule, StakingModuleGuardModule, GuardianMessageModule, GuardianMetricsModule, @@ -60,7 +56,6 @@ describe('GuardianService', () => { }).compile(); keysApiService = moduleRef.get(KeysApiService); - blockGuardService = moduleRef.get(BlockGuardService); repositoryService = moduleRef.get(RepositoryService); locatorService = moduleRef.get(LocatorService); @@ -80,16 +75,14 @@ describe('GuardianService', () => { it('should exit if the previous call is not completed', async () => { // OneAtTime test const getOperatorsAndModulesMock = jest - .spyOn(keysApiService, 'getOperatorListWithModule') + .spyOn(keysApiService, 'getModules') .mockImplementation(async () => ({ data: [], - meta: { - elBlockSnapshot: { - blockNumber: 0, - blockHash: 'string', - timestamp: 0, - lastChangedBlockHash: '', - }, + elBlockSnapshot: { + blockNumber: 0, + blockHash: 'string', + timestamp: 0, + lastChangedBlockHash: '', }, })); @@ -105,8 +98,8 @@ describe('GuardianService', () => { }, })); - const getBlockGuardServiceMock = jest - .spyOn(blockGuardService, 'isNeedToProcessNewState') + const isNeedToProcessNewStatMock = jest + .spyOn(guardianService, 'isNeedToProcessNewState') .mockImplementation(() => false); // run concurrently and check that second attempt @@ -115,7 +108,7 @@ describe('GuardianService', () => { guardianService.handleNewBlock(), ]); - expect(getBlockGuardServiceMock).toBeCalledTimes(1); + expect(isNeedToProcessNewStatMock).toBeCalledTimes(1); expect(getOperatorsAndModulesMock).toBeCalledTimes(1); }); }); diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 6ee7c4bb..987a8b91 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -8,7 +8,7 @@ import { compare } from 'compare-versions'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { SchedulerRegistry } from '@nestjs/schedule'; import { CronJob } from 'cron'; -import { DepositService } from 'contracts/deposit'; +import { DepositRegistryService } from 'contracts/deposits-registry'; import { SecurityService } from 'contracts/security'; import { RepositoryService } from 'contracts/repository'; import { @@ -16,9 +16,10 @@ import { GUARDIAN_DEPOSIT_JOB_NAME, } from './guardian.constants'; import { OneAtTime } from 'common/decorators'; -import { StakingRouterService } from 'staking-router'; +import { StakingModuleDataCollectorService } from 'staking-module-data-collector'; + +import { BlockDataCollectorService } from './block-data-collector'; -import { BlockGuardService } from './block-guard'; import { StakingModuleGuardService } from './staking-module-guard'; import { GuardianMessageService } from './guardian-message'; import { GuardianMetricsService } from './guardian-metrics'; @@ -26,11 +27,12 @@ import { BlockData, StakingModuleData } from './interfaces'; import { ProviderService } from 'provider'; import { KeysApiService } from 'keys-api/keys-api.service'; import { MIN_KAPI_VERSION } from './guardian.constants'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; import { UnvettingService } from './unvetting/unvetting.service'; -import { Meta } from 'keys-api/interfaces/Meta'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -import { SROperatorListWithModule } from 'keys-api/interfaces/SROperatorListWithModule'; +import { StakingRouterService } from 'contracts/staking-router'; +import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; +import { SRModule } from 'keys-api/interfaces'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; @Injectable() export class GuardianService implements OnModuleInit { @@ -43,20 +45,22 @@ export class GuardianService implements OnModuleInit { private schedulerRegistry: SchedulerRegistry, - private depositService: DepositService, + private depositService: DepositRegistryService, private securityService: SecurityService, - private stakingRouterService: StakingRouterService, + private stakingModuleDataCollectorService: StakingModuleDataCollectorService, - private blockGuardService: BlockGuardService, + private blockDataCollectorService: BlockDataCollectorService, private stakingModuleGuardService: StakingModuleGuardService, private guardianMessageService: GuardianMessageService, private guardianMetricsService: GuardianMetricsService, private providerService: ProviderService, private keysApiService: KeysApiService, - private signingKeyEventsCacheService: SigningKeyEventsCacheService, + private signingKeysRegistryService: SigningKeysRegistryService, private unvettingService: UnvettingService, + + private stakingRouterService: StakingRouterService, ) {} public async onModuleInit(): Promise { @@ -67,10 +71,15 @@ export class GuardianService implements OnModuleInit { const block = await this.repositoryService.initOrWaitCachedContracts(); const blockHash = block.hash; + const stakingRouterModuleAddresses = + await this.stakingRouterService.getStakingModulesAddresses(blockHash); + await Promise.all([ - this.depositService.initialize(block.number), + this.depositService.initialize(), this.securityService.initialize({ blockHash }), - this.signingKeyEventsCacheService.initialize(block.number), + this.signingKeysRegistryService.initialize( + stakingRouterModuleAddresses, + ), ]); const chainId = await this.providerService.getChainId(); @@ -96,11 +105,6 @@ export class GuardianService implements OnModuleInit { ); } - // The event cache is stored with an N block lag to avoid caching data from uncle blocks - // so we don't worry about blockHash here - await this.depositService.updateEventsCache(); - await this.signingKeyEventsCacheService.updateEventsCache(); - this.subscribeToModulesUpdates(); } catch (error) { this.logger.error(error); @@ -134,46 +138,44 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - // Fetch the minimum required data to make an early exit - // fetch data from Keys api - const { data: operatorsByModules, meta } = - await this.keysApiService.getOperatorListWithModule(); + // Fetch the minimum required data fro Keys Api to make an early exit + const { data: stakingModules, elBlockSnapshot: firstRequestMeta } = + await this.keysApiService.getModules(); - const { - elBlockSnapshot: { blockHash, blockNumber }, - } = meta; + const { blockHash, blockNumber } = firstRequestMeta; - // contracts init - await this.repositoryService.initCachedContracts({ blockHash }); - - const isNewBlock = this.blockGuardService.isNeedToProcessNewState({ + // Compare the block stored in memory from the previous iteration with the current block from the Keys API. + const isNewBlock = this.isNeedToProcessNewState({ blockHash, blockNumber, }); if (!isNewBlock) return; - const stakingModulesCount = operatorsByModules.length; + const stakingModulesCount = stakingModules.length; this.logger.log('Staking modules loaded', { modulesCount: stakingModulesCount, }); // fetch all lido keys - const { data: lidoKeys, meta: currMeta } = + const { data: lidoKeys, meta: secondRequestMeta } = await this.keysApiService.getKeys(); // check that there were no updates in Keys Api between two requests this.keysApiService.verifyMetaDataConsistency( - meta.elBlockSnapshot.lastChangedBlockHash, - currMeta.elBlockSnapshot.lastChangedBlockHash, + firstRequestMeta.lastChangedBlockHash, + secondRequestMeta.elBlockSnapshot.lastChangedBlockHash, ); - await this.depositService.handleNewBlock(blockNumber); + // contracts initialization + await this.repositoryService.initCachedContracts({ blockHash }); + + await this.depositService.handleNewBlock(); const { stakingModulesData, blockData } = await this.collectData( - operatorsByModules, - meta, + stakingModules, + firstRequestMeta, lidoKeys, ); @@ -194,6 +196,9 @@ export class GuardianService implements OnModuleInit { return; } + // To avoid blocking the pause, run the following tasks asynchronously: + // updating the SigningKeyAdded events cache, checking keys, handling the unvetting of keys, + // and sending deposit messages to the queue. this.handleKeys(stakingModulesData, blockData, lidoKeys).catch( this.logger.error, ); @@ -208,18 +213,26 @@ export class GuardianService implements OnModuleInit { } } - async collectData( - operatorsByModules: SROperatorListWithModule[], - meta: Meta, + private async collectData( + stakingModules: SRModule[], + meta: ELBlockSnapshot, lidoKeys: RegistryKey[], ) { - const { - elBlockSnapshot: { blockHash, blockNumber }, - } = meta; - const blockData = await this.blockGuardService.getCurrentBlockData({ - blockHash, - blockNumber, - }); + const { blockHash, blockNumber } = meta; + + const [blockData, stakingModulesData] = await Promise.all([ + this.blockDataCollectorService.getCurrentBlockData({ + blockHash, + blockNumber, + }), + // Construct the Staking Module data array using information fetched from the Keys API, + // identifying vetted unused keys and checking the module pause status + this.stakingModuleDataCollectorService.collectStakingModuleData({ + stakingModules, + meta, + lidoKeys, + }), + ]); this.logger.debug?.('Current block data loaded', { guardianIndex: blockData.guardianIndex, @@ -228,23 +241,14 @@ export class GuardianService implements OnModuleInit { securityVersion: blockData.securityVersion, }); - // collect some data and check keys - const stakingModulesData: StakingModuleData[] = - await this.stakingRouterService.collectStakingModuleData({ - operatorsByModules, - meta, - lidoKeys, - blockData, - }); - return { blockData, stakingModulesData }; } /** - * This method check keys and if they are correct send deposit message in queue, another way send unvet transation + * This method check keys and if they are correct send deposit message in queue, another way send unvet transaction */ @OneAtTime() - async handleKeys( + private async handleKeys( stakingModulesData: StakingModuleData[], blockData: BlockData, lidoKeys: RegistryKey[], @@ -256,7 +260,7 @@ export class GuardianService implements OnModuleInit { await this.handleDeposit(stakingModulesData, blockData); const { blockHash, blockNumber } = blockData; - this.blockGuardService.setLastProcessedStateMeta({ + this.setLastProcessedStateMeta({ blockHash, blockNumber, }); @@ -264,25 +268,28 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle end'); } - async checkKeys( + private async checkKeys( stakingModulesData: StakingModuleData[], blockData: BlockData, lidoKeys: RegistryKey[], ) { + const stakingRouterModuleAddresses = stakingModulesData.map( + (stakingModule) => stakingModule.stakingModuleAddress, + ); // update cache if needs - await this.signingKeyEventsCacheService.handleNewBlock( - blockData.blockNumber, + await this.signingKeysRegistryService.handleNewBlock( + stakingRouterModuleAddresses, ); // check keys on duplicates, attempts of front-run and check signatures - await this.stakingRouterService.checkKeys( + await this.stakingModuleDataCollectorService.checkKeys( stakingModulesData, lidoKeys, blockData, ); } - async handleUnvetting( + private async handleUnvetting( stakingModulesData: StakingModuleData[], blockData: BlockData, ) { @@ -314,15 +321,14 @@ export class GuardianService implements OnModuleInit { } private hasInvalidKeys(moduleData: StakingModuleData): boolean { - const keys = [ - ...moduleData.invalidKeys, - ...moduleData.duplicatedKeys, - ...moduleData.frontRunKeys, - ]; + const keys = moduleData.invalidKeys.concat( + moduleData.duplicatedKeys, + moduleData.frontRunKeys, + ); return keys.length > 0; } - async handleDeposit( + private async handleDeposit( stakingModulesData: StakingModuleData[], blockData: BlockData, ) { @@ -333,21 +339,14 @@ export class GuardianService implements OnModuleInit { blockData, ); - // Check the integrity of the cache, we can only make a deposit - // if the integrity of the deposit event data is intact - await blockData.depositedEvents.checkRoot(); - if ( - this.cannotDeposit( + this.ignoreDeposits( stakingModuleData, blockData.theftHappened, blockData.alreadyPausedDeposits, + stakingModuleData.stakingModuleId, ) ) { - this.logger.warn('Deposits are not available', { - stakingModuleId: stakingModuleData.stakingModuleId, - blockHash: blockData.blockHash, - }); return; } @@ -359,24 +358,64 @@ export class GuardianService implements OnModuleInit { ); } - cannotDeposit( + private ignoreDeposits( stakingModuleData: StakingModuleData, theftHappened: boolean, alreadyPausedDeposits: boolean, + stakingModuleId: number, ): boolean { - const keysForUnvetting = [ - ...stakingModuleData.invalidKeys, - ...stakingModuleData.frontRunKeys, - ...stakingModuleData.duplicatedKeys, - ]; + const keysForUnvetting = stakingModuleData.invalidKeys.concat( + stakingModuleData.frontRunKeys, + stakingModuleData.duplicatedKeys, + ); // if neither of this conditions is true, deposits are allowed for module - return ( + const ignoreDeposits = keysForUnvetting.length > 0 || stakingModuleData.unresolvedDuplicatedKeys.length > 0 || alreadyPausedDeposits || theftHappened || - stakingModuleData.isModuleDepositsPaused - ); + stakingModuleData.isModuleDepositsPaused; + + if (ignoreDeposits) { + this.logger.warn('Deposits are not available', { + keysForUnvetting: keysForUnvetting.length, + duplicates: stakingModuleData.unresolvedDuplicatedKeys.length, + alreadyPausedDeposits, + theftHappened, + isModuleDepositsPaused: stakingModuleData.isModuleDepositsPaused, + stakingModuleId, + }); + } + + return ignoreDeposits; + } + + public isNeedToProcessNewState(newMeta: { + blockHash: string; + blockNumber: number; + }) { + const lastMeta = this.lastProcessedStateMeta; + if (!lastMeta) return true; + if (lastMeta.blockNumber > newMeta.blockNumber) { + this.logger.error('Keys API returns old state', { newMeta, lastMeta }); + return false; + } + const isSameBlock = lastMeta.blockHash === newMeta.blockHash; + + if (isSameBlock) { + this.logger.log(`The block has not changed since the last cycle. Exit`, { + newMeta, + }); + } + + return !isSameBlock; + } + + private setLastProcessedStateMeta(newMeta: { + blockHash: string; + blockNumber: number; + }) { + this.lastProcessedStateMeta = newMeta; } } diff --git a/src/guardian/interfaces/block.interface.ts b/src/guardian/interfaces/block.interface.ts index 47b6ed53..9580e095 100644 --- a/src/guardian/interfaces/block.interface.ts +++ b/src/guardian/interfaces/block.interface.ts @@ -1,4 +1,4 @@ -import { VerifiedDepositedEventGroup } from 'contracts/deposit'; +import { VerifiedDepositedEventGroup } from 'contracts/deposits-registry'; export interface BlockData { blockNumber: number; diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index feafc61a..e8861f6c 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -1,8 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { KeyValidatorInterface, bufferFromHexString, - Pubkey, WithdrawalCredentialsBuffer, Key, } from '@lido-nestjs/key-validation'; @@ -11,10 +10,9 @@ import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; import { LRUCache } from 'lru-cache'; import { DEPOSIT_DATA_LRU_CACHE_SIZE } from './constants'; import { ProviderService } from 'provider'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -type DepositData = { - key: Pubkey; - depositSignature: string; +type DepositKey = RegistryKey & { withdrawalCredentials: WithdrawalCredentialsBuffer; genesisForkVersion: Buffer; }; @@ -26,6 +24,7 @@ export class KeysValidationService { constructor( private readonly keyValidator: KeyValidatorInterface, private readonly provider: ProviderService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, ) { this.depositDataCache = new LRUCache({ max: DEPOSIT_DATA_LRU_CACHE_SIZE }); } @@ -44,29 +43,45 @@ export class KeysValidationService { ); const genesisForkVersion: Uint8Array = await this.forkVersion(); const genesisForkVersionBuffer = Buffer.from(genesisForkVersion.buffer); - const depositDataList = this.createDepositDataList( - keys, - withdrawalCredentialsBuffer, - genesisForkVersionBuffer, - ); - return await this.findInvalidKeys(keys, depositDataList); - } - async findInvalidKeys( - keys: RegistryKey[], - depositDataList: DepositData[], - ): Promise { - const validatedKeys = await this.validateKeys(depositDataList); + const { cachedInvalidKeyList, uncachedDepositKeyList } = + this.partitionCachedData( + keys, + withdrawalCredentialsBuffer, + genesisForkVersionBuffer, + ); + + this.logger.log('Validation status of deposit keys:', { + cachedInvalidKeyCount: cachedInvalidKeyList.length, + keysNeedingValidationCount: uncachedDepositKeyList.length, + totalKeysCount: keys.length, + }); + + const validatedDepositKeyList: [DepositKey & Key, boolean][] = + await this.keyValidator.validateKeys(uncachedDepositKeyList); + + this.updateCache(validatedDepositKeyList); + const invalidKeys = this.filterInvalidKeys(validatedDepositKeyList); + + return cachedInvalidKeyList.concat(invalidKeys); + } + + private filterInvalidKeys( + validatedKeys: [DepositKey & Key, boolean][], + ): RegistryKey[] { return validatedKeys.reduce( (invalidKeys, [data, isValid]) => { if (!isValid) { - const matchingInvalidKeys = keys.filter( - (key) => - key.key === data.key && - key.depositSignature === data.depositSignature, - ); - invalidKeys.push(...matchingInvalidKeys); + invalidKeys.push({ + key: data.key, + depositSignature: data.depositSignature, + operatorIndex: data.operatorIndex, + used: data.used, + index: data.index, + moduleAddress: data.moduleAddress, + vetted: data.vetted, + }); } return invalidKeys; }, @@ -74,55 +89,47 @@ export class KeysValidationService { ); } - /* - * Validate data with use of cache - */ - public async validateKeys( - depositDataList: DepositData[], - ): Promise<[Key & DepositData, boolean][]> { - const { cachedDepositData, uncachedDepositData } = - this.partitionCachedData(depositDataList); - - const validatedDepositData: [Key & DepositData, boolean][] = - await this.keyValidator.validateKeys(uncachedDepositData); - - this.updateCache(validatedDepositData); - - return [...cachedDepositData, ...validatedDepositData]; - } - /** * Partition the deposit data into cached invalid data and uncached data. * @param depositDataList List of deposit data to check against the cache * @returns An object containing cached invalid data and uncached data */ - private partitionCachedData(depositDataList: DepositData[]): { - cachedDepositData: [DepositData, boolean][]; - uncachedDepositData: DepositData[]; + private partitionCachedData( + keys: RegistryKey[], + withdrawalCredentialsBuffer: WithdrawalCredentialsBuffer, + genesisForkVersionBuffer: Buffer, + ): { + cachedInvalidKeyList: RegistryKey[]; + uncachedDepositKeyList: DepositKey[]; } { - return depositDataList.reduce<{ - cachedDepositData: [DepositData, boolean][]; - uncachedDepositData: DepositData[]; + return keys.reduce<{ + cachedInvalidKeyList: RegistryKey[]; + uncachedDepositKeyList: DepositKey[]; }>( - (acc, depositData) => { - const cacheResult = this.getCachedDepositData(depositData); - - if (cacheResult === false || cacheResult === true) { - acc.cachedDepositData.push([depositData, cacheResult]); + (acc, key) => { + const depositKey = { + ...key, + withdrawalCredentials: withdrawalCredentialsBuffer, + genesisForkVersion: genesisForkVersionBuffer, + }; + const cacheResult = this.getCachedDepositData(depositKey); + + if (cacheResult === false) { + acc.cachedInvalidKeyList.push(key); } if (cacheResult === undefined) { - acc.uncachedDepositData.push(depositData); + acc.uncachedDepositKeyList.push(depositKey); } return acc; }, - { cachedDepositData: [], uncachedDepositData: [] }, + { cachedInvalidKeyList: [], uncachedDepositKeyList: [] }, ); } - private getCachedDepositData(depositData: DepositData): boolean | undefined { - return this.depositDataCache.get(this.serializeDepositData(depositData)); + private getCachedDepositData(depositKey: DepositKey): boolean | undefined { + return this.depositDataCache.get(this.serializeDepositData(depositKey)); } private async forkVersion(): Promise { @@ -136,7 +143,7 @@ export class KeysValidationService { return forkVersion; } - private async updateCache(validatedKeys: [Key & DepositData, boolean][]) { + private updateCache(validatedKeys: [Key & DepositKey, boolean][]) { validatedKeys.forEach(([depositData, isValid]) => this.depositDataCache.set( this.serializeDepositData(depositData), @@ -145,24 +152,12 @@ export class KeysValidationService { ); } - private serializeDepositData(depositData: DepositData): string { + private serializeDepositData(depositKey: DepositKey): string { return JSON.stringify({ - ...depositData, - withdrawalCredentials: depositData.withdrawalCredentials.toString('hex'), - genesisForkVersion: depositData.genesisForkVersion.toString('hex'), + key: depositKey.key, + depositSignature: depositKey.depositSignature, + withdrawalCredentials: depositKey.withdrawalCredentials.toString('hex'), + genesisForkVersion: depositKey.genesisForkVersion.toString('hex'), }); } - - private createDepositDataList( - keys: RegistryKey[], - withdrawalCredentialsBuffer: WithdrawalCredentialsBuffer, - genesisForkVersionBuffer: Buffer, - ): DepositData[] { - return keys.map((key) => ({ - key: key.key, - depositSignature: key.depositSignature, - withdrawalCredentials: withdrawalCredentialsBuffer, - genesisForkVersion: genesisForkVersionBuffer, - })); - } } diff --git a/src/guardian/keys-validation/keys-validation.spec.ts b/src/guardian/keys-validation/keys-validation.spec.ts index 1192d2cd..eebc91ad 100644 --- a/src/guardian/keys-validation/keys-validation.spec.ts +++ b/src/guardian/keys-validation/keys-validation.spec.ts @@ -16,6 +16,7 @@ import { validKeys, } from './keys.fixtures'; import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; describe('KeysValidationService', () => { let keysValidationService: KeysValidationService; @@ -25,7 +26,9 @@ describe('KeysValidationService', () => { const wc = '0x010000000000000000000000dc62f9e8c34be08501cdef4ebde0a280f576d762'; - beforeEach(async () => { + const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; + + beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [ ConfigModule.forRoot(), @@ -40,97 +43,88 @@ describe('KeysValidationService', () => { keysValidator = moduleRef.get(KeyValidatorInterface); validateKeysFun = jest.spyOn(keysValidator, 'validateKeys'); + + const loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); + jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); }); - it('should find and return invalid keys from the provided list', async () => { - // Test scenario where new invalid keys are added to the list - const result = await keysValidationService.getInvalidKeys( - [...validKeys, invalidKey1, invalidKey2], - wc, - ); - - const expected = [invalidKey1, invalidKey2]; - - const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; - - const depositData = [...validKeys, invalidKey1, invalidKey2].map((key) => ({ - key: key.key, - depositSignature: key.depositSignature, - withdrawalCredentials: bufferFromHexString(wc), - genesisForkVersion: Buffer.from(fork.buffer), - })); - - expect(validateKeysFun).toBeCalledTimes(1); - expect(validateKeysFun).toBeCalledWith(depositData); - expect(result).toEqual(expect.arrayContaining(expected)); - expect(result.length).toEqual(expected.length); - - validateKeysFun.mockClear(); - // Test scenario where one invalid key was removed from request's list - const newResult = await keysValidationService.getInvalidKeys( - [...validKeys, invalidKey1], - wc, - ); - - const newExpected = [invalidKey1]; - const invalidKey2DepositData = JSON.stringify({ - key: invalidKey2.key, - depositSignature: invalidKey2.depositSignature, - withdrawalCredentials: wc.replace(/^0x/, ''), - genesisForkVersion: Buffer.from(fork.buffer).toString('hex'), + describe('Validate again if signature was changed', () => { + beforeEach(() => { + validateKeysFun.mockClear(); }); - expect( - keysValidationService['depositDataCache'].get(invalidKey2DepositData), - ).toEqual(false); - - expect(validateKeysFun).toBeCalledTimes(1); - expect(validateKeysFun).toBeCalledWith([]); - expect(newResult).toEqual(expect.arrayContaining(newExpected)); - expect(newResult.length).toEqual(newExpected.length); - }); - it('should validate key again if signature was changed', async () => { - // if signature was changed we need to repeat validation - // invalid key could become valid and visa versa - // Test scenario where new invalid keys are added to the list - const result = await keysValidationService.getInvalidKeys( - [...validKeys, invalidKey1, invalidKey2], - wc, - ); - const expected = [invalidKey1, invalidKey2]; - const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; - const depositData = [...validKeys, invalidKey1, invalidKey2].map((key) => ({ - key: key.key, - depositSignature: key.depositSignature, - withdrawalCredentials: bufferFromHexString(wc), - genesisForkVersion: Buffer.from(fork.buffer), - })); - expect(validateKeysFun).toBeCalledTimes(1); - expect(validateKeysFun).toBeCalledWith(depositData); - expect(result).toEqual(expect.arrayContaining(expected)); - expect(result.length).toEqual(expected.length); - validateKeysFun.mockClear(); - // Test scenario where one invalid key was changed - const newResult = await keysValidationService.getInvalidKeys( - [ + it('validate without use of cache', async () => { + const duplicate = { ...invalidKey1, index: 102 }; + const keysForValidation = [ ...validKeys, invalidKey1, - { ...invalidKey2, depositSignature: invalidKey2GoodSign }, - ], - wc, - ); - const newDepositData = [ - { ...invalidKey2, depositSignature: invalidKey2GoodSign }, - ].map((key) => ({ - key: key.key, - depositSignature: key.depositSignature, - withdrawalCredentials: bufferFromHexString(wc), - genesisForkVersion: Buffer.from(fork.buffer), - })); - const newExpected = [invalidKey1]; - expect(validateKeysFun).toBeCalledTimes(1); - expect(validateKeysFun).toBeCalledWith(newDepositData); - expect(newResult).toEqual(expect.arrayContaining(newExpected)); - expect(newResult.length).toEqual(newExpected.length); + // getInvalidKeys should return all invalid duplicates + duplicate, + invalidKey2, + ]; + const result = await keysValidationService.getInvalidKeys( + keysForValidation, + wc, + ); + + // we extended RegistryKey to satisfy DepositData type + const depositKeyList = keysForValidation.map((key) => ({ + ...key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(depositKeyList); + expect(result).toEqual([invalidKey1, duplicate, invalidKey2]); + + expect(result[0].index).toEqual(invalidKey1.index); + expect(result[0].operatorIndex).toEqual(invalidKey1.operatorIndex); + expect(result[0].used).toEqual(invalidKey1.used); + expect(result[0].moduleAddress).toEqual(invalidKey1.moduleAddress); + }); + + it('validate with use of cache ', async () => { + const duplicate = { ...invalidKey1, index: 102 }; + // Test scenario where one invalid key was removed from request's list + const newResult = await keysValidationService.getInvalidKeys( + [...validKeys, invalidKey1, duplicate, invalidKey2], + wc, + ); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith([]); + expect(newResult).toEqual([invalidKey1, duplicate, invalidKey2]); + }); + + it('validate without use of cache because of signature change', async () => { + const duplicate = { ...invalidKey1, index: 102 }; + const invalidKey2Fix = { + ...invalidKey2, + depositSignature: invalidKey2GoodSign, + }; + const keyForValidation = [ + ...validKeys, + invalidKey1, + duplicate, + // change signature on valid + invalidKey2Fix, + ]; + const newResult = await keysValidationService.getInvalidKeys( + keyForValidation, + wc, + ); + const depositKeyList = [invalidKey2Fix].map((key) => ({ + ...key, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(depositKeyList); + expect(newResult).toEqual([invalidKey1, duplicate]); + }); }); }); diff --git a/src/guardian/keys-validation/keys.fixtures.ts b/src/guardian/keys-validation/keys.fixtures.ts index a3de6fa1..e45c348a 100644 --- a/src/guardian/keys-validation/keys.fixtures.ts +++ b/src/guardian/keys-validation/keys.fixtures.ts @@ -10,6 +10,7 @@ export const validKeys: RegistryKey[] = [ used: false, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', index: 51, + vetted: true, }, { key: '0xb3c90525010a5710d43acbea46047fc37ed55306d032527fa15dd7e8cd8a9a5fa490347cc5fce59936fb8300683cd9f3', @@ -19,6 +20,7 @@ export const validKeys: RegistryKey[] = [ used: false, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', index: 52, + vetted: true, }, ]; export const invalidKey1: RegistryKey = { @@ -29,6 +31,7 @@ export const invalidKey1: RegistryKey = { used: false, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 5, + vetted: true, }; export const invalidKey2: RegistryKey = { @@ -39,6 +42,7 @@ export const invalidKey2: RegistryKey = { used: false, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', index: 54, + vetted: true, }; export const invalidKey2GoodSign = diff --git a/src/guardian/staking-module-guard/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts index d9a83f61..0d38d3b2 100644 --- a/src/guardian/staking-module-guard/keys.fixtures.ts +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -1,4 +1,6 @@ -export const vettedKeys = [ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + +export const vettedKeys: RegistryKey[] = [ { key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', depositSignature: @@ -7,260 +9,6 @@ export const vettedKeys = [ used: false, moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', index: 101, - }, -]; - -export const vettedKeysDuplicatesAcrossModules: any = [ - { - stakingModuleId: 100, - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - ], - }, - { - stakingModuleId: 102, - vettedUnusedKeys: [ - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - ], - }, - { - stakingModuleId: 103, - vettedUnusedKeys: [ - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, - }, - ], - }, -]; - -export const vettedKeysDuplicatesAcrossOneModule: any = [ - { - stakingModuleId: 100, - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 102, - }, - ], - }, - { - stakingModuleId: 102, - vettedUnusedKeys: [ - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - ], - }, - - { - stakingModuleId: 103, - vettedUnusedKeys: [ - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, - }, - ], - }, -]; - -export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ - { - stakingModuleId: 100, - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - ], - }, - { - stakingModuleId: 102, - vettedUnusedKeys: [ - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 6, - }, - ], - }, - { - stakingModuleId: 103, - vettedUnusedKeys: [ - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, - }, - ], - }, -]; - -export const vettedKeysWithoutDuplicates: any = [ - { - stakingModuleId: 100, - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - ], - }, - - { - stakingModuleId: 102, - vettedUnusedKeys: [ - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - ], - }, - - { - stakingModuleId: 103, - vettedUnusedKeys: [ - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, - }, - ], + vetted: true, }, ]; diff --git a/src/guardian/staking-module-guard/staking-module-guard.service.ts b/src/guardian/staking-module-guard/staking-module-guard.service.ts index 7e6d05ce..da74f1b5 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -4,7 +4,7 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { VerifiedDepositEvent, VerifiedDepositEventGroup, -} from 'contracts/deposit'; +} from 'contracts/deposits-registry'; import { SecurityService } from 'contracts/security'; import { ContractsState, BlockData, StakingModuleData } from '../interfaces'; @@ -33,7 +33,15 @@ export class StakingModuleGuardService { private lastContractsStateByModuleId: Record = {}; - isFirstEventEarlier( + /** + * Determines if the first event occurred earlier than the second event. + * Compares block numbers first; if they are equal, compares log indexes. + * + * @param firstEvent - The first event to compare. + * @param secondEvent - The second event to compare. + * @returns True if the first event is earlier, false otherwise. + */ + private isFirstEventEarlier( firstEvent: VerifiedDepositEvent, secondEvent: VerifiedDepositEvent, ) { @@ -49,70 +57,136 @@ export class StakingModuleGuardService { return isFirstEventEarlier; } + /** - * Method is not taking into account WC rotation since historical deposits were checked manually - * @param blockData - * @returns + * Filters and retrieves deposit events that have Lido's withdrawal credentials + * and are marked as valid. + * + * @param depositedEvents - A group of deposit events. + * @param lidoWC - The withdrawal credential associated with Lido. + * @returns An array of deposit events that match the Lido withdrawal credential and are valid. */ - public async getHistoricalFrontRun( + private getDepositsWithLidoWC( depositedEvents: VerifiedDepositEventGroup, lidoWC: string, - ) { - const potentialLidoDepositsEvents = depositedEvents.events.filter( + ): VerifiedDepositEvent[] { + // Filter events for those with Lido withdrawal credentials and valid status + const depositsMatchingLidoWC = depositedEvents.events.filter( ({ wc, valid }) => wc === lidoWC && valid, ); - this.logger.log('potential lido deposits events count', { - count: potentialLidoDepositsEvents.length, + this.logger.log('Deposits matching Lido WC count', { + count: depositsMatchingLidoWC.length, }); - const potentialLidoDepositsKeysMap: Record = + return depositsMatchingLidoWC; + } + + /** + * Creates a map of the earliest deposit events for each public key. + * If multiple deposits are found for the same public key, only the earliest one is stored. + * + * @param depositsMatchingLidoWC - Array of deposit events that match the Lido withdrawal credential + * @returns A record map with public keys as keys and the earliest deposit events as values. + */ + private getEarliestDepositsMap( + depositsMatchingLidoWC: VerifiedDepositEvent[], + ): Record { + const earliestLidoWCDepositsByPubkey: Record = {}; - potentialLidoDepositsEvents.forEach((event) => { - if (potentialLidoDepositsKeysMap[event.pubkey]) { - const existed = potentialLidoDepositsKeysMap[event.pubkey]; - const isExisted = this.isFirstEventEarlier(existed, event); - // this should not happen, since Lido deposits once per key. - // but someone can still make such a deposit. - if (isExisted) return; + depositsMatchingLidoWC.forEach((event) => { + const existingDeposit = earliestLidoWCDepositsByPubkey[event.pubkey]; + + if (existingDeposit) { + const isExistingEarlier = this.isFirstEventEarlier( + existingDeposit, + event, + ); + // This should not happen, since only one deposit per key is expected. + // However, someone could still make such a deposit. + if (isExistingEarlier) return; } - potentialLidoDepositsKeysMap[event.pubkey] = event; + earliestLidoWCDepositsByPubkey[event.pubkey] = event; }); - const duplicatedDepositEvents: VerifiedDepositEvent[] = []; + return earliestLidoWCDepositsByPubkey; + } - depositedEvents.events.forEach((event) => { - if (potentialLidoDepositsKeysMap[event.pubkey] && event.wc !== lidoWC) { - duplicatedDepositEvents.push(event); + /** + * Identifies duplicated deposit events that have non-Lido withdrawal credentials. + * These are deposits made on the same public key but with different withdrawal credentials. + * + * @param depositEventGroup - A group of deposit events. + * @param lidoWithdrawalCredential - The withdrawal credential associated with Lido. + * @param earliestDepositsByPubkey - A map of the earliest deposit events with Lido wc by public key. + * @returns An array of duplicated deposit events with non-Lido withdrawal credentials. + */ + private getNonLidoDuplicatedDeposits( + depositedEventsGroup: VerifiedDepositEventGroup, + lidoWC: string, + earliestLidoWCDepositsByPubkey: Record, + ): VerifiedDepositEvent[] { + const nonLidoDuplicatedDeposits: VerifiedDepositEvent[] = []; + + const { events: depositedEvents } = depositedEventsGroup; + + depositedEvents.forEach((event) => { + if (earliestLidoWCDepositsByPubkey[event.pubkey] && event.wc !== lidoWC) { + nonLidoDuplicatedDeposits.push(event); } }); - this.logger.log('duplicated deposit events', { - count: duplicatedDepositEvents.length, + this.logger.log('Non-Lido duplicated deposit events count', { + count: nonLidoDuplicatedDeposits.length, }); - const validDuplicatedDepositEvents = duplicatedDepositEvents.filter( + return nonLidoDuplicatedDeposits; + } + + /** + * Filters and returns valid duplicated deposit events from a given list. + * @param nonLidoDuplicatedDeposits - An array of duplicated deposit events with non-Lido withdrawal credentials + * @returns An array of valid duplicated deposit events. + */ + private getValidNonLidoDuplicatedDeposits( + nonLidoDuplicatedDeposits: VerifiedDepositEvent[], + ): VerifiedDepositEvent[] { + const validNonLidoDuplicatedDeposits = nonLidoDuplicatedDeposits.filter( (event) => event.valid, ); - this.logger.log('valid duplicated deposit events', { - count: validDuplicatedDepositEvents.length, + this.logger.log('Valid non-lido duplicated deposit events count', { + count: validNonLidoDuplicatedDeposits.length, }); - const frontRunnedDepositEvents = validDuplicatedDepositEvents.filter( + return validNonLidoDuplicatedDeposits; + } + + /** + * Identifies and returns the public keys associated with deposit events that front-ran the deposits with lido withdrawal credentials. + * @param validNonLidoDuplicatedDeposits - An array of duplicated deposit events with non-Lido withdrawal credentials. + * @param earliestLidoWCDepositsByPubkey - A map of the earliest deposit events with Lido withdrawal credentials by public key. + * @returns An array of public keys for events that front-ran deposits with lido withdrawal credentials. + */ + private getFrontRun( + validNonLidoDuplicatedDeposits: VerifiedDepositEvent[], + earliestLidoWCDepositsByPubkey: Record, + ): string[] { + const frontRunnedDepositEvents = validNonLidoDuplicatedDeposits.filter( (suspectedEvent) => { // get event from lido map const sameKeyLidoDeposit = - potentialLidoDepositsKeysMap[suspectedEvent.pubkey]; + earliestLidoWCDepositsByPubkey[suspectedEvent.pubkey]; + // TODO: do we need to leave here this check if (!sameKeyLidoDeposit) throw new Error('expected event not found'); return this.isFirstEventEarlier(suspectedEvent, sameKeyLidoDeposit); }, ); - this.logger.log('front runned deposit events', { + this.logger.log('Front-ran deposit events', { events: frontRunnedDepositEvents, }); @@ -120,30 +194,78 @@ export class StakingModuleGuardService { ({ pubkey }) => pubkey, ); - if (!frontRunnedDepositKeys.length) { + return frontRunnedDepositKeys; + } + + /** + * Retrieves the keys associated with front-runned deposits that were previously deposited by Lido. + * + * @param frontRunnedDepositKeys - An array of public keys for events that front-ran deposits with lido withdrawal credentials. + * @returns An array of registry keys that were previously deposited by Lido. + */ + private async getKeysDepositedByLido( + frontRunnedDepositKeys: string[], + ): Promise { + const { data: lidoDepositedKeys } = + await this.keysApiService.getKeysByPubkeys(frontRunnedDepositKeys); + + return lidoDepositedKeys.filter((key) => key.used); + } + + /** + * Checks if Lido deposits have been front-ran in the past based on historical deposit data. + * This method does not account for WC rotation as historical deposits were manually checked. + * + * @param depositedEvents - A group of historical deposit events. + * @param lidoWC - The withdrawal credential associated with Lido. + * @returns True if front-running was detected at any point in the past; false if no front-running occurred. + */ + public async getHistoricalFrontRun( + depositedEvents: VerifiedDepositEventGroup, + lidoWC: string, + ) { + const lidoWCDeposits = this.getDepositsWithLidoWC(depositedEvents, lidoWC); + + const earliestDepositsMap = this.getEarliestDepositsMap(lidoWCDeposits); + + const nonLidoDuplicatedDeposits = this.getNonLidoDuplicatedDeposits( + depositedEvents, + lidoWC, + earliestDepositsMap, + ); + + const validNonLidoDeposits = this.getValidNonLidoDuplicatedDeposits( + nonLidoDuplicatedDeposits, + ); + + const frontRunnedDepositKeys = this.getFrontRun( + validNonLidoDeposits, + earliestDepositsMap, + ); + + if (frontRunnedDepositKeys.length === 0) { return false; } - // TODO: deposit could be made by someone else - // and pubkey maybe not used. and we are able to unvet it - // but we will pause - // so maybe we need to filter by used field - const lidoDepositedKeys = await this.keysApiService.getKeysByPubkeys( + // front run happened only if these keys exist in lido contracts + const frontRunnedLidoDeposits = await this.getKeysDepositedByLido( frontRunnedDepositKeys, ); - const isLidoDepositedKeys = lidoDepositedKeys.data.length; + const hasFrontRunning = frontRunnedLidoDeposits.length > 0; - if (isLidoDepositedKeys) { - this.logger.warn('historical front-run found'); + if (hasFrontRunning) { + this.logger.warn('Found historical front-run', { + frontRunnedLidoDeposits, + }); } - return !!isLidoDepositedKeys; + return hasFrontRunning; } public async alreadyPausedDeposits(blockData: BlockData, version: number) { if (version === 3) { - const alreadyPaused = await this.securityService.isDepositContractPaused({ + const alreadyPaused = await this.securityService.isDepositsPaused({ blockHash: blockData.blockHash, }); @@ -239,9 +361,13 @@ export class StakingModuleGuardService { } public async handlePauseV3(blockData: BlockData): Promise { - const { blockNumber, guardianAddress, guardianIndex } = blockData; + const { blockNumber, blockHash, guardianAddress, guardianIndex } = + blockData; - const signature = await this.securityService.signPauseDataV3(blockNumber); + const signature = await this.securityService.signPauseDataV3( + blockNumber, + blockHash, + ); const pauseMessage = { guardianAddress, @@ -322,6 +448,7 @@ export class StakingModuleGuardService { const signature = await this.securityService.signPauseDataV2( blockNumber, + blockHash, stakingModuleId, ); @@ -362,13 +489,11 @@ export class StakingModuleGuardService { const { nonce, stakingModuleId, lastChangedBlockHash } = stakingModuleData; - // if we are here we didn't find invalid keys const currentContractState = { nonce, depositRoot, blockNumber, lastChangedBlockHash, - // if we are here we didn't find invalid keys }; const lastContractsState = @@ -458,10 +583,12 @@ export class StakingModuleGuardService { if (!firstState || !secondState) return false; if (firstState.depositRoot !== secondState.depositRoot) return false; - // If the nonce is unchanged, the state might still have changed. - // Therefore, we need to compare the 'lastChangedBlockHash' instead - // It's important to note that it's not possible for the nonce to be different - // while having the same 'lastChangedBlockHash'. + // If the nonce is unchanged, the state might still have changed due to a reorganization. + // Therefore, we need to compare the 'lastChangedBlockHash' instead. + // It's important to note that the nonce cannot be different while having the same 'lastChangedBlockHash'. + // Additionally, it's important to note that 'lastChangedBlockHash' will change not only during key update-related events, + // but also when a node operator is added, when node operator data is changed, during a reorganization, and so on. + // TODO: We may need to reconsider this approach for the Data Bus. if (firstState.lastChangedBlockHash !== secondState.lastChangedBlockHash) return false; diff --git a/src/guardian/staking-module-guard/staking-module-guard.spec.ts b/src/guardian/staking-module-guard/staking-module-guard.spec.ts index c5e6cb31..479fb90d 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -7,7 +7,6 @@ import { ConfigModule } from 'common/config'; import { PrometheusModule } from 'common/prometheus'; import { SecurityModule, SecurityService } from 'contracts/security'; import { RepositoryModule } from 'contracts/repository'; -import { LidoModule } from 'contracts/lido'; import { StakingModuleGuardModule } from './staking-module-guard.module'; import { GuardianMetricsModule } from '../guardian-metrics'; import { @@ -56,7 +55,6 @@ describe('StakingModuleGuardService', () => { LoggerModule, StakingModuleGuardModule, SecurityModule, - LidoModule, KeysApiModule, GuardianMetricsModule, GuardianMessageModule, @@ -309,7 +307,6 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', - invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates( { ...state }, @@ -324,7 +321,6 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', - invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -339,7 +335,6 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', - invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -354,7 +349,6 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', - invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -371,7 +365,6 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', - invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, diff --git a/src/guardian/unvetting/fixtures.ts b/src/guardian/unvetting/fixtures.ts index 7681946a..af0bf964 100644 --- a/src/guardian/unvetting/fixtures.ts +++ b/src/guardian/unvetting/fixtures.ts @@ -1,5 +1,7 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + // holesky -export const mockKeys = [ +export const mockKeys: RegistryKey[] = [ { key: '0x80d12670ec69b62abd4d24c828136cbb1666a63374a66269031d6101973419b66711ed712d17da05d7ca6c0b28ecd21f', depositSignature: @@ -8,6 +10,7 @@ export const mockKeys = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 0, + vetted: true, }, { key: '0x81011ad6ebe5c7844e59b1799e12de769f785f66df3f63debb06149c1782d574c8c2cd9c923fa881e9dcf6d413159863', @@ -17,6 +20,7 @@ export const mockKeys = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 1, + vetted: true, }, { key: '0x823c9c577aead54ac40c7986ceb8596eaf45df0140fe9b637bb8d465f878884e3f9e39914edf39c3c64f5720ec0be3a4', @@ -26,6 +30,7 @@ export const mockKeys = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 2, + vetted: true, }, { key: '0x837851278c4ab4a4641128a709c9c985f7e4c7c35082e5e2a75ae4ed712c8161b493b135b35d39ee8a65024122feb7c1', @@ -35,6 +40,7 @@ export const mockKeys = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 3, + vetted: true, }, { key: '0x80db3318374e7c1489e1f421a66bf1ef51a48f6ad02a6ad304c67fbbad60a0a5ce51a939aa008930c3b0ed25db63710f', @@ -44,10 +50,11 @@ export const mockKeys = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 0, + vetted: true, }, ]; -export const mockKeys2 = [ +export const mockKeys2: RegistryKey[] = [ ...mockKeys, { key: '0x8101cf19c664f22c5209c4129cf20629d8375a2de6a26f089ea37d142d000abe6b3585ab5bc7818c7449ed5089c86054', @@ -57,5 +64,6 @@ export const mockKeys2 = [ used: true, moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', index: 1, + vetted: true, }, ]; diff --git a/src/guardian/unvetting/unvetting.service.spec.ts b/src/guardian/unvetting/unvetting.service.spec.ts index 9cb00994..7240042a 100644 --- a/src/guardian/unvetting/unvetting.service.spec.ts +++ b/src/guardian/unvetting/unvetting.service.spec.ts @@ -11,6 +11,7 @@ import { LoggerModule } from 'common/logger'; import { UnvettingModule } from './unvetting.module'; import { PrometheusModule } from 'common/prometheus'; import { MockProviderModule } from 'provider'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; jest.mock('../../transport/stomp/stomp.client'); @@ -52,6 +53,10 @@ describe('UnvettingService', () => { guardianMessageService = moduleRef.get( GuardianMessageService, ); + + const loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); + jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); }); beforeEach(() => { diff --git a/src/guardian/unvetting/unvetting.service.ts b/src/guardian/unvetting/unvetting.service.ts index 2c1ed0e1..aee26607 100644 --- a/src/guardian/unvetting/unvetting.service.ts +++ b/src/guardian/unvetting/unvetting.service.ts @@ -52,11 +52,10 @@ export class UnvettingService { private collectInvalidKeys( stakingModuleData: StakingModuleData, ): RegistryKey[] { - return [ - ...stakingModuleData.invalidKeys, - ...stakingModuleData.duplicatedKeys, - ...stakingModuleData.frontRunKeys, - ]; + return stakingModuleData.invalidKeys.concat( + stakingModuleData.duplicatedKeys, + stakingModuleData.frontRunKeys, + ); } private logNoUnvettingNeeded( diff --git a/src/keys-api/interfaces/RegistryKey.ts b/src/keys-api/interfaces/RegistryKey.ts index 4b133b3f..4fd43f8d 100644 --- a/src/keys-api/interfaces/RegistryKey.ts +++ b/src/keys-api/interfaces/RegistryKey.ts @@ -24,4 +24,8 @@ export type RegistryKey = { * Staking module address */ moduleAddress: string; + /** + * Vetted key status + */ + vetted: boolean; }; diff --git a/src/keys-api/interfaces/SRModuleListResponse.ts b/src/keys-api/interfaces/SRModuleListResponse.ts new file mode 100644 index 00000000..9bbf7f3a --- /dev/null +++ b/src/keys-api/interfaces/SRModuleListResponse.ts @@ -0,0 +1,7 @@ +import { SRModule } from '.'; +import { ELBlockSnapshot } from './ELBlockSnapshot'; + +export type SRModuleListResponse = { + data: Array; + elBlockSnapshot: ELBlockSnapshot; +}; diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index e79931ad..5097a481 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -7,6 +7,7 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse'; import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; +import { SRModuleListResponse } from './interfaces/SRModuleListResponse'; @Injectable() export class KeysApiService { @@ -16,6 +17,13 @@ export class KeysApiService { protected readonly fetchService: FetchService, ) {} + private getBaseUrl() { + const baseUrl = + this.config.KEYS_API_URL || + `${this.config.KEYS_API_HOST}:${this.config.KEYS_API_PORT}`; + return baseUrl; + } + protected async fetch(url: string, requestInit?: RequestInit) { const controller = new AbortController(); const { signal } = controller; @@ -24,8 +32,7 @@ export class KeysApiService { controller.abort(); }, FETCH_REQUEST_TIMEOUT); - const baseUrl = `${this.config.KEYS_API_HOST}:${this.config.KEYS_API_PORT}`; - + const baseUrl = this.getBaseUrl(); try { const res: Response = await this.fetchService.fetchJson( `${baseUrl}${url}`, @@ -34,9 +41,10 @@ export class KeysApiService { ...requestInit, }, ); + clearTimeout(timer); return res; - } catch (error) { + } catch (error: any) { clearTimeout(timer); throw error; } @@ -80,6 +88,11 @@ export class KeysApiService { return result; } + public async getModules() { + const result = await this.fetch(`/v1/modules`); + return result; + } + /** * Verifies the consistency of metadata by comparing hashes. * @param firstRequestHash - Hash of the first request diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index bb107bd2..d591b2d9 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -54,8 +54,8 @@ export class ProviderService { /** * Returns current block */ - public async getBlock(): Promise { - return await this.provider.getBlock('latest'); + public async getBlock(tag: string | number = 'latest'): Promise { + return await this.provider.getBlock(tag); } /** diff --git a/src/staking-module-data-collector/index.ts b/src/staking-module-data-collector/index.ts new file mode 100644 index 00000000..3734cbb3 --- /dev/null +++ b/src/staking-module-data-collector/index.ts @@ -0,0 +1,2 @@ +export * from './staking-module-data-collector.module'; +export * from './staking-module-data-collector.service'; diff --git a/src/staking-router/keys.fixtures.ts b/src/staking-module-data-collector/keys.fixtures.ts similarity index 100% rename from src/staking-router/keys.fixtures.ts rename to src/staking-module-data-collector/keys.fixtures.ts diff --git a/src/staking-router/operators.fixtures.ts b/src/staking-module-data-collector/operators.fixtures.ts similarity index 100% rename from src/staking-router/operators.fixtures.ts rename to src/staking-module-data-collector/operators.fixtures.ts diff --git a/src/staking-router/staking-router.module.ts b/src/staking-module-data-collector/staking-module-data-collector.module.ts similarity index 57% rename from src/staking-router/staking-router.module.ts rename to src/staking-module-data-collector/staking-module-data-collector.module.ts index f5db2a41..4f1dc34a 100644 --- a/src/staking-router/staking-router.module.ts +++ b/src/staking-module-data-collector/staking-module-data-collector.module.ts @@ -1,20 +1,20 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from 'common/config'; -import { StakingRouterService } from './staking-router.service'; -import { SecurityModule } from 'contracts/security'; +import { StakingModuleDataCollectorService } from './staking-module-data-collector.service'; import { StakingModuleGuardModule } from 'guardian/staking-module-guard'; import { KeysDuplicationCheckerModule } from 'guardian/duplicates'; import { GuardianMetricsModule } from 'guardian/guardian-metrics'; +import { StakingRouterModule } from 'contracts/staking-router'; @Module({ imports: [ ConfigModule, - SecurityModule, StakingModuleGuardModule, KeysDuplicationCheckerModule, GuardianMetricsModule, + StakingRouterModule, ], - providers: [StakingRouterService], - exports: [StakingRouterService], + providers: [StakingModuleDataCollectorService], + exports: [StakingModuleDataCollectorService], }) -export class StakingRouterModule {} +export class StakingModuleDataCollectorModule {} diff --git a/src/staking-module-data-collector/staking-module-data-collector.service.ts b/src/staking-module-data-collector/staking-module-data-collector.service.ts new file mode 100644 index 00000000..1ac12067 --- /dev/null +++ b/src/staking-module-data-collector/staking-module-data-collector.service.ts @@ -0,0 +1,194 @@ +import { Injectable, LoggerService, Inject } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { StakingModuleData, BlockData } from 'guardian'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { StakingModuleGuardService } from 'guardian/staking-module-guard'; +import { KeysDuplicationCheckerService } from 'guardian/duplicates'; +import { GuardianMetricsService } from 'guardian/guardian-metrics'; +import { StakingRouterService } from 'contracts/staking-router'; +import { SRModule } from 'keys-api/interfaces'; +import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; + +type State = { + stakingModules: SRModule[]; + meta: ELBlockSnapshot; + lidoKeys: RegistryKey[]; +}; + +@Injectable() +export class StakingModuleDataCollectorService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, + private stakingModuleGuardService: StakingModuleGuardService, + private keysDuplicationCheckerService: KeysDuplicationCheckerService, + private guardianMetricsService: GuardianMetricsService, + private stakingRouterService: StakingRouterService, + ) {} + + /** + * Collects basic data about the staking module, including activity status, vetted unused keys list, ID, address, and nonce. + */ + public async collectStakingModuleData({ + stakingModules, + meta, + lidoKeys, + }: State): Promise { + return await Promise.all( + stakingModules.map(async (stakingModule) => { + return { + isModuleDepositsPaused: + await this.stakingRouterService.isModuleDepositsPaused( + stakingModule.id, + { + blockHash: meta.blockHash, + }, + ), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + stakingModuleAddress: stakingModule.stakingModuleAddress, + blockHash: meta.blockHash, + lastChangedBlockHash: meta.lastChangedBlockHash, + vettedUnusedKeys: this.getModuleVettedUnusedKeys( + stakingModule.stakingModuleAddress, + lidoKeys, + ), + duplicatedKeys: [], + invalidKeys: [], + frontRunKeys: [], + unresolvedDuplicatedKeys: [], + }; + }), + ); + } + + /** + * Check for duplicated, invalid, and front-run attempts + */ + public async checkKeys( + stakingModulesData: StakingModuleData[], + lidoKeys: RegistryKey[], + blockData: BlockData, + ): Promise { + const { duplicates, unresolved } = + await this.keysDuplicationCheckerService.getDuplicatedKeys( + lidoKeys, + blockData, + ); + + await Promise.all( + stakingModulesData.map(async (stakingModuleData) => { + // identify keys that were front-run withing vetted unused keys + stakingModuleData.frontRunKeys = + this.stakingModuleGuardService.getFrontRunAttempts( + stakingModuleData, + blockData, + ); + // identify keys with invalid signatures within vetted unused keys + stakingModuleData.invalidKeys = + await this.stakingModuleGuardService.getInvalidKeys( + stakingModuleData, + blockData, + ); + + // Filter all keys for the module to get the total number of duplicated keys, + // for Prometheus metrics + const allModuleDuplicatedKeys = this.getModuleKeys( + stakingModuleData.stakingModuleAddress, + duplicates, + ); + // Filter vetted and unused duplicated keys for the module + stakingModuleData.duplicatedKeys = this.getModuleVettedUnusedKeys( + stakingModuleData.stakingModuleAddress, + duplicates, + ); + + // Filter all unresolved keys (keys without a SigningKeyAdded event) for the module, + // including both vetted and unvetted keys, to show the total count of unresolved keys + // for Prometheus metrics + const allModuleUnresolved = this.getModuleKeys( + stakingModuleData.stakingModuleAddress, + unresolved, + ); + // Filter vetted and unused duplicated keys for the module + stakingModuleData.unresolvedDuplicatedKeys = + this.getModuleVettedUnusedKeys( + stakingModuleData.stakingModuleAddress, + unresolved, + ); + + this.collectModuleMetric( + stakingModuleData, + allModuleUnresolved, + stakingModuleData.unresolvedDuplicatedKeys, + allModuleDuplicatedKeys, + stakingModuleData.duplicatedKeys, + ); + + this.logKeysCheckState(stakingModuleData); + }), + ); + } + + private collectModuleMetric( + stakingModuleData: StakingModuleData, + unresolvedKeys: RegistryKey[], + vettedUnusedUnresolvedKeys: RegistryKey[], + duplicatedKeys: RegistryKey[], + vettedUnusedDuplcaitedKeys: RegistryKey[], + ) { + const { invalidKeys, stakingModuleId } = stakingModuleData; + + // Collect metrics for unresolved and duplicated keys in the staking module: + // - Total unresolved keys (keys without a corresponding SigningKeyAdded event) + // - Subset of unresolved keys that are vetted and unused + // - Total duplicated keys + // - Subset of duplicated keys that are vetted and unused + this.guardianMetricsService.collectDuplicatedKeysMetrics( + stakingModuleId, + unresolvedKeys.length, + vettedUnusedUnresolvedKeys.length, + duplicatedKeys.length, + vettedUnusedDuplcaitedKeys.length, + ); + + // Collect metrics for the total number of vetted unused keys with invalid signatures within the staking module + this.guardianMetricsService.collectInvalidKeysMetrics( + stakingModuleId, + invalidKeys.length, + ); + } + + private logKeysCheckState(stakingModuleData: StakingModuleData) { + const { + stakingModuleId, + blockHash, + frontRunKeys, + invalidKeys, + duplicatedKeys, + unresolvedDuplicatedKeys, + } = stakingModuleData; + this.logger.log('Keys check state', { + stakingModuleId: stakingModuleId, + frontRunAttempt: frontRunKeys.length, + invalid: invalidKeys.length, + duplicated: duplicatedKeys.length, + unresolvedDuplicated: unresolvedDuplicatedKeys.length, + blockHash: blockHash, + }); + } + + private getModuleKeys(stakingModuleAddress: string, keys: RegistryKey[]) { + return keys.filter((key) => key.moduleAddress === stakingModuleAddress); + } + + private getModuleVettedUnusedKeys( + stakingModuleAddress: string, + lidoKeys: RegistryKey[], + ) { + const vettedUnusedKeys = lidoKeys.filter( + (key) => + !key.used && key.vetted && key.moduleAddress === stakingModuleAddress, + ); + return vettedUnusedKeys; + } +} diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts deleted file mode 100644 index b57fdc06..00000000 --- a/src/staking-router/staking-router.service.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Injectable, LoggerService, Inject } from '@nestjs/common'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { StakingModuleData, BlockData } from 'guardian'; -import { getVettedUnusedKeys } from './vetted-keys'; -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -import { Meta } from 'keys-api/interfaces/Meta'; -import { SROperatorListWithModule } from 'keys-api/interfaces/SROperatorListWithModule'; -import { SecurityService } from 'contracts/security'; -import { StakingModuleGuardService } from 'guardian/staking-module-guard'; -import { KeysDuplicationCheckerService } from 'guardian/duplicates'; -import { GuardianMetricsService } from 'guardian/guardian-metrics'; - -type State = { - operatorsByModules: SROperatorListWithModule[]; - meta: Meta; - lidoKeys: RegistryKey[]; - blockData: BlockData; -}; - -@Injectable() -export class StakingRouterService { - constructor( - @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, - private securityService: SecurityService, - private stakingModuleGuardService: StakingModuleGuardService, - private keysDuplicationCheckerService: KeysDuplicationCheckerService, - private guardianMetricsService: GuardianMetricsService, - ) {} - - /** - * Collects basic data about the staking module, including activity status, vetted unused keys list, ID, address, and nonce. - */ - public async collectStakingModuleData({ - operatorsByModules, - meta, - lidoKeys, - blockData, - }: State): Promise { - return await Promise.all( - operatorsByModules.map(async ({ operators, module: stakingModule }) => { - const unusedKeys = lidoKeys.filter( - (key) => - !key.used && - key.moduleAddress === stakingModule.stakingModuleAddress, - ); - - const moduleVettedUnusedKeys = getVettedUnusedKeys( - operators, - unusedKeys, - ); - - // check pause - const isModuleDepositsPaused = - await this.securityService.isModuleDepositsPaused(stakingModule.id, { - blockHash: blockData.blockHash, - }); - - return { - isModuleDepositsPaused, - nonce: stakingModule.nonce, - stakingModuleId: stakingModule.id, - stakingModuleAddress: stakingModule.stakingModuleAddress, - blockHash: blockData.blockHash, - lastChangedBlockHash: meta.elBlockSnapshot.lastChangedBlockHash, - vettedUnusedKeys: moduleVettedUnusedKeys, - duplicatedKeys: [], - invalidKeys: [], - frontRunKeys: [], - unresolvedDuplicatedKeys: [], - }; - }), - ); - } - - /** - * Check for duplicated, invalid, and front-run attempts - */ - public async checkKeys( - stakingModulesData: StakingModuleData[], - lidoKeys: RegistryKey[], - blockData: BlockData, - ): Promise { - const { duplicates, unresolved } = - await this.keysDuplicationCheckerService.getDuplicatedKeys( - lidoKeys, - blockData, - ); - - await Promise.all( - stakingModulesData.map(async (stakingModuleData) => { - stakingModuleData.frontRunKeys = - this.stakingModuleGuardService.getFrontRunAttempts( - stakingModuleData, - blockData, - ); - stakingModuleData.invalidKeys = - await this.stakingModuleGuardService.getInvalidKeys( - stakingModuleData, - blockData, - ); - const allDuplicatedKeys = this.getModuleKeys( - stakingModuleData.stakingModuleAddress, - duplicates, - ); - stakingModuleData.duplicatedKeys = this.getVettedUnusedKeys( - stakingModuleData.vettedUnusedKeys, - allDuplicatedKeys, - ); - - const allUnresolved = this.getModuleKeys( - stakingModuleData.stakingModuleAddress, - unresolved, - ); - - stakingModuleData.unresolvedDuplicatedKeys = this.getVettedUnusedKeys( - stakingModuleData.vettedUnusedKeys, - allUnresolved, - ); - - this.guardianMetricsService.collectDuplicatedKeysMetrics( - stakingModuleData.stakingModuleId, - allUnresolved.length, - stakingModuleData.unresolvedDuplicatedKeys.length, - allDuplicatedKeys.length, - stakingModuleData.duplicatedKeys.length, - ); - - this.guardianMetricsService.collectInvalidKeysMetrics( - stakingModuleData.stakingModuleId, - stakingModuleData.invalidKeys.length, - ); - - this.logger.log('Keys check state', { - stakingModuleId: stakingModuleData.stakingModuleId, - frontRunAttempt: stakingModuleData.frontRunKeys.length, - invalid: stakingModuleData.invalidKeys.length, - duplicated: stakingModuleData.duplicatedKeys.length, - unresolvedDuplicated: - stakingModuleData.unresolvedDuplicatedKeys.length, - blockNumber: blockData.blockNumber, - }); - }), - ); - } - - private getModuleKeys(stakingModuleAddress: string, keys: RegistryKey[]) { - return keys.filter((key) => key.moduleAddress === stakingModuleAddress); - } - - /** - * filter from the list all keys that are not vetted as unused - */ - public getVettedUnusedKeys( - vettedUnusedKeys: RegistryKey[], - keys: RegistryKey[], - ) { - const vettedUnused = keys.filter((key) => { - const r = vettedUnusedKeys.some( - (k) => k.index == key.index && k.operatorIndex == key.operatorIndex, - ); - - return r; - }); - - return vettedUnused; - } -} diff --git a/src/staking-router/vetted-keys.spec.ts b/src/staking-router/vetted-keys.spec.ts deleted file mode 100644 index caa6360e..00000000 --- a/src/staking-router/vetted-keys.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { getVettedUnusedKeys } from './vetted-keys'; // Replace with your actual module path - -describe('getVettedUnusedKeys', () => { - test('should return an empty array for empty input arrays', () => { - expect(getVettedUnusedKeys([], [])).toEqual([]); - }); - - test('should correctly filter and sort keys for multiple operators', () => { - // totalSigningKeys is used here only to describe cases, - // we don't use is in algorithm in function to determine vetted unused keys - const operators = [ - // 2 vetted unused keys, have some available limit - { index: 1, stakingLimit: 3, usedSigningKeys: 1, totalSigningKeys: 4 }, - // 1 vetted unused key, have some available limit - { index: 2, stakingLimit: 1, usedSigningKeys: 0, totalSigningKeys: 2 }, - // 0 vetted unused keys, staking limit wasnt increased - { index: 3, stakingLimit: 0, usedSigningKeys: 0, totalSigningKeys: 1 }, - // 0 vetted unused keys, staking limit exceeded have one used key - { index: 4, stakingLimit: 1, usedSigningKeys: 1, totalSigningKeys: 2 }, - // 0 vetted unused keys, have staking limit, but don't have keys to deposit - { index: 5, stakingLimit: 1, usedSigningKeys: 0, totalSigningKeys: 0 }, - ] as any; - - const unusedKeys = [ - // operator 1 unused keys - { operatorIndex: 1, index: 1 }, - { operatorIndex: 1, index: 0 }, - { operatorIndex: 1, index: 2 }, - // operator 2 unused keys - { operatorIndex: 2, index: 0 }, - { operatorIndex: 2, index: 1 }, - // operator 3 unused keys - { operatorIndex: 3, index: 0 }, - // operator 4 unused keys - { operatorIndex: 4, index: 0 }, - ] as any; - - const expected = [ - { operatorIndex: 1, index: 0 }, - { operatorIndex: 1, index: 1 }, - { operatorIndex: 2, index: 0 }, - ]; - const result = getVettedUnusedKeys(operators, unusedKeys); - expect(result.length).toEqual(expected.length); - expect(getVettedUnusedKeys(operators, unusedKeys)).toEqual(expected); - }); - - test('should correctly sort keys within operators', () => { - const operators = [ - { index: 1, stakingLimit: 4, usedSigningKeys: 1, totalSigningKeys: 5 }, - ] as any; - const unusedKeys = [ - { operatorIndex: 1, index: 3 }, - { operatorIndex: 1, index: 1 }, - { operatorIndex: 1, index: 2 }, - { operatorIndex: 1, index: 4 }, - ] as any; - const expected = [ - { operatorIndex: 1, index: 1 }, - { operatorIndex: 1, index: 2 }, - { operatorIndex: 1, index: 3 }, - ]; - expect(getVettedUnusedKeys(operators, unusedKeys)).toEqual(expected); - }); -}); diff --git a/src/staking-router/vetted-keys.ts b/src/staking-router/vetted-keys.ts deleted file mode 100644 index cfdc0671..00000000 --- a/src/staking-router/vetted-keys.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; - -export function getVettedUnusedKeys( - operators: RegistryOperator[], - unusedKeys: RegistryKey[], -): RegistryKey[] { - return operators.flatMap((operator) => { - const operatorKeys = unusedKeys - .filter((key) => key.operatorIndex === operator.index) - .sort((a, b) => a.index - b.index) - // stakingLimit limit cant be less than usedSigningKeys - .slice(0, operator.stakingLimit - operator.usedSigningKeys); - - return operatorKeys; - }); -} diff --git a/src/wallet/wallet.interfaces.ts b/src/wallet/wallet.interfaces.ts index be2bcdc5..26cdf8cd 100644 --- a/src/wallet/wallet.interfaces.ts +++ b/src/wallet/wallet.interfaces.ts @@ -3,7 +3,7 @@ export interface SignDepositDataParams { blockNumber: number; blockHash: string; depositRoot: string; - keysOpIndex: number; + nonce: number; stakingModuleId: number; } diff --git a/src/wallet/wallet.service.spec.ts b/src/wallet/wallet.service.spec.ts index 0c93207c..159ff943 100644 --- a/src/wallet/wallet.service.spec.ts +++ b/src/wallet/wallet.service.spec.ts @@ -79,13 +79,13 @@ describe('WalletService', () => { it('should sign deposit data', async () => { const prefix = hexZeroPad('0x1', 32); const depositRoot = hexZeroPad('0x2', 32); - const keysOpIndex = 1; + const nonce = 1; const blockNumber = 1; const blockHash = hexZeroPad('0x3', 32); const signature = await walletService.signDepositData({ prefix, depositRoot, - keysOpIndex, + nonce, blockNumber, blockHash, stakingModuleId: TEST_MODULE_ID, diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts index 0852242d..1292dffd 100644 --- a/src/wallet/wallet.service.ts +++ b/src/wallet/wallet.service.ts @@ -167,7 +167,7 @@ export class WalletService implements OnModuleInit { * @param signDepositDataParams - parameters for signing deposit message * @param signDepositDataParams.prefix - unique prefix from the contract for this type of message * @param signDepositDataParams.depositRoot - current deposit root from the deposit contract - * @param signDepositDataParams.keysOpIndex - current index of keys operations from the registry contract + * @param signDepositDataParams.nonce - current index of keys operations from the registry contract * @param signDepositDataParams.blockNumber - current block number * @param signDepositDataParams.blockHash - current block hash * @param signDepositDataParams.stakingModuleId - target module id @@ -178,19 +178,12 @@ export class WalletService implements OnModuleInit { blockNumber, blockHash, depositRoot, - keysOpIndex, + nonce, stakingModuleId, }: SignDepositDataParams): Promise { const encodedData = defaultAbiCoder.encode( ['bytes32', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256'], - [ - prefix, - blockNumber, - blockHash, - depositRoot, - stakingModuleId, - keysOpIndex, - ], + [prefix, blockNumber, blockHash, depositRoot, stakingModuleId, nonce], ); const messageHash = keccak256(encodedData); diff --git a/test/duplicates-v3.e2e-spec.ts b/test/duplicates-v3.e2e-spec.ts index bc22d066..0140e624 100644 --- a/test/duplicates-v3.e2e-spec.ts +++ b/test/duplicates-v3.e2e-spec.ts @@ -1,11 +1,3 @@ -import { - mockOperator1, - mockOperator2, - mockedDvtOperators, - mockedOperators, - setupMockModules, -} from './helpers'; - // Constants import { TESTS_TIMEOUT, @@ -14,13 +6,9 @@ import { CHAIN_ID, FORK_BLOCK, GANACHE_PORT, - sk, - pk, NOP_REGISTRY, SIMPLE_DVT, UNLOCKED_ACCOUNTS, - CSM, - SANDBOX, } from './constants'; // Contract Factories @@ -36,43 +24,43 @@ import { closeServer, initLevelDB, } from './helpers/test-setup'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { makeDeposit, signDeposit } from './helpers/deposit'; +import { getWalletAddress } from './helpers/deposit'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; import { ProviderService } from 'provider'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { SecurityService } from 'contracts/security'; import { Server } from 'ganache'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; -import { StakingRouterService } from 'staking-router'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { makeServer } from './server'; import { addGuardians } from './helpers/dsm'; import { BlsService } from 'bls'; import { mockKey, mockKey2, mockKeyEvent } from './helpers/keys-fixtures'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; -import { StakingModuleGuardService } from 'guardian/staking-module-guard'; +import { + keysApiMockGetAllKeys, + keysApiMockGetModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, +} from './helpers'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; describe('Deposits in case of duplicates', () => { let server: Server<'ethereum'>; let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let securityService: SecurityService; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; let signKeyLevelDBService: SignKeyLevelDBService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; - let stakingModuleGuardService: StakingModuleGuardService; let guardianMessageService: GuardianMessageService; - let stakingRouterService: StakingRouterService; - // methods mocks let sendDepositMessage: jest.SpyInstance; let sendUnvetMessage: jest.SpyInstance; @@ -105,23 +93,26 @@ describe('Deposits in case of duplicates', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); // mock unvetting method of contract // as we dont use real keys api and work with fixtures of operators and keys // we cant make real unvetting unvetSigningKeys = jest .spyOn(securityService, 'unvetSigningKeys') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(null as any)); }; const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -130,13 +121,12 @@ describe('Deposits in case of duplicates', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); @@ -151,8 +141,6 @@ describe('Deposits in case of duplicates', () => { // main service that check keys and make decision guardianService = moduleRef.get(GuardianService); - stakingModuleGuardService = moduleRef.get(StakingModuleGuardService); - stakingRouterService = moduleRef.get(StakingRouterService); }; beforeEach(async () => { @@ -172,12 +160,8 @@ describe('Deposits in case of duplicates', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - // TODO: mine new block instead - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); - // Set deposit cache - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -185,35 +169,41 @@ describe('Deposits in case of duplicates', () => { }, }); - // Keys api mock - const unusedKeys = [ - mockKey, - { ...mockKey, index: 1 }, - { - ...mockKey, - index: 2, - }, + const earliestKey = { + ...mockKey, + operatorIndex: 0, + moduleAddress: NOP_REGISTRY, + index: 0, + }; + const duplicates = [ + earliestKey, + { ...mockKey, operatorIndex: 0, moduleAddress: NOP_REGISTRY, index: 1 }, + { ...mockKey, operatorIndex: 0, moduleAddress: NOP_REGISTRY, index: 2 }, + ]; + // Mock Keys API + const vettedUnusedKeys = [ + ...duplicates, { ...mockKey2, moduleAddress: SIMPLE_DVT, }, ]; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, vettedUnusedKeys, meta); // mock events cache to check - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], // dont need events in this test headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -230,50 +220,56 @@ describe('Deposits in case of duplicates', () => { await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + const walletAddress = getWalletAddress(); // just skip on this iteration deposit for Curated staking module expect(sendDepositMessage).toBeCalledTimes(1); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); - // check that duplicates problem didnt trigger pause expect(sendPauseMessage).toBeCalledTimes(0); expect(sendUnvetMessage).toBeCalledTimes(1); expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000001', }), ); expect(unvetSigningKeys).toBeCalledTimes(1); + // Mine a new block + await providerService.provider.send('evm_mine', []); + // after deleting duplicates in staking module, // council will resume deposits to module const unusedKeysWithoutDuplicates = [ - mockKey, + earliestKey, { ...mockKey2, moduleAddress: SIMPLE_DVT, }, ]; + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); expect(newBlock.number).toBeGreaterThan(currentBlock.number); - setupMockModules( - newBlock, + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys( keysApiService, - mockedOperators, - mockedDvtOperators, unusedKeysWithoutDuplicates, + newMeta, ); sendDepositMessage.mockClear(); @@ -285,7 +281,7 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage.mock.calls[0][0]).toEqual( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 1, }), @@ -293,7 +289,7 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage.mock.calls[1][0]).toEqual( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 2, }), @@ -307,10 +303,8 @@ describe('Deposits in case of duplicates', () => { 'skip deposits for module if find duplicated key across operators of two modules', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); - - await depositService.setCachedEvents({ + const walletAddress = getWalletAddress(); + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -318,23 +312,23 @@ describe('Deposits in case of duplicates', () => { }, }); - const unusedKeys = [ - mockKey, + const duplicates = [ + { ...mockKey, moduleAddress: NOP_REGISTRY }, { ...mockKey, moduleAddress: SIMPLE_DVT, }, ]; - const { sdvtModule, curatedModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [ mockKeyEvent, // key of second module was added later @@ -348,7 +342,7 @@ describe('Deposits in case of duplicates', () => { headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -371,48 +365,49 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); - // check that duplicates problem didnt trigger pause + // check that duplicates problem didn't trigger pause expect(sendPauseMessage).toBeCalledTimes(0); expect(sendUnvetMessage).toBeCalledTimes(1); expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000000', }), ); expect(unvetSigningKeys).toBeCalledTimes(1); - expect(sendDepositMessage).toBeCalledTimes(1); - expect(sendPauseMessage).toBeCalledTimes(0); // after deleting duplicates in staking module, // council will resume deposits to module const unusedKeysWithoutDuplicates = [ - mockKey, - { - ...mockKey2, - moduleAddress: SIMPLE_DVT, - }, + { ...mockKey, moduleAddress: NOP_REGISTRY }, ]; + + // Mine a new block + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, + // setup elBlockSnapshot + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys( keysApiService, - mockedOperators, - mockedDvtOperators, unusedKeysWithoutDuplicates, + newMeta, ); sendDepositMessage.mockClear(); sendUnvetMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); @@ -422,21 +417,21 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); - expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendUnvetMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -445,10 +440,9 @@ describe('Deposits in case of duplicates', () => { 'skip deposits for module if find duplicated key across operators of one modules', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -456,37 +450,42 @@ describe('Deposits in case of duplicates', () => { }, }); - const unusedKeys = [ - { ...mockKey, operatorIndex: mockOperator1.index }, + const duplicates = [ + { ...mockKey, index: 0, operatorIndex: 0 }, { ...mockKey, - operatorIndex: mockOperator2.index, + index: 0, + operatorIndex: 1, }, ]; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [ - { ...mockKeyEvent, operatorIndex: mockOperator1.index }, - // key of second module was added later { ...mockKeyEvent, - operatorIndex: mockOperator2.index, - blockNumber: mockKeyEvent.blockNumber + 1, - blockHash: 'somefakehash', + blockNumber: currentBlock.number - 4, + blockHash: 'somefakehash1', + operatorIndex: 0, + }, + // key of second operator was added later + { + ...mockKeyEvent, + blockNumber: currentBlock.number - 3, + blockHash: 'somefakehash2', + operatorIndex: 1, }, ], headers: { - startBlock: currentBlock.number - 2, + startBlock: currentBlock.number - 5, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -509,9 +508,9 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); // check that duplicates problem didnt trigger pause @@ -520,39 +519,33 @@ describe('Deposits in case of duplicates', () => { expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000001', vettedKeysByOperator: '0x00000000000000000000000000000000', }), ); expect(unvetSigningKeys).toBeCalledTimes(1); - expect(sendDepositMessage).toBeCalledTimes(1); expect(sendPauseMessage).toBeCalledTimes(0); // after deleting duplicates in staking module, // council will resume deposits to module + const noDuplicatesKeys = [{ ...mockKey, operatorIndex: 0 }]; - const unusedKeysWithoutDuplicates = [ - { ...mockKey, operatorIndex: mockOperator1.index }, - { - ...mockKey2, - operatorIndex: mockOperator2.index, - }, - ]; - + // Mine a new block + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeysWithoutDuplicates, - ); + // setup elBlockSnapshot + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, noDuplicatesKeys, newMeta); sendDepositMessage.mockClear(); sendUnvetMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); @@ -562,20 +555,22 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); + expect(sendPauseMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -584,10 +579,9 @@ describe('Deposits in case of duplicates', () => { 'added unused keys for that deposit was already made', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -595,29 +589,29 @@ describe('Deposits in case of duplicates', () => { }, }); - const keys = [ - { ...mockKey, operatorIndex: mockOperator1.index, used: true }, + const duplicates = [ + { ...mockKey, operatorIndex: 0, used: true }, { ...mockKey, - operatorIndex: mockOperator2.index, + operatorIndex: 1, used: false, }, ]; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - keys, - ); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -636,14 +630,13 @@ describe('Deposits in case of duplicates', () => { await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); // deposit will be skipped until unvetting - // so list of keys can be changed expect(sendDepositMessage).toBeCalledTimes(1); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); @@ -651,9 +644,9 @@ describe('Deposits in case of duplicates', () => { expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000001', vettedKeysByOperator: '0x00000000000000000000000000000000', }), @@ -662,20 +655,20 @@ describe('Deposits in case of duplicates', () => { // after deleting duplicates in staking module, // council will resume deposits to module + const noDuplicatesKeys = [{ ...mockKey, operatorIndex: 0, used: true }]; + // Mine a new block + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - const keysWithoutDuplicates = [ - { ...mockKey, operatorIndex: mockOperator1.index, used: true }, - ]; - setupMockModules( - newBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - keysWithoutDuplicates, - ); + + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, noDuplicatesKeys, newMeta); sendDepositMessage.mockClear(); sendUnvetMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -684,31 +677,31 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendUnvetMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); test('adding not vetted duplicate will not set on soft pause module', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -716,50 +709,34 @@ describe('Deposits in case of duplicates', () => { }, }); - const keys = [ - { ...mockKey, operatorIndex: mockOperator1.index, used: false }, + const duplicates = [ + { ...mockKey, index: 0, operatorIndex: 1, used: false, vetted: true }, { ...mockKey, - index: mockKey.index + 1, - operatorIndex: mockOperator1.index, + index: 1, + operatorIndex: 1, used: false, + vetted: false, }, ]; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - [{ ...mockOperator1, stakingLimit: 1 }], - mockedDvtOperators, - keys, - ); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], - }, - }); - - await depositService.setCachedEvents({ - data: [], - headers: { - startBlock: currentBlock.number, - endBlock: currentBlock.number, + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); - const handleCorrectKeys = jest.spyOn( - stakingModuleGuardService, - 'handleCorrectKeys', - ); - - const getVettedUnusedKeys = jest.spyOn( - stakingRouterService, - 'getVettedUnusedKeys', - ); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -767,49 +744,30 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); - expect(getVettedUnusedKeys).toBeCalledTimes(4); - expect(getVettedUnusedKeys).toHaveBeenCalledWith( - expect.arrayContaining([keys[0]]), - expect.arrayContaining([keys[1]]), - ); - - //unresolved duplicates - expect(getVettedUnusedKeys).toHaveBeenCalledWith( - expect.arrayContaining([keys[0]]), - [], - ); - expect(getVettedUnusedKeys).toHaveBeenCalledWith([], []); - //unresolved duplicates - expect(getVettedUnusedKeys).toHaveBeenCalledWith([], []); - - expect(handleCorrectKeys).toBeCalledTimes(2); - expect(handleCorrectKeys).toHaveBeenCalledWith( - expect.objectContaining({ duplicatedKeys: [] }), - expect.anything(), - ); + expect(sendPauseMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); }); test( 'skip deposits if cannot resolve duplicates', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -817,28 +775,27 @@ describe('Deposits in case of duplicates', () => { }, }); - const unusedKeys = [ - mockKey, + const duplicates = [ + { ...mockKey, moduleAddress: NOP_REGISTRY }, { ...mockKey, moduleAddress: SIMPLE_DVT, }, ]; - const { sdvtModule, curatedModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [mockKeyEvent], headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, CSM, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -858,34 +815,26 @@ describe('Deposits in case of duplicates', () => { // just skip on this iteration deposit for Curated staking module expect(sendDepositMessage).toBeCalledTimes(0); - // check that duplicates problem didnt trigger pause expect(sendPauseMessage).toBeCalledTimes(0); expect(sendUnvetMessage).toBeCalledTimes(0); expect(unvetSigningKeys).toBeCalledTimes(0); - expect(sendDepositMessage).toBeCalledTimes(0); expect(sendPauseMessage).toBeCalledTimes(0); // after deleting duplicates in staking module, // council will resume deposits to module - const unusedKeysWithoutDuplicates = [ - mockKey, - { - ...mockKey2, - moduleAddress: SIMPLE_DVT, - }, - ]; + await providerService.provider.send('evm_mine', []); + const noDuplicatesKeys = [{ ...mockKey, moduleAddress: NOP_REGISTRY }]; const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeysWithoutDuplicates, - ); + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, noDuplicatesKeys, newMeta); sendDepositMessage.mockClear(); sendUnvetMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); @@ -895,24 +844,126 @@ describe('Deposits in case of duplicates', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendUnvetMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); - // TODO: test on unvetting of key of two modules + test( + 'if duplicates in both modules, skip deposits for modules and unvet only for first on first iteration', + async () => { + const currentBlock = await providerService.provider.getBlock('latest'); + // await providerService.provider.send('evm_mine', []); + + // Set deposit cache + await levelDBService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + }, + }); + + const earliestKey = { + ...mockKey, + operatorIndex: 0, + moduleAddress: NOP_REGISTRY, + index: 0, + }; + const duplicatesСurated = [ + earliestKey, + { ...mockKey, operatorIndex: 0, moduleAddress: NOP_REGISTRY, index: 1 }, + { ...mockKey, operatorIndex: 0, moduleAddress: NOP_REGISTRY, index: 2 }, + ]; + + const duplicatesSimpleDVT = [ + { + ...mockKey2, + operatorIndex: 0, + moduleAddress: SIMPLE_DVT, + index: 0, + }, + { + ...mockKey2, + operatorIndex: 0, + moduleAddress: SIMPLE_DVT, + index: 1, + }, + ]; + + // Mock Keys API + const vettedUnusedKeys = [...duplicatesСurated, ...duplicatesSimpleDVT]; + + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, vettedUnusedKeys, meta); + + // mock events cache to check + await signingKeysRegistryService.setCachedEvents({ + data: [], // dont need events in this test + headers: { + startBlock: currentBlock.number - 2, + endBlock: currentBlock.number, + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], + }, + }); + + // Check that module was not paused + const routerContract = StakingRouterAbi__factory.connect( + STAKING_ROUTER, + providerService.provider, + ); + const isOnPause = await routerContract.getStakingModuleIsDepositsPaused( + 1, + ); + expect(isOnPause).toBe(false); + await guardianService.handleNewBlock(); + + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + const walletAddress = getWalletAddress(); + // just skip on this iteration deposit for Curated staking module + expect(sendDepositMessage).toBeCalledTimes(1); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 2, + }), + ); + expect(sendPauseMessage).toBeCalledTimes(0); + expect(sendUnvetMessage).toBeCalledTimes(1); + expect(sendUnvetMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 1, + operatorIds: '0x0000000000000000', + vettedKeysByOperator: '0x00000000000000000000000000000001', + }), + ); + }, + TESTS_TIMEOUT, + ); }); diff --git a/test/duplicates.e2e-spec.ts b/test/duplicates.e2e-spec.ts index 6e87a17d..7ce4d974 100644 --- a/test/duplicates.e2e-spec.ts +++ b/test/duplicates.e2e-spec.ts @@ -1,8 +1,9 @@ import { - mockOperator1, - mockedDvtOperators, - mockedOperators, - setupMockModules, + mockMeta, + keysApiMockGetModules, + keysApiMockGetAllKeys, + mockedModuleCurated, + mockedModuleDvt, } from './helpers'; // Constants @@ -12,13 +13,10 @@ import { STAKING_ROUTER, CHAIN_ID, GANACHE_PORT, - sk, - pk, NOP_REGISTRY, SIMPLE_DVT, UNLOCKED_ACCOUNTS_V2, FORK_BLOCK_V2, - SANDBOX, SECURITY_MODULE_V2, SECURITY_MODULE_OWNER_V2, } from './constants'; @@ -36,21 +34,18 @@ import { closeServer, initLevelDB, } from './helpers/test-setup'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { makeDeposit, signDeposit } from './helpers/deposit'; -import { StakingModuleGuardService } from 'guardian/staking-module-guard'; +import { getWalletAddress } from './helpers/deposit'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; import { ProviderService } from 'provider'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { Server } from 'ganache'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; -import { StakingRouterService } from 'staking-router'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { addGuardians } from './helpers/dsm'; import { makeServer } from './server'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; import { BlsService } from 'bls'; import { mockKey, mockKey2, mockKeyEvent } from './helpers/keys-fixtures'; @@ -59,15 +54,12 @@ describe('ganache e2e tests', () => { let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let sendDepositMessage: jest.SpyInstance; let sendPauseMessage: jest.SpyInstance; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; - let stakingModuleGuardService: StakingModuleGuardService; + let signingKeysRegistryService: SigningKeysRegistryService; let guardianMessageService: GuardianMessageService; - let stakingRouterService: StakingRouterService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; const setupServer = async () => { @@ -96,16 +88,19 @@ describe('ganache e2e tests', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); }; const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -114,13 +109,12 @@ describe('ganache e2e tests', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); @@ -132,8 +126,6 @@ describe('ganache e2e tests', () => { // main service that check keys and make decision guardianService = moduleRef.get(GuardianService); - stakingModuleGuardService = moduleRef.get(StakingModuleGuardService); - stakingRouterService = moduleRef.get(StakingRouterService); }; beforeEach(async () => { @@ -152,10 +144,9 @@ describe('ganache e2e tests', () => { 'skip deposit if find duplicated key', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -163,36 +154,29 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); // Keys api mock - const unusedKeys = [ - mockKey, + const duplicates = [ + { ...mockKey, index: 0 }, { ...mockKey, index: 1 }, - { - ...mockKey, - index: 2, - }, - { - ...mockKey2, - moduleAddress: SIMPLE_DVT, - }, + { ...mockKey, index: 2 }, + { ...mockKey2, moduleAddress: SIMPLE_DVT }, ]; - setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); // Check that module was not paused const routerContract = StakingRouterAbi__factory.connect( @@ -205,6 +189,7 @@ describe('ganache e2e tests', () => { expect(isOnPause).toBe(false); await guardianService.handleNewBlock(); + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); // just skip on this iteration deposit for Curated staking module @@ -212,7 +197,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 2, }), @@ -222,40 +207,40 @@ describe('ganache e2e tests', () => { // after deleting duplicates in staking module, // council will resume deposits to module const unusedKeysWithoutDuplicates = [ - mockKey, - { - ...mockKey2, - moduleAddress: SIMPLE_DVT, - }, + { ...mockKey, index: 0, moduleAddress: NOP_REGISTRY }, + { ...mockKey2, index: 0, moduleAddress: SIMPLE_DVT }, ]; + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys( keysApiService, - mockedOperators, - mockedDvtOperators, unusedKeysWithoutDuplicates, + newMeta, ); + sendDepositMessage.mockClear(); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); expect(sendDepositMessage).toBeCalledTimes(2); - - expect(sendDepositMessage.mock.calls[0][0]).toEqual( + expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 1, }), ); - expect(sendDepositMessage.mock.calls[1][0]).toEqual( + expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 2, }), @@ -268,10 +253,9 @@ describe('ganache e2e tests', () => { 'skip deposit if find duplicated key in another staking module', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -280,23 +264,19 @@ describe('ganache e2e tests', () => { }); // Keys api mock - const unusedKeys = [ - mockKey, - { - ...mockKey, - moduleAddress: SIMPLE_DVT, - }, + const duplicates = [ + { ...mockKey, index: 0, moduleAddress: NOP_REGISTRY }, + { ...mockKey, index: 0, moduleAddress: SIMPLE_DVT }, ]; - const { curatedModule } = setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - unusedKeys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [ mockKeyEvent, { @@ -308,7 +288,7 @@ describe('ganache e2e tests', () => { headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -329,30 +309,32 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendPauseMessage).toBeCalledTimes(0); + await providerService.provider.send('evm_mine', []); // after deleting duplicates in staking module, // council will resume deposits to module const unusedKeysWithoutDuplicates = [ - mockKey, + { ...mockKey, index: 0, moduleAddress: NOP_REGISTRY }, { ...mockKey2, moduleAddress: SIMPLE_DVT, }, ]; - const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys( keysApiService, - mockedOperators, - mockedDvtOperators, unusedKeysWithoutDuplicates, + newMeta, ); sendDepositMessage.mockClear(); @@ -364,7 +346,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 1, }), @@ -373,7 +355,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 2, }), @@ -386,10 +368,9 @@ describe('ganache e2e tests', () => { 'added unused keys for that deposit was already made', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -398,7 +379,7 @@ describe('ganache e2e tests', () => { }); // Keys api mock - const keys = [ + const duplicates = [ { ...mockKey, used: true }, { ...mockKey, @@ -407,20 +388,19 @@ describe('ganache e2e tests', () => { }, ]; - setupMockModules( - currentBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - keys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -442,18 +422,14 @@ describe('ganache e2e tests', () => { // after deleting duplicates in staking module, // council will resume deposits to module - + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - const keysWithoutDulicates = [{ ...mockKey, used: true }]; - - setupMockModules( - newBlock, - keysApiService, - mockedOperators, - mockedDvtOperators, - keysWithoutDulicates, - ); + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keysWithoutDulicates, newMeta); sendDepositMessage.mockClear(); @@ -464,7 +440,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 1, }), @@ -472,7 +448,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, stakingModuleId: 2, }), @@ -483,10 +459,9 @@ describe('ganache e2e tests', () => { test('adding not vetted duplicate will not set on soft pause module', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - const { depositData } = signDeposit(pk, sk); - await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -495,61 +470,52 @@ describe('ganache e2e tests', () => { }); // Keys api mock - const keys = [ - { ...mockKey, index: 0, operatorIndex: mockOperator1.index, used: false }, + const duplicates = [ + { ...mockKey, index: 0, operatorIndex: 0, used: false, vetted: true }, { ...mockKey, index: 1, - operatorIndex: mockOperator1.index, + operatorIndex: 0, used: false, + vetted: false, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [{ ...mockOperator1, stakingLimit: 1 }], - mockedDvtOperators, - keys, - ); + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, duplicates, meta); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number - 2, endBlock: currentBlock.number - 1, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); - const handleCorrectKeys = jest.spyOn( - stakingModuleGuardService, - 'handleCorrectKeys', - ); - - const getVettedUnusedKeys = jest.spyOn( - stakingRouterService, - 'getVettedUnusedKeys', - ); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); expect(sendDepositMessage).toBeCalledTimes(2); - expect(handleCorrectKeys).toBeCalledTimes(2); - expect(getVettedUnusedKeys).toBeCalledTimes(4); - - expect(getVettedUnusedKeys).toHaveBeenCalledWith( - expect.arrayContaining([keys[0]]), - expect.arrayContaining([keys[1]]), - ); - //unresolved duplicates - expect(getVettedUnusedKeys).toHaveBeenCalledWith( - expect.arrayContaining([keys[0]]), - expect.arrayContaining([]), + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 1, + }), ); - expect(handleCorrectKeys).toHaveBeenCalledWith( - expect.objectContaining({ duplicatedKeys: [] }), - expect.anything(), + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 2, + }), ); }); }); diff --git a/test/front-run-v3.e2e-spec.ts b/test/front-run-v3.e2e-spec.ts index 1753eada..660857ef 100644 --- a/test/front-run-v3.e2e-spec.ts +++ b/test/front-run-v3.e2e-spec.ts @@ -3,16 +3,12 @@ import { toHexString } from '@chainsafe/ssz'; // Helpers import { - mockedDvtOperators, + keysApiMockGetAllKeys, + keysApiMockGetModules, mockedKeysApiFind, - mockedKeysApiGetAllKeys, - mockedKeysApiOperatorsMany, - mockedMeta, - mockedModule, - mockedOperators, - mockOperator1, - mockOperator2, - setupMockModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, } from './helpers'; // Constants @@ -28,10 +24,9 @@ import { pk, NOP_REGISTRY, SIMPLE_DVT, - SANDBOX, UNLOCKED_ACCOUNTS, - CSM, SECURITY_MODULE, + NO_PRIVKEY_MESSAGE, } from './constants'; // Contract Factories @@ -46,21 +41,21 @@ import { initLevelDB, } from './helpers/test-setup'; import { SecurityService } from 'contracts/security'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { ProviderService } from 'provider'; import { Server } from 'ganache'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; import { makeServer } from './server'; import { addGuardians } from './helpers/dsm'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; import { BlsService } from 'bls'; import { makeDeposit, signDeposit } from './helpers/deposit'; import { mockKey, mockKey2 } from './helpers/keys-fixtures'; +import { ethers } from 'ethers'; // Mock rabbit straight away jest.mock('../src/transport/stomp/stomp.client.ts'); @@ -72,12 +67,11 @@ describe('ganache e2e tests', () => { let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let securityService: SecurityService; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; let guardianMessageService: GuardianMessageService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; // method mocks @@ -97,7 +91,7 @@ describe('ganache e2e tests', () => { const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -106,13 +100,12 @@ describe('ganache e2e tests', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); @@ -146,18 +139,21 @@ describe('ganache e2e tests', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); // mock unvetting method of contract // as we dont use real keys api and work with fixtures of operators and keys // we cant make real unvetting unvetSigningKeys = jest .spyOn(securityService, 'unvetSigningKeys') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(null as any)); }; beforeEach(async () => { @@ -185,18 +181,23 @@ describe('ganache e2e tests', () => { { key: toHexString(pk), depositSignature: toHexString(signature), - operatorIndex: mockOperator1.index, - used: false, // TODO: true + operatorIndex: 0, + used: false, index: 1, moduleAddress: NOP_REGISTRY, + vetted: true, + }, + { + ...mockKey2, + index: 0, + moduleAddress: SIMPLE_DVT, + operatorIndex: 0, + vetted: true, }, - // simple dvt - mockKey2, ]; // add in deposit cache event of deposit on key with lido creds - // TODO: replace with real deposit - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -205,12 +206,12 @@ describe('ganache e2e tests', () => { }); // dont set events for keys as we check this cache only in case of duplicated keys - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -221,13 +222,13 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const { curatedModule } = setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -241,7 +242,7 @@ describe('ganache e2e tests', () => { blockNumber: newBlock.number, guardianAddress: wallet.address, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000001', }), @@ -265,7 +266,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -273,12 +274,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -297,26 +298,30 @@ describe('ganache e2e tests', () => { { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, - used: false, // TODO: true + operatorIndex: 0, + used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, + }, + { + ...mockKey2, + index: 0, + moduleAddress: SIMPLE_DVT, + operatorIndex: 0, + vetted: true, }, - // simple dvt - mockKey2, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); - - // we make check that there are no duplicated used keys - // this request return keys along with their duplicates - // mockedKeysApiFind(keysApiService, unusedKeys, newMeta); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -332,6 +337,8 @@ describe('ganache e2e tests', () => { expect(isOnPause).toBe(false); expect(sendPauseMessage).toBeCalledTimes(0); expect(sendDepositMessage).toBeCalledTimes(2); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -341,7 +348,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -349,12 +356,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -369,30 +376,35 @@ describe('ganache e2e tests', () => { 1, ); - const unusedKeys = [ + const keys = [ { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, + operatorIndex: 0, used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - unusedKeys, - ); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); expect(sendPauseMessage).toBeCalledTimes(0); expect(sendDepositMessage).toBeCalledTimes(2); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -402,7 +414,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -410,12 +422,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -428,24 +440,27 @@ describe('ganache e2e tests', () => { const { wallet } = await makeDeposit(depositData, providerService); - const unusedKeys = [ + const keys = [ { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, + operatorIndex: 0, used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - unusedKeys, - ); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Check if the service is ok and ready to go await guardianService.handleNewBlock(); @@ -454,7 +469,7 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledTimes(2); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ - blockNumber: currentBlock.number, + blockNumber: newBlock.number, guardianAddress: wallet.address, guardianIndex: 7, stakingModuleId: 1, @@ -462,12 +477,14 @@ describe('ganache e2e tests', () => { ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ - blockNumber: currentBlock.number, + blockNumber: newBlock.number, guardianAddress: wallet.address, guardianIndex: 7, stakingModuleId: 2, }), ); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -476,7 +493,7 @@ describe('ganache e2e tests', () => { 'inconsistent kapi requests data', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -484,27 +501,25 @@ describe('ganache e2e tests', () => { }, }); - // mocked curated module - const stakingModule = mockedModule(currentBlock, currentBlock.hash); - const meta = mockedMeta(currentBlock, currentBlock.hash); - - mockedKeysApiOperatorsMany( - keysApiService, - [{ operators: mockedOperators, module: stakingModule }], - meta, - ); - - const unusedKeys = [mockKey]; - - const hashWasChanged = - '0xd921055dbb407e09f64afe5182a64c1bd309fe28f26909a96425cdb6bfc48959'; - const newMeta = mockedMeta(currentBlock, hashWasChanged); - mockedKeysApiGetAllKeys(keysApiService, unusedKeys, newMeta); + // Mock Keys API + const keys = [mockKey]; + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + await providerService.provider.send('evm_mine', []); + const newBlock = await providerService.provider.getBlock('latest'); + const newMeta = mockMeta(newBlock, newBlock.hash); + keysApiMockGetAllKeys(keysApiService, keys, newMeta); await guardianService.handleNewBlock(); expect(sendDepositMessage).toBeCalledTimes(0); expect(sendPauseMessage).toBeCalledTimes(0); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); @@ -514,52 +529,129 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ - data: [], + const { signature: lidoSign } = signDeposit(pk, sk); + const { signature: theftDepositSign } = signDeposit(pk, sk, BAD_WC); + + const keys = [ + { + key: toHexString(pk), + depositSignature: toHexString(lidoSign), + operatorIndex: 0, + used: true, + index: 0, + moduleAddress: NOP_REGISTRY, + vetted: true, + }, + ]; + + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); + mockedKeysApiFind(keysApiService, keys, meta); + + await levelDBService.setCachedEvents({ + data: [ + { + valid: true, + pubkey: toHexString(pk), + amount: '32000000000', + wc: BAD_WC, + signature: toHexString(theftDepositSign), + tx: '0x122', + blockHash: '0x123456', + blockNumber: currentBlock.number - 1, + logIndex: 1, + depositCount: 1, + depositDataRoot: new Uint8Array(), + index: '', + }, + { + valid: true, + pubkey: toHexString(pk), + amount: '32000000000', + wc: LIDO_WC, + signature: toHexString(lidoSign), + tx: '0x123', + blockHash: currentBlock.hash, + blockNumber: currentBlock.number, + logIndex: 1, + depositCount: 2, + depositDataRoot: new Uint8Array(), + index: '', + }, + ], headers: { - startBlock: currentBlock.number, + startBlock: currentBlock.number - 2, endBlock: currentBlock.number, }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await guardianService.handleNewBlock(); + + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + expect(sendPauseMessage).toBeCalledTimes(1); + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(unvetSigningKeys).toBeCalledTimes(0); + + const securityContract = SecurityAbi__factory.connect( + SECURITY_MODULE, + providerService.provider, + ); + + const isOnPause = await securityContract.isDepositsPaused(); + expect(isOnPause).toBe(true); + }, + TESTS_TIMEOUT, + ); + + test( + 'should not trigger pause for front-run attempt with non-Lido WC and Lido WC deposits when key is unused', + async () => { + const currentBlock = await providerService.provider.getBlock('latest'); + + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); const { signature: lidoSign } = signDeposit(pk, sk); const { signature: theftDepositSign } = signDeposit(pk, sk, BAD_WC); + if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); + const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); + const keys = [ { key: toHexString(pk), depositSignature: toHexString(lidoSign), operatorIndex: 0, - used: true, + used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); - - mockedKeysApiFind( - keysApiService, - keys, - mockedMeta(currentBlock, currentBlock.hash), - ); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); + mockedKeysApiFind(keysApiService, keys, meta); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [ { valid: true, @@ -591,7 +683,7 @@ describe('ganache e2e tests', () => { }, ], headers: { - startBlock: currentBlock.number, + startBlock: currentBlock.number - 2, endBlock: currentBlock.number, }, }); @@ -600,7 +692,7 @@ describe('ganache e2e tests', () => { await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); - expect(sendPauseMessage).toBeCalledTimes(1); + expect(sendPauseMessage).toBeCalledTimes(0); const securityContract = SecurityAbi__factory.connect( SECURITY_MODULE, @@ -609,7 +701,115 @@ describe('ganache e2e tests', () => { const isOnPause = await securityContract.isDepositsPaused(); - expect(isOnPause).toBe(true); + expect(isOnPause).toBe(false); + + expect(sendPauseMessage).toBeCalledTimes(0); + expect(sendUnvetMessage).toBeCalledTimes(1); + expect(sendUnvetMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 1, + operatorIds: '0x0000000000000000', + vettedKeysByOperator: '0x00000000000000000000000000000000', + }), + ); + expect(unvetSigningKeys).toBeCalledTimes(1); + expect(sendDepositMessage).toBeCalledTimes(1); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 2, + }), + ); + }, + TESTS_TIMEOUT, + ); + + test( + 'frontrun of unvetted key will not set module on soft pause', + async () => { + const currentBlock = await providerService.provider.getBlock('latest'); + + await levelDBService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + }, + }); + + await signingKeysRegistryService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], + }, + }); + + const { signature: goodSign } = signDeposit(pk, sk, LIDO_WC, 32000000000); + + const { depositData: theftDepositData } = signDeposit(pk, sk, BAD_WC); + const { wallet } = await makeDeposit(theftDepositData, providerService); + + const unvettedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSign), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + vetted: false, + }, + ]; + + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, unvettedKeys, meta); + + // Check if the service is ok and ready to go + // the same scenario as "failed 1eth deposit attack to stop deposits" + await guardianService.handleNewBlock(); + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + expect(sendDepositMessage).toBeCalledTimes(2); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 1, + }), + ); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 2, + }), + ); + expect(unvetSigningKeys).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); + + const securityContract = SecurityAbi__factory.connect( + SECURITY_MODULE, + providerService.provider, + ); + + const isOnPause = await securityContract.isDepositsPaused(); + expect(isOnPause).toBe(false); }, TESTS_TIMEOUT, ); diff --git a/test/front-run.e2e-spec.ts b/test/front-run.e2e-spec.ts index 5d21aece..b929fc27 100644 --- a/test/front-run.e2e-spec.ts +++ b/test/front-run.e2e-spec.ts @@ -3,16 +3,12 @@ import { toHexString } from '@chainsafe/ssz'; // Helpers import { - mockedDvtOperators, mockedKeysApiFind, - mockedKeysApiGetAllKeys, - mockedKeysApiOperatorsMany, - mockedMeta, - mockedModule, - mockedOperators, - mockOperator1, - mockOperator2, - setupMockModules, + keysApiMockGetAllKeys, + keysApiMockGetModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, } from './helpers'; // Constants @@ -32,7 +28,6 @@ import { UNLOCKED_ACCOUNTS_V2, FORK_BLOCK_V2, SECURITY_MODULE_OWNER_V2, - SANDBOX, } from './constants'; // Contract Factories @@ -46,18 +41,17 @@ import { closeServer, initLevelDB, } from './helpers/test-setup'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { ProviderService } from 'provider'; import { Server } from 'ganache'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; import { makeServer } from './server'; import { addGuardians } from './helpers/dsm'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; import { BlsService } from 'bls'; import { makeDeposit, signDeposit } from './helpers/deposit'; import { mockKey, mockKey2 } from './helpers/keys-fixtures'; @@ -72,13 +66,12 @@ describe('ganache e2e tests', () => { let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let sendDepositMessage: jest.SpyInstance; let sendPauseMessage: jest.SpyInstance; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; let guardianMessageService: GuardianMessageService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; const setupServer = async () => { @@ -95,7 +88,7 @@ describe('ganache e2e tests', () => { const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -104,13 +97,12 @@ describe('ganache e2e tests', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); // keys api servies @@ -137,11 +129,14 @@ describe('ganache e2e tests', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); }; beforeEach(async () => { @@ -164,23 +159,27 @@ describe('ganache e2e tests', () => { const { signature } = signDeposit(pk, sk, LIDO_WC); // Keys api mock - // all keys in keys api on current block state const keys = [ { key: toHexString(pk), depositSignature: toHexString(signature), - operatorIndex: mockOperator1.index, + operatorIndex: 0, used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, + }, + { + ...mockKey2, + index: 0, + moduleAddress: SIMPLE_DVT, + operatorIndex: 0, + vetted: true, }, - // simple dvt - mockKey2, ]; // add in deposit cache event of deposit on key with lido creds - // TODO: replace with real deposit - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -188,13 +187,13 @@ describe('ganache e2e tests', () => { }, }); - // dont set events for keys as we check this cahce only in case of duplicated keys - await signingKeyEventsCacheService.setCachedEvents({ + // dont set events for keys as we check this cache only in case of duplicated keys + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -204,14 +203,13 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -237,7 +235,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -245,12 +243,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -269,26 +267,30 @@ describe('ganache e2e tests', () => { { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, - used: false, // TODO: true + operatorIndex: 0, + used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, + }, + { + ...mockKey2, + index: 0, + moduleAddress: SIMPLE_DVT, + operatorIndex: 0, + vetted: true, }, - // simple dvt - mockKey2, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); - - // we make check that there are no duplicated used keys - // this request return keys along with their duplicates - // mockedKeysApiFind(keysApiService, unusedKeys, newMeta); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -314,7 +316,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -322,12 +324,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -342,24 +344,27 @@ describe('ganache e2e tests', () => { 1, ); - const unusedKeys = [ + const keys = [ { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, + operatorIndex: 0, used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - unusedKeys, - ); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -375,7 +380,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -383,12 +388,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -401,33 +406,37 @@ describe('ganache e2e tests', () => { const { wallet } = await makeDeposit(depositData, providerService); - const unusedKeys = [ + const keys = [ { key: toHexString(pk), depositSignature: toHexString(goodSign), - operatorIndex: mockOperator1.index, - used: false, + operatorIndex: 0, + used: true, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - unusedKeys, - ); + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); // Check if the service is ok and ready to go + // the same scenario as "failed 1eth deposit attack to stop deposits" await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); expect(sendDepositMessage).toBeCalledTimes(2); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ - blockNumber: currentBlock.number, + blockNumber: newBlock.number, guardianAddress: wallet.address, guardianIndex: 7, stakingModuleId: 1, @@ -435,7 +444,8 @@ describe('ganache e2e tests', () => { ); expect(sendDepositMessage).toHaveBeenCalledWith( expect.objectContaining({ - blockNumber: currentBlock.number, + blockNumber: newBlock.number, + guardianAddress: wallet.address, guardianIndex: 7, stakingModuleId: 2, @@ -451,6 +461,10 @@ describe('ganache e2e tests', () => { 1, ); expect(isOnPause).toBe(false); + const isOnPause2 = await routerContract.getStakingModuleIsDepositsPaused( + 2, + ); + expect(isOnPause2).toBe(false); }, TESTS_TIMEOUT, ); @@ -459,7 +473,7 @@ describe('ganache e2e tests', () => { 'inconsistent kapi requests data', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -467,22 +481,19 @@ describe('ganache e2e tests', () => { }, }); - // mocked curated module - const stakingModule = mockedModule(currentBlock, currentBlock.hash); - const meta = mockedMeta(currentBlock, currentBlock.hash); - - mockedKeysApiOperatorsMany( - keysApiService, - [{ operators: mockedOperators, module: stakingModule }], - meta, - ); - - const unusedKeys = [mockKey]; + const keys = [mockKey]; - const hashWasChanged = - '0xd921055dbb407e09f64afe5182a64c1bd309fe28f26909a96425cdb6bfc48959'; - const newMeta = mockedMeta(currentBlock, hashWasChanged); - mockedKeysApiGetAllKeys(keysApiService, unusedKeys, newMeta); + // Mock Keys API + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + await providerService.provider.send('evm_mine', []); + const newBlock = await providerService.provider.getBlock('latest'); + const newMeta = mockMeta(newBlock, newBlock.hash); + keysApiMockGetAllKeys(keysApiService, keys, newMeta); await guardianService.handleNewBlock(); @@ -493,11 +504,11 @@ describe('ganache e2e tests', () => { ); test( - 'historical front-run', + 'frontrun of unvetted key will not set module on soft pause', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -505,12 +516,90 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], + }, + }); + + const { signature: goodSign } = signDeposit(pk, sk, LIDO_WC, 32000000000); + + const { depositData: theftDepositData } = signDeposit(pk, sk, BAD_WC); + const { wallet } = await makeDeposit(theftDepositData, providerService); + + const unvettedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSign), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + vetted: false, + }, + ]; + + // Mock Keys API again on new block + const newBlock = await providerService.provider.getBlock('latest'); + // setup elBlockSnapshot + const meta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, unvettedKeys, meta); + + // Check if the service is ok and ready to go + // the same scenario as "failed 1eth deposit attack to stop deposits" + await guardianService.handleNewBlock(); + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + expect(sendDepositMessage).toBeCalledTimes(2); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 1, + }), + ); + expect(sendDepositMessage).toHaveBeenCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 7, + stakingModuleId: 2, + }), + ); + expect(sendPauseMessage).toBeCalledTimes(0); + + // Check if on pause now + const routerContract = StakingRouterAbi__factory.connect( + STAKING_ROUTER, + providerService.provider, + ); + const isOnPause = await routerContract.getStakingModuleIsDepositsPaused( + 1, + ); + expect(isOnPause).toBe(false); + }, + TESTS_TIMEOUT, + ); + + test( + 'historical front-run', + async () => { + const currentBlock = await providerService.provider.getBlock('latest'); + + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -525,24 +614,11 @@ describe('ganache e2e tests', () => { used: true, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }, ]; - setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); - - mockedKeysApiFind( - keysApiService, - keys, - mockedMeta(currentBlock, currentBlock.hash), - ); - - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [ { valid: true, @@ -574,11 +650,20 @@ describe('ganache e2e tests', () => { }, ], headers: { - startBlock: currentBlock.number, + startBlock: currentBlock.number - 2, endBlock: currentBlock.number, }, }); + // setup elBlockSnapshot + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); + mockedKeysApiFind(keysApiService, keys, meta); + await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -593,31 +678,27 @@ describe('ganache e2e tests', () => { expect(isOnPause).toBe(true); - await routerContract.getStakingModuleIsDepositsPaused(2); + const isOnPause2Module = + await routerContract.getStakingModuleIsDepositsPaused(2); + + expect(isOnPause2Module).toBe(false); + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(1); // Mine a new block await providerService.provider.send('evm_mine', []); - // Your assertions after mining the block + // // Your assertions after mining the block const newBlock = await providerService.provider.getBlock('latest'); - console.log('Current block number:', { - newBlock: newBlock.number, - currentBlock: currentBlock.number, - }); - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - keys, - ); + // setup elBlockSnapshot + const newMeta = mockMeta(newBlock, newBlock.hash); + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, newMeta); + mockedKeysApiFind(keysApiService, keys, newMeta); - mockedKeysApiFind( - keysApiService, - keys, - mockedMeta(newBlock, newBlock.hash), - ); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); @@ -632,6 +713,9 @@ describe('ganache e2e tests', () => { await routerContract.getStakingModuleIsDepositsPaused(2); expect(isOnPause2NextIter).toBe(true); + + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(1); }, TESTS_TIMEOUT, ); diff --git a/test/guardian-balance-monitoring.e2e-spec.ts b/test/guardian-balance-monitoring.e2e-spec.ts index 8f8f05a5..75b9d47e 100644 --- a/test/guardian-balance-monitoring.e2e-spec.ts +++ b/test/guardian-balance-monitoring.e2e-spec.ts @@ -7,10 +7,11 @@ import { Server } from 'ganache'; // Helper Functions and Mocks import { - mockedDvtOperators, - mockOperator1, - mockOperator2, - setupMockModules, + keysApiMockGetAllKeys, + keysApiMockGetModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, } from './helpers'; import { @@ -28,39 +29,37 @@ import { GANACHE_PORT, NOP_REGISTRY, SIMPLE_DVT, - SANDBOX, UNLOCKED_ACCOUNTS, FORK_BLOCK, - CSM, } from './constants'; // Contract and Service Imports import { SecurityService } from 'contracts/security'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { ProviderService } from 'provider'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; import { BlsService } from 'bls'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; // Test Data import { mockKey, mockKey2 } from './helpers/keys-fixtures'; import { addGuardians, setGuardianBalance } from './helpers/dsm'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { ethers } from 'ethers'; describe('Guardian balance monitoring test', () => { let server: Server<'ethereum'>; let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; let guardianMessageService: GuardianMessageService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; let securityService: SecurityService; @@ -123,7 +122,7 @@ describe('Guardian balance monitoring test', () => { } const setupDefaultCache = async (blockNumber) => { - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: blockNumber, @@ -131,37 +130,44 @@ describe('Guardian balance monitoring test', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: blockNumber, endBlock: blockNumber, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); }; - const setupKAPIWithInvalidSignProblem = (block) => { - const norKeyWithWrongSign = { + const setupKAPIWithInvalidSignProblem = (block: ethers.providers.Block) => { + // keys fixtures + const norKeyWithWrongSign: RegistryKey = { ...mockKey, depositSignature: '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + vetted: true, }; - - const dvtKey = { + const dvtKey: RegistryKey = { ...mockKey2, + index: 1, used: false, - operatorIndex: mockedDvtOperators[0].index, + operatorIndex: 0, moduleAddress: SIMPLE_DVT, + vetted: true, }; + const dvtKey2 = { ...dvtKey, index: 2 }; - setupMockModules( - block, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [norKeyWithWrongSign, dvtKey, { ...dvtKey, index: dvtKey.index + 1 }], - ); + // setup elBlockSnapshot + const meta = mockMeta(block, block.hash); + + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + + // setup /v1/keys + const keys = [norKeyWithWrongSign, dvtKey, dvtKey2]; + keysApiMockGetAllKeys(keysApiService, keys, meta); }; async function waitForProcessing() { @@ -188,7 +194,7 @@ describe('Guardian balance monitoring test', () => { }; const initializeLevelDBServices = async (moduleRef) => { - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); }; @@ -197,11 +203,10 @@ describe('Guardian balance monitoring test', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); }; const initializeKeyEventServices = (moduleRef) => { - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); }; const initializeProviders = (moduleRef) => { @@ -243,16 +248,19 @@ describe('Guardian balance monitoring test', () => { const mockDepositCacheMethods = () => { jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); }; const mockUnvettingMethod = () => { unvetSigningKeys = jest .spyOn(securityService, 'unvetSigningKeys') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(null as any)); }; }); diff --git a/test/helpers/deposit.ts b/test/helpers/deposit.ts index c601d518..896ddee4 100644 --- a/test/helpers/deposit.ts +++ b/test/helpers/deposit.ts @@ -53,3 +53,9 @@ export async function makeDeposit( return { wallet: signer, depositSign: depositData.signature }; } + +export function getWalletAddress() { + if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); + const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); + return wallet.address; +} diff --git a/test/helpers/keys-fixtures.ts b/test/helpers/keys-fixtures.ts index 25075a70..cbaf0754 100644 --- a/test/helpers/keys-fixtures.ts +++ b/test/helpers/keys-fixtures.ts @@ -8,6 +8,7 @@ export const mockKey = { used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }; export const mockKeyEvent = { @@ -27,4 +28,5 @@ export const mockKey2 = { used: true, moduleAddress: NOP_REGISTRY, index: 1, + vetted: true, }; diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index b4378432..7d2c0ec8 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -2,42 +2,11 @@ import ethers from 'ethers'; import { KeysApiService } from '../../src/keys-api/keys-api.service'; import { SIMPLE_DVT, NOP_REGISTRY } from './../constants'; -import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; import { SRModule } from 'keys-api/interfaces'; import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -export const setupMockModules = ( - currentBlock: ethers.providers.Block, - keysApiService: KeysApiService, - mockedOperators: RegistryOperator[], - mockedDvtOperators: RegistryOperator[], - unusedKeys: RegistryKey[], -) => { - const curatedModule = mockedModule(currentBlock, currentBlock.hash); - const sdvtModule = mockedModuleDvt(currentBlock, currentBlock.hash); - const meta = mockedMeta(currentBlock, currentBlock.hash); - - mockedKeysApiOperatorsMany( - keysApiService, - [ - { operators: mockedOperators, module: curatedModule }, - { operators: mockedDvtOperators, module: sdvtModule }, - ], - meta, - ); - - mockedKeysApiGetAllKeys(keysApiService, unusedKeys, meta); - - return { curatedModule, sdvtModule, meta }; -}; - -export const mockedModule = ( - block: ethers.providers.Block, - lastChangedBlockHash: string, - nonce = 6046, -): SRModule => ({ - nonce, +export const mockedModuleCurated: SRModule = { type: 'curated-onchain-v1', id: 1, stakingModuleAddress: NOP_REGISTRY, @@ -46,19 +15,15 @@ export const mockedModule = ( targetShare: 10, status: 1, name: 'NodeOperatorRegistry', - lastDepositAt: block.timestamp, - lastDepositBlock: block.number, - lastChangedBlockHash, + lastDepositAt: 1234345657, + lastDepositBlock: 12345, + lastChangedBlockHash: '', + nonce: 6046, exitedValidatorsCount: 0, active: true, -}); +}; -export const mockedModuleDvt = ( - block: ethers.providers.Block, - lastChangedBlockHash: string, - nonce = 6046, -): SRModule => ({ - nonce, +export const mockedModuleDvt: SRModule = { type: 'curated-onchain-v1', id: 2, stakingModuleAddress: SIMPLE_DVT, @@ -67,14 +32,15 @@ export const mockedModuleDvt = ( targetShare: 10, status: 1, name: 'NodeOperatorRegistrySimpleDvt', - lastDepositAt: block.timestamp, - lastDepositBlock: block.number, - lastChangedBlockHash, + lastDepositAt: 1234345657, + lastDepositBlock: 12345, + lastChangedBlockHash: '', + nonce: 6046, exitedValidatorsCount: 0, active: true, -}); +}; -export const mockedMeta = ( +export const mockMeta = ( block: ethers.providers.Block, lastChangedBlockHash: string, ) => ({ @@ -84,88 +50,41 @@ export const mockedMeta = ( lastChangedBlockHash, }); -export const mockOperator1 = { - name: 'Dev team', - rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', - stakingLimit: 12, - stoppedValidators: 0, - totalSigningKeys: 12, - usedSigningKeys: 0, - index: 0, - active: true, - moduleAddress: NOP_REGISTRY, -}; - -export const mockOperator2 = { - name: 'Dev team', - rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', - stakingLimit: 12, - stoppedValidators: 0, - totalSigningKeys: 12, - usedSigningKeys: 0, - index: 1, - active: true, - moduleAddress: NOP_REGISTRY, -}; - -export const mockedOperators: RegistryOperator[] = [ - mockOperator1, - mockOperator2, -]; - -export const mockedDvtOperators: RegistryOperator[] = [ - { - name: 'Dev DVT team', - rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', - stakingLimit: 12, - stoppedValidators: 0, - totalSigningKeys: 12, - usedSigningKeys: 0, - index: 0, - active: true, - moduleAddress: SIMPLE_DVT, - }, -]; - -export const mockedKeysApiOperatorsMany = ( +export const keysApiMockGetModules = ( keysApiService: KeysApiService, - data: { operators: RegistryOperator[]; module: SRModule }[], - mockedMeta: ELBlockSnapshot, + modules: SRModule[], + meta: ELBlockSnapshot, ) => { - jest - .spyOn(keysApiService, 'getOperatorListWithModule') - .mockImplementation(async () => ({ - data: data, - meta: { - elBlockSnapshot: mockedMeta, - }, - })); + jest.spyOn(keysApiService, 'getModules').mockImplementation(async () => ({ + data: modules, + elBlockSnapshot: meta, + })); }; -export const mockedKeysApiGetAllKeys = ( +export const keysApiMockGetAllKeys = ( keysApiService: KeysApiService, - mockedKeys: RegistryKey[], - mockedMeta: ELBlockSnapshot, + keys: RegistryKey[], + meta: ELBlockSnapshot, ) => { jest.spyOn(keysApiService, 'getKeys').mockImplementation(async () => ({ - data: mockedKeys, + data: keys, meta: { - elBlockSnapshot: mockedMeta, + elBlockSnapshot: meta, }, })); }; export const mockedKeysApiFind = ( keysApiService: KeysApiService, - mockedKeys: RegistryKey[], - mockedMeta: ELBlockSnapshot, + keys: RegistryKey[], + meta: ELBlockSnapshot, ) => { jest .spyOn(keysApiService, 'getKeysByPubkeys') .mockImplementation(async () => ({ - data: mockedKeys, + data: keys, meta: { - elBlockSnapshot: mockedMeta, + elBlockSnapshot: meta, }, })); }; diff --git a/test/helpers/test-setup.ts b/test/helpers/test-setup.ts index d2d37a08..3dee69ba 100644 --- a/test/helpers/test-setup.ts +++ b/test/helpers/test-setup.ts @@ -2,16 +2,16 @@ import { Test } from '@nestjs/testing'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; import { PrometheusModule } from 'common/prometheus'; -import { DepositModule } from 'contracts/deposit'; -import { LidoModule } from 'contracts/lido'; +import { DepositsRegistryModule } from 'contracts/deposits-registry'; import { RepositoryModule } from 'contracts/repository'; import { SecurityModule } from 'contracts/security'; import { GuardianModule } from 'guardian'; import { KeysApiModule } from 'keys-api/keys-api.module'; import { GanacheProviderModule } from 'provider'; import { WalletModule } from 'wallet'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; export const setupTestingModule = async () => { const moduleRef = await Test.createTestingModule({ @@ -24,17 +24,23 @@ export const setupTestingModule = async () => { RepositoryModule, WalletModule, KeysApiModule, - LidoModule, - DepositModule, + DepositsRegistryModule.register('latest'), SecurityModule, ], }).compile(); + const loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); + + jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'debug').mockImplementation(() => undefined); + jest.spyOn(loggerService, 'error').mockImplementation(() => undefined); + return moduleRef; }; export const initLevelDB = async ( - levelDBService: LevelDBService, + levelDBService: DepositsRegistryStoreService, signKeyLevelDBService: SignKeyLevelDBService, ) => { await levelDBService.initialize(); @@ -43,7 +49,7 @@ export const initLevelDB = async ( export const closeServer = async ( server, - levelDBService: LevelDBService, + levelDBService: DepositsRegistryStoreService, signKeyLevelDBService: SignKeyLevelDBService, ) => { await server.close(); @@ -54,7 +60,7 @@ export const closeServer = async ( }; export const closeLevelDB = async ( - levelDBService: LevelDBService, + levelDBService: DepositsRegistryStoreService, signKeyLevelDBService: SignKeyLevelDBService, ) => { await levelDBService.deleteCache(); diff --git a/test/invalid-keys-v3.e2e-spec.ts b/test/invalid-keys-v3.e2e-spec.ts index fdbd19e4..37ae4251 100644 --- a/test/invalid-keys-v3.e2e-spec.ts +++ b/test/invalid-keys-v3.e2e-spec.ts @@ -3,10 +3,11 @@ import { toHexString } from '@chainsafe/ssz'; // Helpers import { - mockedDvtOperators, - mockOperator1, - mockOperator2, - setupMockModules, + keysApiMockGetAllKeys, + keysApiMockGetModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, } from './helpers'; // Constants @@ -19,11 +20,9 @@ import { pk, NOP_REGISTRY, SIMPLE_DVT, - SANDBOX, LIDO_WC, UNLOCKED_ACCOUNTS, FORK_BLOCK, - CSM, } from './constants'; // Mock rabbit straight away @@ -37,20 +36,20 @@ import { initLevelDB, } from './helpers/test-setup'; import { SecurityService } from 'contracts/security'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { ProviderService } from 'provider'; import { Server } from 'ganache'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { KeyValidatorInterface } from '@lido-nestjs/key-validation'; -import { makeDeposit, signDeposit } from './helpers/deposit'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; + +import { getWalletAddress, signDeposit } from './helpers/deposit'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; import { addGuardians } from './helpers/dsm'; import { BlsService } from 'bls'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; import { makeServer } from './server'; import { mockKey } from './helpers/keys-fixtures'; @@ -59,12 +58,11 @@ describe('ganache e2e tests', () => { let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let keyValidator: KeyValidatorInterface; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; let guardianMessageService: GuardianMessageService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; let securityService: SecurityService; @@ -86,7 +84,7 @@ describe('ganache e2e tests', () => { const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -95,13 +93,12 @@ describe('ganache e2e tests', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); @@ -138,11 +135,14 @@ describe('ganache e2e tests', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); // sign validation validateKeys = jest.spyOn(keyValidator, 'validateKeys'); @@ -152,7 +152,7 @@ describe('ganache e2e tests', () => { // we cant make real unvetting unvetSigningKeys = jest .spyOn(securityService, 'unvetSigningKeys') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(null as any)); }; beforeEach(async () => { @@ -172,7 +172,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -180,12 +180,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -198,17 +198,18 @@ describe('ganache e2e tests', () => { used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign], - ); - const { depositData: depositData } = signDeposit(pk, sk, LIDO_WC); - const { wallet } = await makeDeposit(depositData, providerService); + const invalidKeys = [keyWithWrongSign]; + + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, invalidKeys, meta); + const walletAddress = await getWalletAddress(); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -230,9 +231,9 @@ describe('ganache e2e tests', () => { expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000000', }), @@ -243,23 +244,23 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); - // if depositData was not changed it will not validate again + await providerService.provider.send('evm_mine', []); + // if depositData was not changed it will not validate again + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign], - ); + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, invalidKeys, newMeta); validateKeys.mockClear(); sendDepositMessage.mockClear(); @@ -277,9 +278,9 @@ describe('ganache e2e tests', () => { expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000000', }), @@ -290,9 +291,9 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); @@ -303,7 +304,7 @@ describe('ganache e2e tests', () => { test('should validate again if deposit data was changed', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -311,12 +312,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX, CSM], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -329,6 +330,7 @@ describe('ganache e2e tests', () => { used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }; const dvtKey = { @@ -336,24 +338,22 @@ describe('ganache e2e tests', () => { moduleAddress: SIMPLE_DVT, }; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign, dvtKey], - ); + const invalidKeys = [keyWithWrongSign, dvtKey]; + + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, invalidKeys, meta); + + const walletAddress = await getWalletAddress(); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); - const { depositData: depositData, signature: lidoSign } = signDeposit( - pk, - sk, - LIDO_WC, - ); - const { wallet } = await makeDeposit(depositData, providerService); + const { signature: lidoSign } = signDeposit(pk, sk, LIDO_WC); expect(validateKeys).toBeCalledTimes(2); expect(validateKeys).toHaveBeenNthCalledWith( @@ -380,9 +380,9 @@ describe('ganache e2e tests', () => { expect(sendUnvetMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, operatorIds: '0x0000000000000000', vettedKeysByOperator: '0x00000000000000000000000000000000', }), @@ -392,27 +392,27 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); - const newBlock = await providerService.provider.getBlock('latest'); - const fixedKey = { ...keyWithWrongSign, depositSignature: toHexString(lidoSign), }; - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [fixedKey, dvtKey], - ); + const fixedKeys = [fixedKey, dvtKey]; + + await providerService.provider.send('evm_mine', []); + const newBlock = await providerService.provider.getBlock('latest'); + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, fixedKeys, newMeta); validateKeys.mockClear(); sendDepositMessage.mockClear(); @@ -438,21 +438,105 @@ describe('ganache e2e tests', () => { 1, expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); }); + + test('adding not vetted invalid key will not set on soft pause module', async () => { + const currentBlock = await providerService.provider.getBlock('latest'); + + await levelDBService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + }, + }); + + await signingKeysRegistryService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], + }, + }); + + const keyWithWrongSign = { + key: toHexString(pk), + // just some random sign + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + vetted: false, + }; + + const dvtKey = { + ...mockKey, + moduleAddress: SIMPLE_DVT, + }; + + const keys = [keyWithWrongSign, dvtKey]; + + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); + + const walletAddress = await getWalletAddress(); + + await guardianService.handleNewBlock(); + + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + expect(validateKeys).toBeCalledTimes(2); + expect(validateKeys).toHaveBeenNthCalledWith(1, []); + expect(validateKeys).toHaveBeenNthCalledWith( + 2, + expect.arrayContaining([ + expect.objectContaining({ + key: mockKey.key, + depositSignature: mockKey.depositSignature, + }), + ]), + ); + expect(sendUnvetMessage).toBeCalledTimes(0); + expect(sendDepositMessage).toBeCalledTimes(2); + expect(sendDepositMessage).toBeCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 1, + }), + ); + expect(sendDepositMessage).toBeCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: walletAddress, + guardianIndex: 7, + stakingModuleId: 2, + }), + ); + expect(sendPauseMessage).toBeCalledTimes(0); + }); }); diff --git a/test/invalid-keys.e2e-spec.ts b/test/invalid-keys.e2e-spec.ts index 9e6b95d3..791c2b1f 100644 --- a/test/invalid-keys.e2e-spec.ts +++ b/test/invalid-keys.e2e-spec.ts @@ -3,10 +3,11 @@ import { toHexString } from '@chainsafe/ssz'; // Helpers import { - mockedDvtOperators, - mockOperator1, - mockOperator2, - setupMockModules, + keysApiMockGetAllKeys, + keysApiMockGetModules, + mockedModuleCurated, + mockedModuleDvt, + mockMeta, } from './helpers'; // Constants @@ -19,7 +20,6 @@ import { pk, NOP_REGISTRY, SIMPLE_DVT, - SANDBOX, LIDO_WC, FORK_BLOCK_V2, UNLOCKED_ACCOUNTS_V2, @@ -37,20 +37,19 @@ import { closeServer, initLevelDB, } from './helpers/test-setup'; -import { DepositService } from 'contracts/deposit'; import { GuardianService } from 'guardian'; import { KeysApiService } from 'keys-api/keys-api.service'; import { ProviderService } from 'provider'; import { Server } from 'ganache'; import { GuardianMessageService } from 'guardian/guardian-message'; -import { LevelDBService } from 'contracts/deposit/leveldb'; -import { LevelDBService as SignKeyLevelDBService } from 'contracts/signing-key-events-cache/leveldb'; +import { DepositsRegistryStoreService } from 'contracts/deposits-registry/store'; +import { SigningKeysStoreService as SignKeyLevelDBService } from 'contracts/signing-keys-registry/store'; import { KeyValidatorInterface } from '@lido-nestjs/key-validation'; -import { makeDeposit, signDeposit } from './helpers/deposit'; -import { SigningKeyEventsCacheService } from 'contracts/signing-key-events-cache'; +import { getWalletAddress, signDeposit } from './helpers/deposit'; +import { SigningKeysRegistryService } from 'contracts/signing-keys-registry'; import { addGuardians } from './helpers/dsm'; import { BlsService } from 'bls'; -import { DepositIntegrityCheckerService } from 'contracts/deposit/integrity-checker'; +import { DepositIntegrityCheckerService } from 'contracts/deposits-registry/sanity-checker'; import { makeServer } from './server'; import { mockKey } from './helpers/keys-fixtures'; @@ -59,15 +58,14 @@ describe('ganache e2e tests', () => { let providerService: ProviderService; let keysApiService: KeysApiService; let guardianService: GuardianService; - let depositService: DepositService; let keyValidator: KeyValidatorInterface; let sendDepositMessage: jest.SpyInstance; let sendPauseMessage: jest.SpyInstance; let validateKeys: jest.SpyInstance; - let levelDBService: LevelDBService; + let levelDBService: DepositsRegistryStoreService; let signKeyLevelDBService: SignKeyLevelDBService; let guardianMessageService: GuardianMessageService; - let signingKeyEventsCacheService: SigningKeyEventsCacheService; + let signingKeysRegistryService: SigningKeysRegistryService; let depositIntegrityCheckerService: DepositIntegrityCheckerService; const setupServer = async () => { @@ -84,7 +82,7 @@ describe('ganache e2e tests', () => { const setupTestingServices = async (moduleRef) => { // leveldb service - levelDBService = moduleRef.get(LevelDBService); + levelDBService = moduleRef.get(DepositsRegistryStoreService); signKeyLevelDBService = moduleRef.get(SignKeyLevelDBService); await initLevelDB(levelDBService, signKeyLevelDBService); @@ -93,13 +91,12 @@ describe('ganache e2e tests', () => { depositIntegrityCheckerService = moduleRef.get( DepositIntegrityCheckerService, ); - depositService = moduleRef.get(DepositService); const blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); // keys events service - signingKeyEventsCacheService = moduleRef.get(SigningKeyEventsCacheService); + signingKeysRegistryService = moduleRef.get(SigningKeysRegistryService); providerService = moduleRef.get(ProviderService); @@ -130,11 +127,14 @@ describe('ganache e2e tests', () => { // deposit cache mocks jest - .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .spyOn(depositIntegrityCheckerService, 'putEventsToTree') .mockImplementation(() => Promise.resolve()); + jest + .spyOn(depositIntegrityCheckerService, 'checkLatestRoot') + .mockImplementation(() => Promise.resolve(true)); jest .spyOn(depositIntegrityCheckerService, 'checkFinalizedRoot') - .mockImplementation(() => Promise.resolve()); + .mockImplementation(() => Promise.resolve(true)); // sign validation validateKeys = jest.spyOn(keyValidator, 'validateKeys'); @@ -157,7 +157,7 @@ describe('ganache e2e tests', () => { async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -165,17 +165,16 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); - const { depositData: depositData } = signDeposit(pk, sk, LIDO_WC); - const { wallet } = await makeDeposit(depositData, providerService); + const walletAddress = await getWalletAddress(); const keyWithWrongSign = { key: toHexString(pk), @@ -186,15 +185,17 @@ describe('ganache e2e tests', () => { used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }; - const { sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign], - ); + const keys = [keyWithWrongSign]; + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); + await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -216,26 +217,25 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); // if depositData was not changed it will not validate again - + await providerService.provider.send('evm_mine', []); const newBlock = await providerService.provider.getBlock('latest'); - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign], - ); + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, newMeta); validateKeys.mockClear(); sendDepositMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); @@ -249,9 +249,9 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); @@ -262,7 +262,7 @@ describe('ganache e2e tests', () => { test('should validate again if deposit data was changed', async () => { const currentBlock = await providerService.provider.getBlock('latest'); - await depositService.setCachedEvents({ + await levelDBService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, @@ -270,12 +270,12 @@ describe('ganache e2e tests', () => { }, }); - await signingKeyEventsCacheService.setCachedEvents({ + await signingKeysRegistryService.setCachedEvents({ data: [], headers: { startBlock: currentBlock.number, endBlock: currentBlock.number, - stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT, SANDBOX], + stakingModulesAddresses: [NOP_REGISTRY, SIMPLE_DVT], }, }); @@ -288,31 +288,29 @@ describe('ganache e2e tests', () => { used: false, index: 0, moduleAddress: NOP_REGISTRY, + vetted: true, }; const dvtKey = { ...mockKey, moduleAddress: SIMPLE_DVT, + vetted: true, }; - const { curatedModule, sdvtModule } = setupMockModules( - currentBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [keyWithWrongSign, dvtKey], - ); + const keys = [keyWithWrongSign, dvtKey]; + const meta = mockMeta(currentBlock, currentBlock.hash); + // setup /v1/modules + const stakingModules = [mockedModuleCurated, mockedModuleDvt]; + keysApiMockGetModules(keysApiService, stakingModules, meta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, keys, meta); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); - const { depositData: depositData, signature: lidoSign } = signDeposit( - pk, - sk, - LIDO_WC, - ); - const { wallet } = await makeDeposit(depositData, providerService); + const { signature: lidoSign } = signDeposit(pk, sk, LIDO_WC); + const walletAddress = await getWalletAddress(); expect(validateKeys).toBeCalledTimes(2); expect(validateKeys).toHaveBeenNthCalledWith( @@ -330,8 +328,8 @@ describe('ganache e2e tests', () => { 2, expect.arrayContaining([ expect.objectContaining({ - key: mockKey.key, - depositSignature: mockKey.depositSignature, + key: dvtKey.key, + depositSignature: dvtKey.depositSignature, }), ]), ); @@ -339,30 +337,31 @@ describe('ganache e2e tests', () => { expect(sendDepositMessage).toBeCalledWith( expect.objectContaining({ blockNumber: currentBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); expect(sendPauseMessage).toBeCalledTimes(0); - const newBlock = await providerService.provider.getBlock('latest'); - const fixedKey = { ...keyWithWrongSign, depositSignature: toHexString(lidoSign), }; - setupMockModules( - newBlock, - keysApiService, - [mockOperator1, mockOperator2], - mockedDvtOperators, - [fixedKey, dvtKey], - ); + const fixedKeys = [fixedKey, dvtKey]; + await providerService.provider.send('evm_mine', []); + const newBlock = await providerService.provider.getBlock('latest'); + + const newMeta = mockMeta(newBlock, newBlock.hash); + // setup /v1/modules + keysApiMockGetModules(keysApiService, stakingModules, newMeta); + // setup /v1/keys + keysApiMockGetAllKeys(keysApiService, fixedKeys, newMeta); validateKeys.mockClear(); sendDepositMessage.mockClear(); + sendPauseMessage.mockClear(); await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); @@ -383,18 +382,18 @@ describe('ganache e2e tests', () => { 1, expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: curatedModule.id, + stakingModuleId: 1, }), ); expect(sendDepositMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ blockNumber: newBlock.number, - guardianAddress: wallet.address, + guardianAddress: walletAddress, guardianIndex: 7, - stakingModuleId: sdvtModule.id, + stakingModuleId: 2, }), ); diff --git a/tsconfig.json b/tsconfig.json index 042e8f28..4d726a82 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "es2020", "sourceMap": true, "outDir": "./dist", "baseUrl": "./src", diff --git a/yarn.lock b/yarn.lock index 3c975a48..948aea51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -461,22 +461,7 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@ethersproject/abi@5.5.0", "@ethersproject/abi@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" - integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w== - dependencies: - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -491,19 +476,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@ethersproject/abstract-provider@5.5.1", "@ethersproject/abstract-provider@^5.5.0": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" - integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/networks" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/web" "^5.5.0" - "@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" @@ -517,17 +489,6 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/web" "^5.7.0" -"@ethersproject/abstract-signer@5.5.0", "@ethersproject/abstract-signer@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" - integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA== - dependencies: - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" @@ -539,17 +500,6 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.5.0", "@ethersproject/address@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" - integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - "@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" @@ -561,13 +511,6 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/rlp" "^5.7.0" -"@ethersproject/base64@5.5.0", "@ethersproject/base64@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" - integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" @@ -575,14 +518,6 @@ dependencies: "@ethersproject/bytes" "^5.7.0" -"@ethersproject/basex@5.5.0", "@ethersproject/basex@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3" - integrity sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" @@ -591,15 +526,6 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/bignumber@5.5.0", "@ethersproject/bignumber@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527" - integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - bn.js "^4.11.9" - "@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" @@ -609,13 +535,6 @@ "@ethersproject/logger" "^5.7.0" bn.js "^5.2.1" -"@ethersproject/bytes@5.5.0", "@ethersproject/bytes@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" - integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog== - dependencies: - "@ethersproject/logger" "^5.5.0" - "@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" @@ -623,13 +542,6 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/constants@5.5.0", "@ethersproject/constants@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" - integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" @@ -637,22 +549,6 @@ dependencies: "@ethersproject/bignumber" "^5.7.0" -"@ethersproject/contracts@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197" - integrity sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg== - dependencies: - "@ethersproject/abi" "^5.5.0" - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/contracts@5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" @@ -669,20 +565,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/transactions" "^5.7.0" -"@ethersproject/hash@5.5.0", "@ethersproject/hash@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" - integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" @@ -698,24 +580,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@ethersproject/hdnode@5.5.0", "@ethersproject/hdnode@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6" - integrity sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/basex" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/pbkdf2" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/signing-key" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/wordlists" "^5.5.0" - "@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" @@ -734,25 +598,6 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/wordlists" "^5.7.0" -"@ethersproject/json-wallets@5.5.0", "@ethersproject/json-wallets@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325" - integrity sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/hdnode" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/pbkdf2" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/random" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - "@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" @@ -772,14 +617,6 @@ aes-js "3.0.0" scrypt-js "3.0.1" -"@ethersproject/keccak256@5.5.0", "@ethersproject/keccak256@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" - integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - js-sha3 "0.8.0" - "@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" @@ -788,23 +625,11 @@ "@ethersproject/bytes" "^5.7.0" js-sha3 "0.8.0" -"@ethersproject/logger@5.5.0", "@ethersproject/logger@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" - integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== - "@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== -"@ethersproject/networks@5.5.2", "@ethersproject/networks@^5.5.0": - version "5.5.2" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.2.tgz#784c8b1283cd2a931114ab428dae1bd00c07630b" - integrity sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ== - dependencies: - "@ethersproject/logger" "^5.5.0" - "@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" @@ -812,14 +637,6 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/pbkdf2@5.5.0", "@ethersproject/pbkdf2@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050" - integrity sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" @@ -828,13 +645,6 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/sha2" "^5.7.0" -"@ethersproject/properties@5.5.0", "@ethersproject/properties@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" - integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA== - dependencies: - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" @@ -842,31 +652,6 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.5.3", "@ethersproject/providers@^5.4.5": - version "5.5.3" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.3.tgz#56c2b070542ac44eb5de2ed3cf6784acd60a3130" - integrity sha512-ZHXxXXXWHuwCQKrgdpIkbzMNJMvs+9YWemanwp1fA7XZEv7QlilseysPvQe0D7Q7DlkJX/w/bGA1MdgK2TbGvA== - dependencies: - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/basex" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/networks" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/random" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/web" "^5.5.0" - bech32 "1.1.4" - ws "7.4.6" - "@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.5.3": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" @@ -893,14 +678,6 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/random@5.5.1", "@ethersproject/random@^5.5.0": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.1.tgz#7cdf38ea93dc0b1ed1d8e480ccdaf3535c555415" - integrity sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" @@ -909,14 +686,6 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/rlp@5.5.0", "@ethersproject/rlp@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0" - integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" @@ -925,15 +694,6 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/sha2@5.5.0", "@ethersproject/sha2@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7" - integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - hash.js "1.1.7" - "@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" @@ -943,18 +703,6 @@ "@ethersproject/logger" "^5.7.0" hash.js "1.1.7" -"@ethersproject/signing-key@5.5.0", "@ethersproject/signing-key@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" - integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - bn.js "^4.11.9" - elliptic "6.5.4" - hash.js "1.1.7" - "@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" @@ -967,18 +715,6 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f" - integrity sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/solidity@5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" @@ -991,15 +727,6 @@ "@ethersproject/sha2" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@ethersproject/strings@5.5.0", "@ethersproject/strings@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" - integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" @@ -1009,21 +736,6 @@ "@ethersproject/constants" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/transactions@5.5.0", "@ethersproject/transactions@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" - integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA== - dependencies: - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - "@ethersproject/signing-key" "^5.5.0" - "@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" @@ -1039,15 +751,6 @@ "@ethersproject/rlp" "^5.7.0" "@ethersproject/signing-key" "^5.7.0" -"@ethersproject/units@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.5.0.tgz#104d02db5b5dc42cc672cc4587bafb87a95ee45e" - integrity sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/units@5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" @@ -1057,27 +760,6 @@ "@ethersproject/constants" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/wallet@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75" - integrity sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q== - dependencies: - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/hdnode" "^5.5.0" - "@ethersproject/json-wallets" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/random" "^5.5.0" - "@ethersproject/signing-key" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/wordlists" "^5.5.0" - "@ethersproject/wallet@5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" @@ -1099,17 +781,6 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/wordlists" "^5.7.0" -"@ethersproject/web@5.5.1", "@ethersproject/web@^5.5.0": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" - integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg== - dependencies: - "@ethersproject/base64" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" @@ -1121,17 +792,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@ethersproject/wordlists@5.5.0", "@ethersproject/wordlists@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f" - integrity sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" @@ -1654,9 +1314,9 @@ integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== "@typechain/ethers-v5@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-7.1.2.tgz#dbf31663f75cc50f2d9ad232f6e354c6a3e81465" - integrity sha512-sD4HVkTL5aIJa3Ft+CmqiOapba0zzZ8xa+QywcWH40Rm/dcxvZWwcCMnnI3En0JebkxOcAVfH3do+kQ9rKSxYw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-7.2.0.tgz#d559cffe0efe6bdbc20e644b817f6fa8add5e8f8" + integrity sha512-jfcmlTvaaJjng63QsT49MT6R1HFhtO/TBMWbyzPFSzMmVIqb2tL6prnKBs4ZJrSvmgIXWy+ttSjpaxCTq8D/Tw== dependencies: lodash "^4.17.15" ts-essentials "^7.0.1" @@ -1829,7 +1489,12 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/prettier@^2.1.1", "@types/prettier@^2.1.5": +"@types/prettier@^2.1.1": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== + +"@types/prettier@^2.1.5": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== @@ -2186,7 +1851,7 @@ acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0: aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== agent-base@6, agent-base@^6.0.2: version "6.0.2" @@ -2353,7 +2018,7 @@ argparse@^1.0.7: array-back@^1.0.3, array-back@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" - integrity sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs= + integrity sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw== dependencies: typical "^2.6.0" @@ -2549,7 +2214,7 @@ braces@^3.0.1, braces@~3.0.2: brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== browser-level@^1.0.1: version "1.0.1" @@ -3108,13 +2773,20 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" +debug@^4.1.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -3541,43 +3213,7 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -ethers@^5.4.7: - version "5.5.4" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.4.tgz#e1155b73376a2f5da448e4a33351b57a885f4352" - integrity sha512-N9IAXsF8iKhgHIC6pquzRgPBJEzc9auw3JoRkaKe+y4Wl/LFBtDDunNe7YmdomontECAcC5APaAgWZBiu1kirw== - dependencies: - "@ethersproject/abi" "5.5.0" - "@ethersproject/abstract-provider" "5.5.1" - "@ethersproject/abstract-signer" "5.5.0" - "@ethersproject/address" "5.5.0" - "@ethersproject/base64" "5.5.0" - "@ethersproject/basex" "5.5.0" - "@ethersproject/bignumber" "5.5.0" - "@ethersproject/bytes" "5.5.0" - "@ethersproject/constants" "5.5.0" - "@ethersproject/contracts" "5.5.0" - "@ethersproject/hash" "5.5.0" - "@ethersproject/hdnode" "5.5.0" - "@ethersproject/json-wallets" "5.5.0" - "@ethersproject/keccak256" "5.5.0" - "@ethersproject/logger" "5.5.0" - "@ethersproject/networks" "5.5.2" - "@ethersproject/pbkdf2" "5.5.0" - "@ethersproject/properties" "5.5.0" - "@ethersproject/providers" "5.5.3" - "@ethersproject/random" "5.5.1" - "@ethersproject/rlp" "5.5.0" - "@ethersproject/sha2" "5.5.0" - "@ethersproject/signing-key" "5.5.0" - "@ethersproject/solidity" "5.5.0" - "@ethersproject/strings" "5.5.0" - "@ethersproject/transactions" "5.5.0" - "@ethersproject/units" "5.5.0" - "@ethersproject/wallet" "5.5.0" - "@ethersproject/web" "5.5.1" - "@ethersproject/wordlists" "5.5.0" - -ethers@^5.5.4: +ethers@5.7.2, ethers@^5.5.4: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -3822,7 +3458,7 @@ finalhandler@~1.1.2: find-replace@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" - integrity sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A= + integrity sha512-KrUnjzDCD9426YnCP56zGYy/eieTnhtK6Vn++j+JJzmlsWWwEkDnsyVF575spT6HJ6Ow9tlbT3TQTDsa+O4UWA== dependencies: array-back "^1.0.4" test-value "^2.1.0" @@ -3941,7 +3577,7 @@ fs-monkey@1.0.3: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" @@ -4041,7 +3677,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -4053,6 +3689,18 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4077,7 +3725,12 @@ globby@^11.0.3: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== @@ -4144,7 +3797,7 @@ hdr-histogram-percentiles-obj@^3.0.0: hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== dependencies: hash.js "^1.0.3" minimalistic-assert "^1.0.0" @@ -4275,7 +3928,7 @@ infer-owner@^1.0.4: inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -5012,7 +4665,7 @@ jsonc-parser@3.0.0: jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" @@ -5357,9 +5010,9 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.4: +minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -5678,7 +5331,7 @@ on-finished@^2.3.0, on-finished@~2.3.0: once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -5841,7 +5494,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" @@ -5920,7 +5573,12 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.1.2, prettier@^2.3.2: +prettier@^2.1.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +prettier@^2.3.2: version "2.4.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== @@ -6655,7 +6313,7 @@ test-exclude@^6.0.0: test-value@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" - integrity sha1-Edpv9nDzRxpztiXKTz/c97t0gpE= + integrity sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w== dependencies: array-back "^1.0.3" typical "^2.6.0" @@ -6894,9 +6552,9 @@ type@^2.5.0: integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== typechain@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/typechain/-/typechain-5.1.2.tgz#c8784d6155a8e69397ca47f438a3b4fb2aa939da" - integrity sha512-FuaCxJd7BD3ZAjVJoO+D6TnqKey3pQdsqOBsC83RKYWKli5BDhdf0TPkwfyjt20TUlZvOzJifz+lDwXsRkiSKA== + version "5.2.0" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-5.2.0.tgz#10525a44773a34547eb2eed8978cb72c0a39a0f4" + integrity sha512-0INirvQ+P+MwJOeMct+WLkUE4zov06QxC96D+i3uGFEHoiSkZN70MKDQsaj8zkL86wQwByJReI2e7fOUwECFuw== dependencies: "@types/prettier" "^2.1.1" command-line-args "^4.0.7" @@ -6934,7 +6592,7 @@ typescript@^4.3.5: typical@^2.6.0, typical@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" - integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= + integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== unique-filename@^1.1.1: version "1.1.1" @@ -7201,7 +6859,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3"