diff --git a/eth2near/contract_wrapper/src/dao_eth_client_contract.rs b/eth2near/contract_wrapper/src/dao_eth_client_contract.rs index 6feb5b050..164919cac 100644 --- a/eth2near/contract_wrapper/src/dao_eth_client_contract.rs +++ b/eth2near/contract_wrapper/src/dao_eth_client_contract.rs @@ -224,7 +224,9 @@ mod tests { finalized_beacon_header, current_sync_committee, next_sync_committee, - signer_account_id.clone() + 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 e754d6c9c..01726256d 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,9 +35,11 @@ impl EthClientContract { finalized_beacon_header: ExtendedBeaconBlockHeader, current_sync_committee: SyncCommittee, next_sync_committee: SyncCommittee, - trusted_signer: String, + 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, @@ -59,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::::Some(trusted_signer.parse().unwrap()), + 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(), @@ -193,6 +200,7 @@ mod tests { use crate::sandbox_contract_wrapper::SandboxContractWrapper; use eth_types::eth2::{ExtendedBeaconBlockHeader, LightClientUpdate, SyncCommittee}; use eth_types::BlockHeader; + use near_primitives::types::AccountId; use tokio::runtime::Runtime; // TODO: use a more clean approach to include binary @@ -323,7 +331,9 @@ mod tests { finalized_beacon_header, current_sync_committee, next_sync_committee, - trusted_signer, + None, + None, + Option::::Some(trusted_signer.parse().unwrap()), ); eth_state.current_light_client_update = 1; } diff --git a/eth2near/eth2near-block-relay-rs/Cargo.lock b/eth2near/eth2near-block-relay-rs/Cargo.lock index 1d1a14f0a..28388fa11 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 32ec3cd66..40eecafd9 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 63d74caa9..cd556486a 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/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 9d43fee50..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,19 +83,45 @@ 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.signer_account_id.clone(), + 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/lib.rs b/eth2near/eth2near-block-relay-rs/src/lib.rs index 8dc1e5c62..7a8684822 100644 --- a/eth2near/eth2near-block-relay-rs/src/lib.rs +++ b/eth2near/eth2near-block-relay-rs/src/lib.rs @@ -9,13 +9,14 @@ 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; #[cfg(test)] pub mod config_for_tests; -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 2c642dd3c..61f159d46 100644 --- a/eth2near/eth2near-block-relay-rs/src/test_utils.rs +++ b/eth2near/eth2near-block-relay-rs/src/test_utils.rs @@ -77,7 +77,9 @@ pub fn init_contract_from_files( finalized_beacon_header, current_sync_committee, next_sync_committee, - eth_client_contract.get_signature_account_id().to_string(), + None, + None, + Some(eth_client_contract.get_signer_account_id()), ); thread::sleep(time::Duration::from_secs(30)); } @@ -149,7 +151,9 @@ pub fn init_contract_from_specific_slot( finalized_beacon_header, current_sync_committee, next_sync_committee, - eth_client_contract.get_signature_account_id().to_string(), + None, + None, + Some(eth_client_contract.get_signer_account_id()), ); thread::sleep(time::Duration::from_secs(30)); @@ -191,6 +195,8 @@ fn get_config(config_for_test: &ConfigForTests) -> Config { state_requests_timeout_seconds: 1000, sleep_time_on_sync_secs: 0, sleep_time_after_submission_secs: 5, + hashes_gc_threshold: None, + max_submitted_blocks_by_account: None, } } @@ -208,7 +214,7 @@ pub fn get_client_contract( match from_file { true => test_utils::init_contract_from_files(&mut eth_client_contract, config_for_test), - 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)