diff --git a/deploy/.env.example b/deploy/.env.example index b6e4f26..8ba75f2 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,17 +1,11 @@ -# COMMON -LOG_LEVEL=INFO # mainnet or goerli NETWORK=mainnet ENABLE_HEALTH_SERVER=true HEALTH_SERVER_PORT=8080 HEALTH_SERVER_HOST=0.0.0.0 -ETH1_CONFIRMATION_BLOCKS=15 ORACLE_PRIVATE_KEY=0x # ORACLE -IPFS_PIN_ENDPOINTS=/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http -IPFS_FETCH_ENDPOINTS=https://gateway.pinata.cloud,http://cloudflare-ipfs.com,https://ipfs.io -IPFS_PINATA_PIN_ENDPOINT=https://api.pinata.cloud/pinning/pinJSONToIPFS IPFS_PINATA_API_KEY="" IPFS_PINATA_SECRET_KEY="" ETH2_ENDPOINT=http://lighthouse:5052 @@ -19,7 +13,6 @@ ETH2_ENDPOINT=http://lighthouse:5052 ETH2_CLIENT=lighthouse AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" -ORACLE_PROCESS_INTERVAL=180 # Uncomment below lines if use self-hosted graph node # STAKEWISE_SUBGRAPH_URL=http://graph-node:8000/subgraphs/name/stakewise/stakewise @@ -28,10 +21,6 @@ ORACLE_PROCESS_INTERVAL=180 # KEEPER WEB3_ENDPOINT="" -KEEPER_PROCESS_INTERVAL=180 -TRANSACTION_TIMEOUT=900 -# 0.1 ETH -KEEPER_MIN_BALANCE_WEI=100000000000000000 # GRAPH postgres_host=postgres diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 05b7eb9..57f1060 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -21,7 +21,7 @@ volumes: services: oracle: container_name: oracle - image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.0.0-rc.1 + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.0.0-rc.2 restart: always entrypoint: ["python"] command: ["oracle/oracle/main.py"] @@ -30,7 +30,7 @@ services: keeper: container_name: keeper - image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.0.0-rc.1 + image: europe-west4-docker.pkg.dev/stakewiselabs/public/oracle:v2.0.0-rc.2 restart: always entrypoint: ["python"] command: ["oracle/keeper/main.py"] @@ -77,7 +77,7 @@ services: subgraphs: container_name: subgraphs - image: europe-west4-docker.pkg.dev/stakewiselabs/public/subgraphs:v1.0.1 + image: europe-west4-docker.pkg.dev/stakewiselabs/public/subgraphs:v1.0.2 command: > /bin/sh -c "until nc -vz graph-node 8020; do echo 'Waiting graph-node'; sleep 2; done && yarn prepare:${NETWORK} diff --git a/oracle/common/settings.py b/oracle/common/settings.py index 564d85b..fe4c473 100644 --- a/oracle/common/settings.py +++ b/oracle/common/settings.py @@ -4,8 +4,7 @@ REWARD_VOTE_FILENAME = "reward-vote.json" DISTRIBUTOR_VOTE_FILENAME = "distributor-vote.json" -INIT_VALIDATOR_VOTE_FILENAME = "init-validator-vote.json" -FINALIZE_VALIDATOR_VOTE_FILENAME = "finalize-validator-vote.json" +VALIDATOR_VOTE_FILENAME = "validator-vote.json" # supported networks MAINNET = "mainnet" diff --git a/oracle/keeper/settings.py b/oracle/keeper/settings.py index 0fe18a3..15cbb44 100644 --- a/oracle/keeper/settings.py +++ b/oracle/keeper/settings.py @@ -7,7 +7,7 @@ ORACLE_PRIVATE_KEY = config("ORACLE_PRIVATE_KEY") -KEEPER_PROCESS_INTERVAL = config("KEEPER_PROCESS_INTERVAL", default=180, cast=int) +KEEPER_PROCESS_INTERVAL = config("KEEPER_PROCESS_INTERVAL", default=30, cast=int) KEEPER_MIN_BALANCE_WEI = config( "KEEPER_MIN_BALANCE_WEI", default=Web3.toWei(0.1, "ether"), cast=int @@ -24,7 +24,7 @@ ) elif NETWORK == GOERLI: ORACLES_CONTRACT_ADDRESS = Web3.toChecksumAddress( - "0x06b0C9476315634dCc59AA3F3f7d5Df6feCbAa90" + "0x4bBaA17eFd71683dCb9C769DD38E7674994FE38d" ) MULTICALL_CONTRACT_ADDRESS = Web3.toChecksumAddress( "0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e" diff --git a/oracle/keeper/typings.py b/oracle/keeper/typings.py index dd3cab6..126f68d 100644 --- a/oracle/keeper/typings.py +++ b/oracle/keeper/typings.py @@ -17,5 +17,4 @@ class Parameters(NamedTuple): class OraclesVotes(NamedTuple): rewards: List[RewardVote] distributor: List[DistributorVote] - initialize_validator: List[ValidatorVote] - finalize_validator: List[ValidatorVote] + validator: List[ValidatorVote] diff --git a/oracle/keeper/utils.py b/oracle/keeper/utils.py index 191bdb6..3d38a97 100644 --- a/oracle/keeper/utils.py +++ b/oracle/keeper/utils.py @@ -18,9 +18,8 @@ AWS_S3_REGION, DISTRIBUTOR_VOTE_FILENAME, ETH1_CONFIRMATION_BLOCKS, - FINALIZE_VALIDATOR_VOTE_FILENAME, - INIT_VALIDATOR_VOTE_FILENAME, REWARD_VOTE_FILENAME, + VALIDATOR_VOTE_FILENAME, ) from oracle.keeper.clients import web3_client from oracle.keeper.contracts import multicall_contract, oracles_contract @@ -141,8 +140,13 @@ def check_validator_vote(vote: ValidatorVote, oracle: ChecksumAddress) -> bool: """Checks whether oracle's validator vote is correct.""" try: encoded_data: bytes = web3_client.codec.encode_abi( - ["uint256", "bytes", "address"], - [int(vote["nonce"]), vote["public_key"], vote["operator"]], + ["uint256", "bytes", "address", "bytes32"], + [ + int(vote["nonce"]), + vote["public_key"], + vote["operator"], + vote["validators_count"], + ], ) return validate_vote_signature(encoded_data, oracle, vote["signature"]) except: # noqa: E722 @@ -154,9 +158,7 @@ def get_oracles_votes( rewards_nonce: int, validators_nonce: int, oracles: List[ChecksumAddress] ) -> OraclesVotes: """Fetches oracle votes that match current nonces.""" - votes = OraclesVotes( - rewards=[], distributor=[], initialize_validator=[], finalize_validator=[] - ) + votes = OraclesVotes(rewards=[], distributor=[], validator=[]) for oracle in oracles: for arr, filename, correct_nonce, vote_checker in [ @@ -168,14 +170,8 @@ def get_oracles_votes( check_distributor_vote, ), ( - votes.initialize_validator, - INIT_VALIDATOR_VOTE_FILENAME, - validators_nonce, - check_validator_vote, - ), - ( - votes.finalize_validator, - FINALIZE_VALIDATOR_VOTE_FILENAME, + votes.validator, + VALIDATOR_VOTE_FILENAME, validators_nonce, check_validator_vote, ), @@ -319,48 +315,55 @@ def submit_votes(votes: OraclesVotes, total_oracles: int) -> None: ) logger.info("Merkle Distributor has been successfully updated") - for validator_votes, func_name in [ - (votes.initialize_validator, "initializeValidator"), - (votes.finalize_validator, "finalizeValidator"), - ]: - counter = Counter( - [(vote["public_key"], vote["operator"]) for vote in validator_votes] - ) - most_voted = counter.most_common(1) - if most_voted and can_submit(most_voted[0][1], total_oracles): - public_key, operator = most_voted[0][0] - - signatures = [] - i = 0 - while not can_submit(len(signatures), total_oracles): - vote = validator_votes[i] - if (public_key, operator) == ( - vote["public_key"], - vote["operator"], - ): - signatures.append(vote["signature"]) - i += 1 - - validator_vote: ValidatorVote = next( - vote - for vote in validator_votes - if (vote["public_key"], vote["operator"]) == (public_key, operator) - ) + counter = Counter( + [ + (vote["public_key"], vote["operator"], vote["validators_count"]) + for vote in votes.validator + ] + ) + most_voted = counter.most_common(1) + if most_voted and can_submit(most_voted[0][1], total_oracles): + public_key, operator, validators_count = most_voted[0][0] + signatures = [] + i = 0 + while not can_submit(len(signatures), total_oracles): + vote = votes.validator[i] + if (public_key, operator, validators_count) == ( + vote["public_key"], + vote["operator"], + vote["validators_count"], + ): + signatures.append(vote["signature"]) + i += 1 - logger.info( - f"Submitting {func_name}: operator={operator}, public key={public_key}" + validator_vote: ValidatorVote = next( + vote + for vote in votes.validator + if (public_key, operator, validators_count) + == ( + vote["public_key"], + vote["operator"], + vote["validators_count"], ) - submit_update( - getattr(oracles_contract.functions, func_name)( - dict( - operator=validator_vote["operator"], - withdrawalCredentials=validator_vote["withdrawal_credentials"], - depositDataRoot=validator_vote["deposit_data_root"], - publicKey=validator_vote["public_key"], - signature=validator_vote["deposit_data_signature"], - ), - validator_vote["proof"], - signatures, + ) + logger.info( + f"Submitting validator registration: " + f"operator={operator}, " + f"public key={public_key}, " + f"validator count={validators_count}" + ) + submit_update( + oracles_contract.functions.registerValidator( + dict( + operator=validator_vote["operator"], + withdrawalCredentials=validator_vote["withdrawal_credentials"], + depositDataRoot=validator_vote["deposit_data_root"], + publicKey=validator_vote["public_key"], + signature=validator_vote["deposit_data_signature"], ), - ) - logger.info(f"{func_name} has been successfully executed") + validator_vote["proof"], + validators_count, + signatures, + ), + ) + logger.info("Validator registration has been successfully submitted") diff --git a/oracle/oracle/eth1.py b/oracle/oracle/eth1.py index 723d21d..52e07dd 100644 --- a/oracle/oracle/eth1.py +++ b/oracle/oracle/eth1.py @@ -14,20 +14,17 @@ from .distributor.types import DistributorVote, DistributorVotingParameters from .graphql_queries import ( FINALIZED_BLOCK_QUERY, + LATEST_BLOCK_QUERY, ORACLE_QUERY, VOTING_PARAMETERS_QUERY, ) from .rewards.types import RewardsVotingParameters, RewardVote -from .validators.types import ( - FinalizeValidatorVotingParameters, - InitializeValidatorVotingParameters, - ValidatorVote, -) +from .validators.types import ValidatorVote logger = logging.getLogger(__name__) -class FinalizedBlock(TypedDict): +class Block(TypedDict): block_number: BlockNumber timestamp: Timestamp @@ -35,12 +32,10 @@ class FinalizedBlock(TypedDict): class VotingParameters(TypedDict): rewards: RewardsVotingParameters distributor: DistributorVotingParameters - initialize_validator: InitializeValidatorVotingParameters - finalize_validator: FinalizeValidatorVotingParameters @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def get_finalized_block() -> FinalizedBlock: +async def get_finalized_block() -> Block: """Gets the finalized block number and its timestamp.""" result: Dict = await execute_ethereum_gql_query( query=FINALIZED_BLOCK_QUERY, @@ -48,7 +43,22 @@ async def get_finalized_block() -> FinalizedBlock: confirmation_blocks=ETH1_CONFIRMATION_BLOCKS, ), ) - return FinalizedBlock( + return Block( + block_number=BlockNumber(int(result["blocks"][0]["id"])), + timestamp=Timestamp(int(result["blocks"][0]["timestamp"])), + ) + + +@backoff.on_exception(backoff.expo, Exception, max_time=900) +async def get_latest_block() -> Block: + """Gets the latest block number and its timestamp.""" + result: Dict = await execute_ethereum_gql_query( + query=LATEST_BLOCK_QUERY, + variables=dict( + confirmation_blocks=ETH1_CONFIRMATION_BLOCKS, + ), + ) + return Block( block_number=BlockNumber(int(result["blocks"][0]["id"])), timestamp=Timestamp(int(result["blocks"][0]["timestamp"])), ) @@ -66,17 +76,6 @@ async def get_voting_parameters(block_number: BlockNumber) -> VotingParameters: network = result["networks"][0] distributor = result["merkleDistributors"][0] reward_eth_token = result["rewardEthTokens"][0] - pool = result["pools"][0] - - validators = result["validators"] - if validators: - operator = validators[0].get("operator", {}).get("id", None) - if operator is not None: - operator = Web3.toChecksumAddress(operator) - public_key = validators[0].get("id", None) - else: - operator = None - public_key = None rewards = RewardsVotingParameters( rewards_nonce=int(network["oraclesRewardsNonce"]), @@ -95,24 +94,7 @@ async def get_voting_parameters(block_number: BlockNumber) -> VotingParameters: protocol_reward=Wei(int(reward_eth_token["protocolPeriodReward"])), distributor_reward=Wei(int(reward_eth_token["distributorPeriodReward"])), ) - initialize_validator = InitializeValidatorVotingParameters( - validator_index=int(pool["pendingValidators"]) - + int(pool["activatedValidators"]), - validators_nonce=int(network["oraclesValidatorsNonce"]), - pool_balance=Wei(int(pool["balance"])), - finalizing_validator=public_key is not None, - ) - finalize_validator = FinalizeValidatorVotingParameters( - validators_nonce=int(network["oraclesValidatorsNonce"]), - operator=operator, - public_key=public_key, - ) - return VotingParameters( - rewards=rewards, - distributor=distributor, - initialize_validator=initialize_validator, - finalize_validator=finalize_validator, - ) + return VotingParameters(rewards=rewards, distributor=distributor) @backoff.on_exception(backoff.expo, Exception, max_time=900) diff --git a/oracle/oracle/graphql_queries.py b/oracle/oracle/graphql_queries.py index 25fa1a0..1432406 100644 --- a/oracle/oracle/graphql_queries.py +++ b/oracle/oracle/graphql_queries.py @@ -16,17 +16,26 @@ """ ) +LATEST_BLOCK_QUERY = gql( + """ + query getBlock { + blocks( + first: 1 + orderBy: id + orderDirection: desc + ) { + id + timestamp + } + } +""" +) + VOTING_PARAMETERS_QUERY = gql( """ query getVotingParameters($block_number: Int) { networks(block: { number: $block_number }) { oraclesRewardsNonce - oraclesValidatorsNonce - } - pools(block: { number: $block_number }) { - pendingValidators - activatedValidators - balance } merkleDistributors(block: { number: $block_number }) { merkleRoot @@ -41,25 +50,29 @@ updatedAtBlock updatedAtTimestamp } - validators( - block: { number: $block_number } - where: { registrationStatus: Initialized } - ) { - id - operator { - id - } + } +""" +) + +VALIDATOR_VOTING_PARAMETERS_QUERY = gql( + """ + query getVotingParameters($block_number: Int) { + networks(block: { number: $block_number }) { + oraclesValidatorsNonce + } + pools(block: { number: $block_number }) { + balance } } """ ) -FINALIZED_VALIDATORS_QUERY = gql( +REGISTERED_VALIDATORS_QUERY = gql( """ query getValidators($block_number: Int, $last_id: ID) { validators( block: { number: $block_number } - where: { registrationStatus: Finalized, id_gt: $last_id } + where: { id_gt: $last_id } first: 1000 orderBy: id orderDirection: asc @@ -317,33 +330,17 @@ """ ) -INITIALIZE_OPERATORS_QUERY = gql( +OPERATORS_QUERY = gql( """ - query getOperators($block_number: Int, $min_collateral: BigInt) { + query getOperators($block_number: Int) { operators( block: { number: $block_number } - where: { collateral_gte: $min_collateral, committed: true, locked: false } + where: { committed: true } orderBy: validatorsCount orderDirection: asc ) { id - initializeMerkleProofs - collateral - depositDataIndex - } - } -""" -) - -FINALIZE_OPERATOR_QUERY = gql( - """ - query getOperators($block_number: Int, $operator: ID) { - operators( - block: { number: $block_number } - where: { id: $operator, locked: true } - ) { - id - finalizeMerkleProofs + depositDataMerkleProofs depositDataIndex } } @@ -364,15 +361,16 @@ """ ) -VALIDATOR_REGISTRATIONS_QUERY = gql( +VALIDATOR_REGISTRATIONS_LATEST_INDEX_QUERY = gql( """ - query getValidatorRegistrations($block_number: Int, $public_key: Bytes) { + query getValidatorRegistrations($block_number: Int) { validatorRegistrations( block: { number: $block_number } - where: { id: $public_key } + first: 1 + orderBy: createdAtBlock + orderDirection: desc ) { - id - withdrawalCredentials + index } } """ diff --git a/oracle/oracle/main.py b/oracle/oracle/main.py index c6297a4..0ce250e 100644 --- a/oracle/oracle/main.py +++ b/oracle/oracle/main.py @@ -107,16 +107,8 @@ async def main() -> None: ), # check and update merkle distributor distributor_controller.process(voting_parameters["distributor"]), - # initializes validators - validators_controller.initialize( - voting_params=voting_parameters["initialize_validator"], - current_block_number=current_block_number, - ), - # finalizes validators - validators_controller.finalize( - voting_params=voting_parameters["finalize_validator"], - current_block_number=current_block_number, - ), + # process validators registration + validators_controller.process(), ) # wait until next processing time await asyncio.sleep(ORACLE_PROCESS_INTERVAL) diff --git a/oracle/oracle/rewards/controller.py b/oracle/oracle/rewards/controller.py index 35423da..0f35cfc 100644 --- a/oracle/oracle/rewards/controller.py +++ b/oracle/oracle/rewards/controller.py @@ -13,7 +13,7 @@ from oracle.oracle.eth1 import submit_vote from ..settings import SYNC_PERIOD -from .eth1 import get_finalized_validators_public_keys +from .eth1 import get_registered_validators_public_keys from .eth2 import ( PENDING_STATUSES, SECONDS_PER_EPOCH, @@ -80,7 +80,7 @@ async def process( return # fetch pool validator BLS public keys - public_keys = await get_finalized_validators_public_keys(current_block_number) + public_keys = await get_registered_validators_public_keys(current_block_number) # calculate current ETH2 epoch update_timestamp = int( diff --git a/oracle/oracle/rewards/eth1.py b/oracle/oracle/rewards/eth1.py index a016ed7..f334b31 100644 --- a/oracle/oracle/rewards/eth1.py +++ b/oracle/oracle/rewards/eth1.py @@ -4,19 +4,19 @@ from web3.types import BlockNumber from oracle.oracle.clients import execute_sw_gql_query -from oracle.oracle.graphql_queries import FINALIZED_VALIDATORS_QUERY +from oracle.oracle.graphql_queries import REGISTERED_VALIDATORS_QUERY -from .types import FinalizedValidatorsPublicKeys +from .types import RegisteredValidatorsPublicKeys @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def get_finalized_validators_public_keys( +async def get_registered_validators_public_keys( block_number: BlockNumber, -) -> FinalizedValidatorsPublicKeys: +) -> RegisteredValidatorsPublicKeys: """Fetches pool validators public keys.""" last_id = "" result: Dict = await execute_sw_gql_query( - query=FINALIZED_VALIDATORS_QUERY, + query=REGISTERED_VALIDATORS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) validators_chunk = result.get("validators", []) @@ -26,7 +26,7 @@ async def get_finalized_validators_public_keys( while len(validators_chunk) >= 1000: last_id = validators_chunk[-1]["id"] result: Dict = await execute_sw_gql_query( - query=FINALIZED_VALIDATORS_QUERY, + query=REGISTERED_VALIDATORS_QUERY, variables=dict(block_number=block_number, last_id=last_id), ) validators_chunk = result.get("validators", []) diff --git a/oracle/oracle/rewards/types.py b/oracle/oracle/rewards/types.py index 2457018..0c300fd 100644 --- a/oracle/oracle/rewards/types.py +++ b/oracle/oracle/rewards/types.py @@ -18,4 +18,4 @@ class RewardVote(TypedDict): total_rewards: str -FinalizedValidatorsPublicKeys = List[HexStr] +RegisteredValidatorsPublicKeys = List[HexStr] diff --git a/oracle/oracle/settings.py b/oracle/oracle/settings.py index aec7e17..d59a1fb 100644 --- a/oracle/oracle/settings.py +++ b/oracle/oracle/settings.py @@ -7,7 +7,9 @@ from oracle.common.settings import GOERLI, MAINNET, NETWORK IPFS_PIN_ENDPOINTS = config( - "IPFS_PIN_ENDPOINTS", cast=Csv(), default="/dns/ipfs.infura.io/tcp/5001/https" + "IPFS_PIN_ENDPOINTS", + cast=Csv(), + default="/dns/ipfs.infura.io/tcp/5001/https,/dns/ipfs/tcp/5001/http", ) IPFS_FETCH_ENDPOINTS = config( "IPFS_FETCH_ENDPOINTS", @@ -45,7 +47,7 @@ AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID", default="") AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY", default="") -ORACLE_PROCESS_INTERVAL = config("ORACLE_PROCESS_INTERVAL", default=180, cast=int) +ORACLE_PROCESS_INTERVAL = config("ORACLE_PROCESS_INTERVAL", default=30, cast=int) if NETWORK == MAINNET: SYNC_PERIOD = timedelta(days=1) diff --git a/oracle/oracle/validators/controller.py b/oracle/oracle/validators/controller.py index efb970d..d3800b8 100644 --- a/oracle/oracle/validators/controller.py +++ b/oracle/oracle/validators/controller.py @@ -1,26 +1,15 @@ import logging from eth_account.signers.local import LocalAccount -from eth_typing import BlockNumber, HexStr +from eth_typing import HexStr from web3 import Web3 from web3.types import Wei -from oracle.common.settings import ( - FINALIZE_VALIDATOR_VOTE_FILENAME, - INIT_VALIDATOR_VOTE_FILENAME, -) +from oracle.common.settings import VALIDATOR_VOTE_FILENAME -from ..eth1 import submit_vote -from .eth1 import ( - can_finalize_validator, - get_finalize_validator_deposit_data, - select_validator, -) -from .types import ( - FinalizeValidatorVotingParameters, - InitializeValidatorVotingParameters, - ValidatorVote, -) +from ..eth1 import get_latest_block, submit_vote +from .eth1 import get_validators_count, get_voting_parameters, select_validator +from .types import ValidatorVote logger = logging.getLogger(__name__) w3 = Web3() @@ -32,109 +21,59 @@ class ValidatorsController(object): def __init__(self, oracle: LocalAccount) -> None: self.validator_deposit: Wei = Web3.toWei(32, "ether") self.last_vote_public_key = None - self.last_finalized_public_key = None + self.last_vote_validators_count = None self.oracle = oracle - async def initialize( - self, - voting_params: InitializeValidatorVotingParameters, - current_block_number: BlockNumber, - ) -> None: - """Decides on the operator to host the next validator and submits the vote to the IPFS.""" + async def process(self) -> None: + """Process validators registration.""" + latest_block_number = (await get_latest_block())["block_number"] + voting_params = await get_voting_parameters() pool_balance = voting_params["pool_balance"] if pool_balance < self.validator_deposit: - # not enough balance to initiate next validator - return - - if voting_params["finalizing_validator"]: - logger.info( - "Waiting for the current validator to finalize before voting for the next" - ) + # not enough balance to register next validator return # select next validator # TODO: implement scoring system based on the operators performance - validator_deposit_data = await select_validator(current_block_number) + validator_deposit_data = await select_validator(latest_block_number) if validator_deposit_data is None: - logger.warning("Failed to find the next validator to initialize") + logger.warning("Failed to find the next validator to register") return + validators_count = await get_validators_count(latest_block_number) public_key = validator_deposit_data["public_key"] - if self.last_vote_public_key == public_key: - # already voted for the validator initialization + if ( + self.last_vote_validators_count == validators_count + and self.last_vote_public_key == public_key + ): + # already voted for the validator return # submit vote current_nonce = voting_params["validators_nonce"] operator = validator_deposit_data["operator"] encoded_data: bytes = w3.codec.encode_abi( - ["uint256", "bytes", "address"], - [current_nonce, public_key, operator], + ["uint256", "bytes", "address", "bytes32"], + [current_nonce, public_key, operator, validators_count], ) vote = ValidatorVote( - signature=HexStr(""), nonce=current_nonce, **validator_deposit_data + signature=HexStr(""), + nonce=current_nonce, + validators_count=validators_count, + **validator_deposit_data, ) logger.info( - f"Voting for the next validator initialization: operator={operator}, public key={public_key}" + f"Voting for the next validator: operator={operator}, public key={public_key}" ) submit_vote( oracle=self.oracle, encoded_data=encoded_data, vote=vote, - name=INIT_VALIDATOR_VOTE_FILENAME, + name=VALIDATOR_VOTE_FILENAME, ) - logger.info("Submitted validator initialization vote") + logger.info("Submitted validator registration vote") - # skip voting for the same validator in the next check + # skip voting for the same validator and validators count in the next check self.last_vote_public_key = public_key - - async def finalize( - self, - voting_params: FinalizeValidatorVotingParameters, - current_block_number: BlockNumber, - ) -> None: - """Decides on the operator to host the next validator and submits the vote to the IPFS.""" - current_public_key = voting_params["public_key"] - if current_public_key in (None, self.last_finalized_public_key): - # already voted for the validator with the current public key or no validator to finalize - return - - can_finalize = await can_finalize_validator( - block_number=current_block_number, - public_key=current_public_key, - ) - if not can_finalize: - logger.warning( - f"Cannot finalize validator registration: public key={current_public_key}" - ) - self.last_finalized_public_key = current_public_key - return - - # submit vote - current_nonce = voting_params["validators_nonce"] - operator = voting_params["operator"] - encoded_data: bytes = w3.codec.encode_abi( - ["uint256", "bytes", "address"], - [current_nonce, current_public_key, operator], - ) - validator_deposit_data = await get_finalize_validator_deposit_data( - block_number=current_block_number, operator_address=operator - ) - vote = ValidatorVote( - signature=HexStr(""), nonce=current_nonce, **validator_deposit_data - ) - logger.info( - f"Voting for the next validator finalization: operator={operator}, public key={current_public_key}" - ) - - submit_vote( - oracle=self.oracle, - encoded_data=encoded_data, - vote=vote, - name=FINALIZE_VALIDATOR_VOTE_FILENAME, - ) - logger.info("Submitted validator finalization vote") - - # skip voting for the same validator in the next check - self.last_finalized_public_key = current_public_key + self.last_vote_validators_count = validators_count diff --git a/oracle/oracle/validators/eth1.py b/oracle/oracle/validators/eth1.py index 0aafb01..f649c4a 100644 --- a/oracle/oracle/validators/eth1.py +++ b/oracle/oracle/validators/eth1.py @@ -1,9 +1,9 @@ from typing import Dict, Union import backoff -from eth_typing import ChecksumAddress, HexStr +from eth_typing import HexStr from web3 import Web3 -from web3.types import BlockNumber +from web3.types import BlockNumber, Wei from oracle.oracle.clients import ( execute_ethereum_gql_query, @@ -11,33 +11,45 @@ ipfs_fetch, ) from oracle.oracle.graphql_queries import ( - FINALIZE_OPERATOR_QUERY, - INITIALIZE_OPERATORS_QUERY, + OPERATORS_QUERY, + VALIDATOR_REGISTRATIONS_LATEST_INDEX_QUERY, VALIDATOR_REGISTRATIONS_QUERY, + VALIDATOR_VOTING_PARAMETERS_QUERY, ) -from oracle.oracle.settings import WITHDRAWAL_CREDENTIALS -from .types import ValidatorDepositData +from .types import ValidatorDepositData, ValidatorVotingParameters -INITIALIZE_DEPOSIT = Web3.toWei(1, "ether") + +@backoff.on_exception(backoff.expo, Exception, max_time=900) +async def get_voting_parameters(block_number: BlockNumber) -> ValidatorVotingParameters: + """Fetches validator voting parameters.""" + result: Dict = await execute_sw_gql_query( + query=VALIDATOR_VOTING_PARAMETERS_QUERY, + variables=dict( + block_number=block_number, + ), + ) + network = result["networks"][0] + pool = result["pools"][0] + return ValidatorVotingParameters( + validators_nonce=int(network["oraclesValidatorsNonce"]), + pool_balance=Wei(int(pool["balance"])), + ) @backoff.on_exception(backoff.expo, Exception, max_time=900) async def select_validator( block_number: BlockNumber, ) -> Union[None, ValidatorDepositData]: - """Selects operator to initiate validator registration for.""" + """Selects the next validator to register.""" result: Dict = await execute_sw_gql_query( - query=INITIALIZE_OPERATORS_QUERY, - variables=dict( - block_number=block_number, - min_collateral=str(INITIALIZE_DEPOSIT), - ), + query=OPERATORS_QUERY, + variables=dict(block_number=block_number), ) operators = result["operators"] for operator in operators: - merkle_proofs = operator["initializeMerkleProofs"] - if not merkle_proofs or int(operator["collateral"]) < INITIALIZE_DEPOSIT: + merkle_proofs = operator["depositDataMerkleProofs"] + if not merkle_proofs: continue operator_address = Web3.toChecksumAddress(operator["id"]) @@ -49,19 +61,19 @@ async def select_validator( continue selected_deposit_data = deposit_datum[deposit_data_index] - can_initialize = await can_initialize_validator( + can_register = await can_register_validator( block_number, selected_deposit_data["public_key"] ) - while deposit_data_index < max_deposit_data_index and not can_initialize: - # the edge case when the validator was finalized in previous merkle root + while deposit_data_index < max_deposit_data_index and not can_register: + # the edge case when the validator was registered in previous merkle root # and the deposit data is presented in the same. deposit_data_index += 1 selected_deposit_data = deposit_datum[deposit_data_index] - can_initialize = await can_initialize_validator( + can_register = await can_register_validator( block_number, selected_deposit_data["public_key"] ) - if can_initialize: + if can_register: return ValidatorDepositData( operator=operator_address, public_key=selected_deposit_data["public_key"], @@ -73,38 +85,8 @@ async def select_validator( @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def get_finalize_validator_deposit_data( - block_number: BlockNumber, operator_address: ChecksumAddress -) -> ValidatorDepositData: - """Fetches finalize deposit data for the operator validator.""" - result: Dict = await execute_sw_gql_query( - query=FINALIZE_OPERATOR_QUERY, - variables=dict( - block_number=block_number, - operator=operator_address.lower(), - ), - ) - operator = result["operators"][0] - merkle_proofs = operator["finalizeMerkleProofs"] - deposit_data_index = int(operator["depositDataIndex"]) - deposit_datum = await ipfs_fetch(merkle_proofs) - selected_deposit_data = deposit_datum[deposit_data_index] - - return ValidatorDepositData( - operator=operator_address, - public_key=selected_deposit_data["public_key"], - withdrawal_credentials=selected_deposit_data["withdrawal_credentials"], - deposit_data_root=selected_deposit_data["deposit_data_root"], - deposit_data_signature=selected_deposit_data["signature"], - proof=selected_deposit_data["proof"], - ) - - -@backoff.on_exception(backoff.expo, Exception, max_time=900) -async def can_initialize_validator( - block_number: BlockNumber, public_key: HexStr -) -> bool: - """Checks whether it's safe to initialize the validator registration.""" +async def can_register_validator(block_number: BlockNumber, public_key: HexStr) -> bool: + """Checks whether it's safe to register the validator.""" result: Dict = await execute_ethereum_gql_query( query=VALIDATOR_REGISTRATIONS_QUERY, variables=dict(block_number=block_number, public_key=public_key), @@ -115,14 +97,19 @@ async def can_initialize_validator( @backoff.on_exception(backoff.expo, Exception, max_time=900) -async def can_finalize_validator(block_number: BlockNumber, public_key: HexStr) -> bool: - """Checks whether it's safe to finalize the validator registration.""" +async def get_validators_count(block_number: BlockNumber) -> HexStr: + """Fetches validators count for protecting against operator submitting deposit prior to registration.""" result: Dict = await execute_ethereum_gql_query( - query=VALIDATOR_REGISTRATIONS_QUERY, - variables=dict(block_number=block_number, public_key=public_key), + query=VALIDATOR_REGISTRATIONS_LATEST_INDEX_QUERY, + variables=dict(block_number=block_number), ) registrations = result["validatorRegistrations"] - if len(registrations) != 1 or registrations[0]["id"] != public_key: - return False + if not registrations: + validators_count = int.to_bytes(1, 8, byteorder="little") + else: + index = int.from_bytes( + Web3.toBytes(hexstr=registrations[0]["index"]), byteorder="little" + ) + validators_count = int.to_bytes(index + 1, 8, byteorder="little") - return registrations[0]["withdrawalCredentials"] == WITHDRAWAL_CREDENTIALS + return Web3.toHex(Web3.keccak(validators_count)) diff --git a/oracle/oracle/validators/types.py b/oracle/oracle/validators/types.py index 99f5607..c5f8560 100644 --- a/oracle/oracle/validators/types.py +++ b/oracle/oracle/validators/types.py @@ -1,22 +1,14 @@ -from typing import List, TypedDict, Union +from typing import List, TypedDict from eth_typing import ChecksumAddress, HexStr from web3.types import Wei -class InitializeValidatorVotingParameters(TypedDict): - validator_index: int +class ValidatorVotingParameters(TypedDict): validators_nonce: int - finalizing_validator: bool pool_balance: Wei -class FinalizeValidatorVotingParameters(TypedDict): - validators_nonce: int - operator: Union[ChecksumAddress, None] - public_key: Union[HexStr, None] - - class MerkleDepositData(TypedDict): public_key: HexStr signature: HexStr @@ -37,4 +29,5 @@ class ValidatorDepositData(TypedDict): class ValidatorVote(ValidatorDepositData): nonce: int + validators_count: HexStr signature: HexStr