From c55f79ace1c2932199d903a6ad54890dda78974a Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 22 Sep 2022 02:40:51 +0100 Subject: [PATCH 1/2] Eth2-to-Near-relay: improve client initialization (#819) * Improve init of the Eth2Client contract * Move the default init to the client wrapper method * Fix tests * Print init eth2 client input Co-authored-by: Kirill --- .../src/dao_eth_client_contract.rs | 3 +++ .../src/eth_client_contract.rs | 23 ++++++++++++++----- .../eth2near-block-relay-rs/src/config.rs | 9 ++++++++ .../src/init_contract.rs | 10 ++++++++ .../eth2near-block-relay-rs/src/test_utils.rs | 8 +++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/eth2near/contract_wrapper/src/dao_eth_client_contract.rs b/eth2near/contract_wrapper/src/dao_eth_client_contract.rs index c26e3b1da..164919cac 100644 --- a/eth2near/contract_wrapper/src/dao_eth_client_contract.rs +++ b/eth2near/contract_wrapper/src/dao_eth_client_contract.rs @@ -224,6 +224,9 @@ mod tests { finalized_beacon_header, current_sync_committee, next_sync_committee, + None, + None, + Some(eth_client.contract_wrapper.get_signer_account_id()), ); let dao_contract_wrapper = diff --git a/eth2near/contract_wrapper/src/eth_client_contract.rs b/eth2near/contract_wrapper/src/eth_client_contract.rs index 20144af4c..3e8c550b4 100644 --- a/eth2near/contract_wrapper/src/eth_client_contract.rs +++ b/eth2near/contract_wrapper/src/eth_client_contract.rs @@ -14,10 +14,10 @@ use std::error::Error; use std::option::Option; use std::string::String; use std::vec::Vec; - +use serde::Serialize; pub struct EthClientContract { last_slot: u64, - contract_wrapper: Box, + pub contract_wrapper: Box, } impl EthClientContract { @@ -35,8 +35,11 @@ impl EthClientContract { finalized_beacon_header: ExtendedBeaconBlockHeader, current_sync_committee: SyncCommittee, next_sync_committee: SyncCommittee, + hashes_gc_threshold: Option, + max_submitted_blocks_by_account: Option, + trusted_signer: Option, ) { - #[derive(BorshSerialize)] + #[derive(BorshSerialize, Serialize)] pub struct InitInput { pub network: String, pub finalized_execution_header: eth_types::BlockHeader, @@ -58,11 +61,16 @@ impl EthClientContract { next_sync_committee, validate_updates: true, verify_bls_signatures: false, - hashes_gc_threshold: 51000, - max_submitted_blocks_by_account: 8000, - trusted_signer: Option::::None, + hashes_gc_threshold: hashes_gc_threshold.unwrap_or(51_000), + max_submitted_blocks_by_account: max_submitted_blocks_by_account.unwrap_or(8000), + trusted_signer, }; + println!( + "Init eth2 client input: \n {}", + serde_json::to_string_pretty(&init_input).unwrap() + ); + self.contract_wrapper .call_change_method( "init".to_string(), @@ -319,6 +327,9 @@ mod tests { finalized_beacon_header, current_sync_committee, next_sync_committee, + None, + None, + None, ); eth_state.current_light_client_update = 1; } diff --git a/eth2near/eth2near-block-relay-rs/src/config.rs b/eth2near/eth2near-block-relay-rs/src/config.rs index 49153c33a..01319232f 100644 --- a/eth2near/eth2near-block-relay-rs/src/config.rs +++ b/eth2near/eth2near-block-relay-rs/src/config.rs @@ -70,6 +70,15 @@ pub struct Config { // Sleep time in seconds after blocks/light_client_update submission to client pub sleep_time_after_submission_secs: u64, + + /// Max number of stored blocks in the storage of the eth2 client contract. + /// Events that happen past this threshold cannot be verified by the client. + /// It is used on initialization of the Eth2 client. + pub hashes_gc_threshold: Option, + + /// Max number of unfinalized blocks allowed to be stored by one submitter account. + /// It is used on initialization of the Eth2 client. + pub max_submitted_blocks_by_account: Option, } impl Config { diff --git a/eth2near/eth2near-block-relay-rs/src/init_contract.rs b/eth2near/eth2near-block-relay-rs/src/init_contract.rs index 11c75997a..c6c58c07b 100644 --- a/eth2near/eth2near-block-relay-rs/src/init_contract.rs +++ b/eth2near/eth2near-block-relay-rs/src/init_contract.rs @@ -65,6 +65,16 @@ pub fn init_contract( finalized_header, current_sync_committee, next_sync_committee, + config.hashes_gc_threshold, + config.max_submitted_blocks_by_account, + Some( + config + .dao_contract_account_id + .as_ref() + .unwrap_or(&config.signer_account_id) + .parse() + .unwrap(), + ), ); thread::sleep(time::Duration::from_secs(30)); diff --git a/eth2near/eth2near-block-relay-rs/src/test_utils.rs b/eth2near/eth2near-block-relay-rs/src/test_utils.rs index 35791faca..72259ce34 100644 --- a/eth2near/eth2near-block-relay-rs/src/test_utils.rs +++ b/eth2near/eth2near-block-relay-rs/src/test_utils.rs @@ -78,6 +78,9 @@ pub fn init_contract_from_files(eth_client_contract: &mut EthClientContract) { finalized_beacon_header, current_sync_committee, next_sync_committee, + None, + None, + Some(eth_client_contract.contract_wrapper.get_signer_account_id()), ); thread::sleep(time::Duration::from_secs(30)); } @@ -154,6 +157,9 @@ pub fn init_contract_from_specific_slot( finalized_beacon_header, current_sync_committee, next_sync_committee, + None, + None, + Some(eth_client_contract.contract_wrapper.get_signer_account_id()), ); thread::sleep(time::Duration::from_secs(30)); @@ -207,6 +213,8 @@ fn get_config() -> Config { state_requests_timeout_seconds: 1000, sleep_time_after_submission_secs: 5, sleep_time_on_sync_secs: 30, + hashes_gc_threshold: None, + max_submitted_blocks_by_account: None, } } From 5a5cd6539a0a8a6c7e05f515c97502269a5d41ad Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 22 Sep 2022 04:40:56 +0100 Subject: [PATCH 2/2] 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);