From 5a5cd6539a0a8a6c7e05f515c97502269a5d41ad Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 22 Sep 2022 04:40:56 +0100 Subject: [PATCH] Eth2-to-Near-relay: improve init of Eth2 client contract (#817) * Improve init eth2 client contract * separate LightClientSnapshotWithProof * get init block root if not provided * fix code to get the checkpoint Co-authored-by: Olga Kunyavskaya Co-authored-by: Kirill --- eth2near/eth2near-block-relay-rs/Cargo.lock | 1 + eth2near/eth2near-block-relay-rs/Cargo.toml | 1 + .../src/beacon_rpc_client.rs | 40 +++++++++ .../src/init_contract.rs | 81 +++++++++++++++---- eth2near/eth2near-block-relay-rs/src/lib.rs | 3 +- .../src/light_client_snapshot_with_proof.rs | 10 +++ eth2near/eth2near-block-relay-rs/src/main.rs | 7 +- .../eth2near-block-relay-rs/src/test_utils.rs | 12 +-- 8 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 eth2near/eth2near-block-relay-rs/src/light_client_snapshot_with_proof.rs diff --git a/eth2near/eth2near-block-relay-rs/Cargo.lock b/eth2near/eth2near-block-relay-rs/Cargo.lock index 76f091e00..d9bd2cce3 100644 --- a/eth2near/eth2near-block-relay-rs/Cargo.lock +++ b/eth2near/eth2near-block-relay-rs/Cargo.lock @@ -1215,6 +1215,7 @@ dependencies = [ "contract_wrapper", "env_logger", "eth-types", + "eth2-utility", "eth2_hashing", "ethereum-types", "finality-update-verify", diff --git a/eth2near/eth2near-block-relay-rs/Cargo.toml b/eth2near/eth2near-block-relay-rs/Cargo.toml index 27cb334dc..cc27191c8 100644 --- a/eth2near/eth2near-block-relay-rs/Cargo.toml +++ b/eth2near/eth2near-block-relay-rs/Cargo.toml @@ -24,6 +24,7 @@ futures = { version = "0.3.21", default-features = false } async-std = "1.12.0" hex = "*" toml = "0.5.9" +eth2-utility = { path = "../../contracts/near/eth2-utility" } finality-update-verify = { path = "../finality-update-verify" } atomic_refcell = "0.1.8" bitvec = "*" diff --git a/eth2near/eth2near-block-relay-rs/src/beacon_rpc_client.rs b/eth2near/eth2near-block-relay-rs/src/beacon_rpc_client.rs index d1288895d..f7598cce6 100644 --- a/eth2near/eth2near-block-relay-rs/src/beacon_rpc_client.rs +++ b/eth2near/eth2near-block-relay-rs/src/beacon_rpc_client.rs @@ -1,4 +1,5 @@ use crate::execution_block_proof::ExecutionBlockProof; +use crate::light_client_snapshot_with_proof::LightClientSnapshotWithProof; use crate::relay_errors::{ ExecutionPayloadError, FailOnGettingJson, MissSyncAggregationError, NoBlockForSlotError, SignatureSlotNotFoundError, @@ -37,6 +38,7 @@ impl BeaconRPCClient { const URL_GET_LIGHT_CLIENT_UPDATE_API: &'static str = "eth/v1/beacon/light_client/updates"; const URL_FINALITY_LIGHT_CLIENT_UPDATE_PATH: &'static str = "eth/v1/beacon/light_client/finality_update/"; + const URL_GET_BOOTSTRAP: &'static str = "eth/v1/beacon/light_client/bootstrap"; const URL_STATE_PATH: &'static str = "eth/v2/debug/beacon/states"; const SLOTS_PER_EPOCH: u64 = 32; @@ -139,6 +141,44 @@ impl BeaconRPCClient { }) } + // Fetch a bootstrapping state with a proof to a trusted block root. + // The trusted block root should be fetched with similar means to a weak subjectivity checkpoint. + // Only block roots for checkpoints are guaranteed to be available. + pub fn get_bootstrap( + &self, + block_root: String, + ) -> Result> { + let url = format!( + "{}/{}/{}", + self.endpoint_url, + Self::URL_GET_BOOTSTRAP, + block_root + ); + + let light_client_snapshot_json_str = self.get_json_from_raw_request(&url)?; + let parsed_json: Value = serde_json::from_str(&light_client_snapshot_json_str)?; + let beacon_header: BeaconBlockHeader = + serde_json::from_value(parsed_json["data"]["header"].clone())?; + let current_sync_committee: SyncCommittee = + serde_json::from_value(parsed_json["data"]["current_sync_committee"].clone())?; + let current_sync_committee_branch: Vec = + serde_json::from_value(parsed_json["data"]["current_sync_committee_branch"].clone())?; + + Ok(LightClientSnapshotWithProof { + beacon_header, + current_sync_committee, + current_sync_committee_branch, + }) + } + + pub fn get_checkpoint_root(&self) -> Result> { + let url = format!("{}/eth/v1/beacon/states/finalized/finality_checkpoints", self.endpoint_url); + let checkpoint_json_str = self.get_json_from_raw_request(&url)?; + let parsed_json: Value = serde_json::from_str(&checkpoint_json_str)?; + + Ok(trim_quotes(parsed_json["data"]["finalized"]["root"].to_string())) + } + /// Return the last finalized slot in the Beacon chain pub fn get_last_finalized_slot_number(&self) -> Result> { Ok(self.get_beacon_block_header_for_block_id("finalized")?.slot) diff --git a/eth2near/eth2near-block-relay-rs/src/init_contract.rs b/eth2near/eth2near-block-relay-rs/src/init_contract.rs index c6c58c07b..e7c1d7829 100644 --- a/eth2near/eth2near-block-relay-rs/src/init_contract.rs +++ b/eth2near/eth2near-block-relay-rs/src/init_contract.rs @@ -1,15 +1,48 @@ use crate::beacon_rpc_client::BeaconRPCClient; use crate::config::Config; use crate::eth1_rpc_client::Eth1RPCClient; +use crate::light_client_snapshot_with_proof::LightClientSnapshotWithProof; use contract_wrapper::eth_client_contract::EthClientContract; +use eth2_utility::consensus::{convert_branch, floorlog2, get_subtree_index}; use eth_types::eth2::ExtendedBeaconBlockHeader; use eth_types::BlockHeader; use log::info; use std::{thread, time}; +use tree_hash::TreeHash; + +const CURRENT_SYNC_COMMITTEE_INDEX: u32 = 54; +const CURRENT_SYNC_COMMITTEE_TREE_DEPTH: u32 = floorlog2(CURRENT_SYNC_COMMITTEE_INDEX); +const CURRENT_SYNC_COMMITTEE_TREE_INDEX: u32 = get_subtree_index(CURRENT_SYNC_COMMITTEE_INDEX); + +pub fn verify_light_client_snapshot( + block_root: String, + light_client_snapshot: &LightClientSnapshotWithProof, +) -> bool { + let expected_block_root = format!( + "{:#x}", + light_client_snapshot.beacon_header.tree_hash_root() + ); + + if block_root != expected_block_root { + return false; + } + + let branch = convert_branch(&light_client_snapshot.current_sync_committee_branch); + merkle_proof::verify_merkle_proof( + light_client_snapshot + .current_sync_committee + .tree_hash_root(), + &branch, + CURRENT_SYNC_COMMITTEE_TREE_DEPTH.try_into().unwrap(), + CURRENT_SYNC_COMMITTEE_TREE_INDEX.try_into().unwrap(), + light_client_snapshot.beacon_header.state_root.0, + ) +} pub fn init_contract( config: &Config, eth_client_contract: &mut EthClientContract, + mut init_block_root: String, ) -> Result<(), Box> { info!(target: "relay", "=== Contract initialization ==="); @@ -18,22 +51,19 @@ pub fn init_contract( config.eth_requests_timeout_seconds, config.state_requests_timeout_seconds, ); - let eth1_rpc_client = Eth1RPCClient::new(&config.eth1_endpoint); - let start_slot = beacon_rpc_client.get_last_finalized_slot_number().unwrap(); - let period = BeaconRPCClient::get_period_for_slot(start_slot.as_u64()); + let eth1_rpc_client = Eth1RPCClient::new(&config.eth1_endpoint); let light_client_update = beacon_rpc_client .get_finality_light_client_update_with_sync_commity_update() .unwrap(); - let block_id = format!( - "{}", - light_client_update - .finality_update - .header_update - .beacon_header - .slot - ); + let finality_slot = light_client_update + .finality_update + .header_update + .beacon_header + .slot; + + let block_id = format!("{}", finality_slot); let finalized_header: ExtendedBeaconBlockHeader = ExtendedBeaconBlockHeader::from(light_client_update.finality_update.header_update); let finalized_body = beacon_rpc_client @@ -53,17 +83,34 @@ pub fn init_contract( .sync_committee_update .unwrap() .next_sync_committee; - let prev_light_client_update = beacon_rpc_client.get_light_client_update(period - 1)?; - let current_sync_committee = prev_light_client_update - .sync_committee_update - .unwrap() - .next_sync_committee; + + if init_block_root.is_empty() { + init_block_root = beacon_rpc_client + .get_checkpoint_root() + .expect("Fail to get last checkpoint"); + } + + let light_client_snapshot = beacon_rpc_client + .get_bootstrap(init_block_root.clone()) + .expect("Unable to fetch bootstrap state"); + + info!(target: "relay", "init_block_root: {}", init_block_root); + + if BeaconRPCClient::get_period_for_slot(light_client_snapshot.beacon_header.slot) + != BeaconRPCClient::get_period_for_slot(finality_slot) + { + panic!("Period for init_block_root different from current period. Please use snapshot for current period"); + } + + if !verify_light_client_snapshot(init_block_root, &light_client_snapshot) { + return Err("Invalid light client snapshot".into()); + } eth_client_contract.init_contract( config.network.to_string(), finalized_execution_header, finalized_header, - current_sync_committee, + light_client_snapshot.current_sync_committee, next_sync_committee, config.hashes_gc_threshold, config.max_submitted_blocks_by_account, diff --git a/eth2near/eth2near-block-relay-rs/src/lib.rs b/eth2near/eth2near-block-relay-rs/src/lib.rs index f3636c5fb..36290b921 100644 --- a/eth2near/eth2near-block-relay-rs/src/lib.rs +++ b/eth2near/eth2near-block-relay-rs/src/lib.rs @@ -7,10 +7,11 @@ pub mod execution_block_proof; pub mod hand_made_finality_light_client_update; pub mod init_contract; pub mod last_slot_searcher; +pub mod light_client_snapshot_with_proof; pub mod logger; pub mod near_rpc_client; +pub mod prometheus_metrics; pub mod relay_errors; -pub mod prometheus_metrics; #[cfg(test)] pub mod test_utils; diff --git a/eth2near/eth2near-block-relay-rs/src/light_client_snapshot_with_proof.rs b/eth2near/eth2near-block-relay-rs/src/light_client_snapshot_with_proof.rs new file mode 100644 index 000000000..d939cea47 --- /dev/null +++ b/eth2near/eth2near-block-relay-rs/src/light_client_snapshot_with_proof.rs @@ -0,0 +1,10 @@ +use eth_types::eth2::{BeaconBlockHeader, SyncCommittee}; +use eth_types::H256; +use serde::Serialize; + +#[derive(Serialize)] +pub struct LightClientSnapshotWithProof { + pub beacon_header: BeaconBlockHeader, + pub current_sync_committee: SyncCommittee, + pub current_sync_committee_branch: Vec, +} diff --git a/eth2near/eth2near-block-relay-rs/src/main.rs b/eth2near/eth2near-block-relay-rs/src/main.rs index eb475d0bf..cbea59b96 100644 --- a/eth2near/eth2near-block-relay-rs/src/main.rs +++ b/eth2near/eth2near-block-relay-rs/src/main.rs @@ -24,6 +24,11 @@ struct Arguments { /// The eth contract on Near will be initialized init_contract: bool, + #[clap(long, action = ArgAction::Set, default_value = "")] + /// The trusted block root for checkpoint for contract initialization + /// e.g.: --init-contract --init-block-root 0x9cd0c5a8392d0659426b12384e8440c147510ab93eeaeccb08435a462d7bb1c7 + init_block_root: String, + #[clap(long, default_value_t = String::from("info"))] /// Log level (trace, debug, info, warn, error) log_level: String, @@ -101,7 +106,7 @@ fn main() -> Result<(), Box> { if args.init_contract { let mut eth_client_contract = EthClientContract::new(get_eth_contract_wrapper(&config)); - init_contract(&config, &mut eth_client_contract).unwrap(); + init_contract(&config, &mut eth_client_contract, args.init_block_root).unwrap(); } else { let mut eth2near_relay = Eth2NearRelay::init( &config, diff --git a/eth2near/eth2near-block-relay-rs/src/test_utils.rs b/eth2near/eth2near-block-relay-rs/src/test_utils.rs index 72259ce34..cf6c35fb0 100644 --- a/eth2near/eth2near-block-relay-rs/src/test_utils.rs +++ b/eth2near/eth2near-block-relay-rs/src/test_utils.rs @@ -220,16 +220,13 @@ fn get_config() -> Config { pub fn get_client_contract(from_file: bool) -> Box { let (relay_account, contract) = create_contract(); - let contract_wrapper = Box::new(SandboxContractWrapper::new( - &relay_account, - contract, - )); + let contract_wrapper = Box::new(SandboxContractWrapper::new(&relay_account, contract)); let mut eth_client_contract = EthClientContract::new(contract_wrapper); let config = get_config(); match from_file { true => test_utils::init_contract_from_files(&mut eth_client_contract), - false => init_contract(&config, &mut eth_client_contract).unwrap(), + false => init_contract(&config, &mut eth_client_contract, "".to_string()).unwrap(), }; Box::new(eth_client_contract) @@ -271,10 +268,7 @@ pub fn get_relay_from_slot(enable_binsearch: bool, slot: u64) -> Eth2NearRelay { let config = get_config(); let (relay_account, contract) = create_contract(); - let contract_wrapper = Box::new(SandboxContractWrapper::new( - &relay_account, - contract, - )); + let contract_wrapper = Box::new(SandboxContractWrapper::new(&relay_account, contract)); let mut eth_client_contract = EthClientContract::new(contract_wrapper); init_contract_from_specific_slot(&mut eth_client_contract, slot);