diff --git a/Cargo.lock b/Cargo.lock index 477d3cd9..249d584b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9862,6 +9862,11 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" name = "relayer" version = "0.1.0" dependencies = [ + "alloy", + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", "anyhow", "ark-serialize 0.4.2", "axum", @@ -9872,6 +9877,7 @@ dependencies = [ "clap", "derive_more", "dotenv", + "erc20-relay-client", "ethereum-client", "futures", "gclient 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index 7959cced..83a12f3d 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -10,6 +10,11 @@ gear_proof_storage.workspace = true gear-rpc-client.workspace = true prover.workspace = true +alloy.workspace = true +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-primitives.workspace = true +alloy-rlp.workspace = true anyhow.workspace = true ark-serialize = { workspace = true, features = ["std"] } axum.workspace = true @@ -18,6 +23,7 @@ checkpoint_light_client = { workspace = true, features = ["std"] } clap.workspace = true derive_more.workspace = true dotenv.workspace = true +erc20-relay-client.workspace = true futures.workspace = true gear-core.workspace = true gclient.workspace = true @@ -33,7 +39,7 @@ primitive-types = { workspace = true, features = ["std"] } prometheus.workspace = true rand.workspace = true reqwest.workspace = true -sails-rs.workspace = true +sails-rs = { workspace = true, features = ["gclient"] } serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/relayer/src/erc20/mod.rs b/relayer/src/erc20/mod.rs new file mode 100644 index 00000000..a75b14e0 --- /dev/null +++ b/relayer/src/erc20/mod.rs @@ -0,0 +1,338 @@ +use super::{ethereum_checkpoints::utils, *}; +use alloy::{ + network::primitives::BlockTransactionsKind, + providers::{Provider, ProviderBuilder}, + rpc::types::{Log, Receipt, ReceiptEnvelope, ReceiptWithBloom}, +}; +use alloy_consensus::TxType; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::Log as PrimitiveLog; +use alloy_rlp::Encodable; +use anyhow::{anyhow, Result as AnyResult}; +use checkpoint_light_client_io::ethereum_common::{ + beacon::{self, light::Block as LightBeaconBlock, Block as BeaconBlock}, + memory_db, + patricia_trie::{TrieDB, TrieDBMut}, + trie_db::{Recorder, Trie, TrieMut}, + utils as eth_utils, H256, SLOTS_PER_EPOCH, +}; +use erc20_relay_client::{ + traits::Erc20Relay as _, Block, BlockBody, BlockHeader, BlockInclusionProof, Erc20Relay, + EthToVaraEvent, ExecutionPayload, +}; +use futures::StreamExt; +use gclient::{GearApi, WSAddress}; +use reqwest::Client; +use sails_rs::{calls::*, events::*, gclient::calls::*, prelude::*}; +use std::cmp::Ordering; + +// TODO: remove, #152 +fn into_idl_execution_payload( + execution_payload: beacon::light::ExecutionPayload, +) -> ExecutionPayload { + use erc20_relay_client::{ + ByteList, BytesFixed1, BytesFixed2, FixedArray1ForU8, FixedArray2ForU8, ListForU8, + }; + + ExecutionPayload { + parent_hash: BytesFixed1(FixedArray1ForU8(execution_payload.parent_hash.0 .0)), + fee_recipient: BytesFixed2(FixedArray2ForU8(execution_payload.fee_recipient.0 .0)), + state_root: BytesFixed1(FixedArray1ForU8(execution_payload.state_root.0 .0)), + receipts_root: BytesFixed1(FixedArray1ForU8(execution_payload.receipts_root.0 .0)), + logs_bloom: execution_payload.logs_bloom, + prev_randao: BytesFixed1(FixedArray1ForU8(execution_payload.prev_randao.0 .0)), + block_number: execution_payload.block_number, + gas_limit: execution_payload.gas_limit, + gas_used: execution_payload.gas_used, + timestamp: execution_payload.timestamp, + extra_data: ByteList(ListForU8 { + data: execution_payload.extra_data.0.as_ref().into(), + }), + base_fee_per_gas: execution_payload.base_fee_per_gas, + block_hash: BytesFixed1(FixedArray1ForU8(execution_payload.block_hash.0 .0)), + transactions: execution_payload.transactions, + withdrawals: execution_payload.withdrawals, + blob_gas_used: execution_payload.blob_gas_used, + excess_blob_gas: execution_payload.excess_blob_gas, + } +} + +// TODO: remove, #152 +fn into_idl_block_body(block_body: beacon::light::BlockBody) -> BlockBody { + use erc20_relay_client::{BytesFixed1, FixedArray1ForU8}; + + BlockBody { + randao_reveal: block_body.randao_reveal, + eth1_data: block_body.eth1_data, + graffiti: BytesFixed1(FixedArray1ForU8(block_body.graffiti.0 .0)), + proposer_slashings: block_body.proposer_slashings, + attester_slashings: block_body.attester_slashings, + attestations: block_body.attestations, + deposits: block_body.deposits, + voluntary_exits: block_body.voluntary_exits, + sync_aggregate: block_body.sync_aggregate, + execution_payload: into_idl_execution_payload(block_body.execution_payload), + bls_to_execution_changes: block_body.bls_to_execution_changes, + blob_kzg_commitments: block_body.blob_kzg_commitments, + } +} + +// TODO: remove, #152 +fn into_idl_block(block: LightBeaconBlock) -> Block { + Block { + slot: block.slot, + proposer_index: block.proposer_index, + parent_root: block.parent_root, + state_root: block.state_root, + body: into_idl_block_body(block.body), + } +} + +// TODO: remove, #152 +fn into_idl_block_header(block_heaer: beacon::BlockHeader) -> BlockHeader { + BlockHeader { + slot: block_heaer.slot, + proposer_index: block_heaer.proposer_index, + parent_root: block_heaer.parent_root, + state_root: block_heaer.state_root, + body_root: block_heaer.body_root, + } +} + +pub async fn relay(args: RelayErc20Args) { + if let Err(e) = relay_inner(args).await { + log::error!("{e:?}"); + } +} + +async fn relay_inner(args: RelayErc20Args) -> AnyResult<()> { + log::info!("Started"); + + let RelayErc20Args { + program_id, + beacon_endpoint, + vara_domain, + vara_port, + vara_suri, + eth_endpoint, + tx_hash, + } = args; + + let program_id: [u8; 32] = + utils::try_from_hex_encoded(&program_id).expect("Expecting correct ProgramId"); + let tx_hash: [u8; 32] = + utils::try_from_hex_encoded(&tx_hash).expect("Expecting correct hash of a transaction"); + + let rpc_url = eth_endpoint.parse()?; + let provider = ProviderBuilder::new().on_http(rpc_url); + + let receipt = provider + .get_transaction_receipt(tx_hash.into()) + .await? + .ok_or(anyhow!("Transaction receipt is missing"))?; + + let block = match receipt.block_hash { + Some(hash) => provider + .get_block_by_hash(hash, BlockTransactionsKind::Hashes) + .await? + .ok_or(anyhow!("Ethereum block (hash) is missing"))?, + None => match receipt.block_number { + Some(number) => provider + .get_block_by_number(BlockNumberOrTag::Number(number), false) + .await? + .ok_or(anyhow!("Ethereum block (number) is missing"))?, + None => return Err(anyhow!("Unable to get Ethereum block")), + }, + }; + + let beacon_root_parent = block + .header + .parent_beacon_block_root + .ok_or(anyhow!("Unable to determine root of parent beacon block"))?; + let block_number = block + .header + .number + .ok_or(anyhow!("Unable to determine Ethereum block number"))?; + let client_http = Client::new(); + let proof_block = build_inclusion_proof( + &client_http, + &beacon_endpoint, + &beacon_root_parent, + block_number, + ) + .await?; + + // receipt Merkle-proof + let tx_index = receipt + .transaction_index + .ok_or(anyhow!("Unable to determine transaction index"))?; + let receipts = provider + .get_block_receipts(BlockNumberOrTag::Number(block_number)) + .await? + .unwrap_or_default() + .iter() + .map(|tx_receipt| { + let receipt = tx_receipt.as_ref(); + + tx_receipt + .transaction_index + .map(|i| (i, map_receipt_envelope(receipt))) + }) + .collect::>>() + .unwrap_or_default(); + + let mut memory_db = memory_db::new(); + let key_value_tuples = eth_utils::rlp_encode_receipts_and_nibble_tuples(&receipts[..]); + let root = { + let mut root = H256::zero(); + let mut triedbmut = TrieDBMut::new(&mut memory_db, &mut root); + for (key, value) in &key_value_tuples { + triedbmut.insert(key, value)?; + } + + *triedbmut.root() + }; + + let (tx_index, receipt) = receipts + .iter() + .find(|(index, _)| index == &tx_index) + .ok_or(anyhow!("Unable to find transaction's receipt"))?; + + let trie = TrieDB::new(&memory_db, &root)?; + let (key, _expected_value) = eth_utils::rlp_encode_index_and_receipt(tx_index, receipt); + + let mut recorder = Recorder::new(); + let _value = trie.get_with(&key, &mut recorder); + + let mut receipt_rlp = Vec::with_capacity(Encodable::length(receipt)); + Encodable::encode(receipt, &mut receipt_rlp); + let message = EthToVaraEvent { + proof_block, + proof: recorder + .drain() + .into_iter() + .map(|r| r.data) + .collect::>(), + transaction_index: *tx_index, + receipt_rlp, + }; + + let client = GearApi::init_with(WSAddress::new(vara_domain, vara_port), vara_suri).await?; + let gas_limit_block = client.block_gas_limit()?; + // use 95% of block gas limit for all extrinsics + let gas_limit = gas_limit_block / 100 * 95; + + let remoting = GClientRemoting::new(client); + + let mut erc20_service = Erc20Relay::new(remoting.clone()); + let mut listener = erc20_relay_client::erc_20_relay::events::listener(remoting.clone()); + let mut events = listener.listen().await.unwrap(); + + let result = erc20_service + .relay(message) + .with_gas_limit(gas_limit) + .send_recv(program_id.into()) + .await + .unwrap(); + + log::debug!("result = {result:?}"); + if result.is_ok() { + let event = events.next().await.unwrap(); + + log::debug!("event = {event:?}"); + } + + Ok(()) +} + +async fn build_inclusion_proof( + client_http: &Client, + rpc_url: &str, + beacon_root_parent: &[u8; 32], + block_number: u64, +) -> AnyResult { + let beacon_block_parent = + utils::get_block_by_hash(client_http, rpc_url, beacon_root_parent).await?; + + let beacon_block = LightBeaconBlock::from( + find_beacon_block(client_http, rpc_url, block_number, &beacon_block_parent).await?, + ); + + let slot = beacon_block.slot; + if slot % SLOTS_PER_EPOCH == 0 { + return Ok(BlockInclusionProof { + block: into_idl_block(beacon_block), + headers: vec![], + }); + } + + let epoch_next = 1 + eth_utils::calculate_epoch(beacon_block.slot); + let slot_checkpoint = epoch_next * SLOTS_PER_EPOCH; + + Ok(BlockInclusionProof { + block: into_idl_block(beacon_block), + headers: utils::request_headers(client_http, rpc_url, slot + 1, slot_checkpoint + 1) + .await? + .into_iter() + .map(into_idl_block_header) + .collect(), + }) +} + +async fn find_beacon_block( + client_http: &Client, + rpc_url: &str, + block_number: u64, + block_start: &BeaconBlock, +) -> AnyResult { + match block_number.cmp(&block_start.body.execution_payload.block_number) { + Ordering::Less => { + return Err(anyhow!( + "Requested block number is behind the start beacon block" + )) + } + Ordering::Equal => return Ok(block_start.clone()), + Ordering::Greater => (), + } + + let block_finalized = utils::get_block_finalized(client_http, rpc_url).await?; + + let slot_start = block_start.slot + 1; + for slot in slot_start..=block_finalized.slot { + match utils::get_block(client_http, rpc_url, slot).await { + Ok(block) if block.body.execution_payload.block_number == block_number => { + return Ok(block) + } + Ok(_) => (), + Err(e) if e.downcast_ref::().is_some() => (), + Err(e) => return Err(e), + } + } + + Err(anyhow!("Block was not found")) +} + +fn map_receipt_envelope(receipt: &ReceiptEnvelope) -> ReceiptEnvelope { + let logs = receipt + .logs() + .iter() + .map(AsRef::as_ref) + .cloned() + .collect::>(); + + let result = ReceiptWithBloom::new( + Receipt { + status: receipt.status().into(), + cumulative_gas_used: receipt.cumulative_gas_used(), + logs, + }, + *receipt.logs_bloom(), + ); + + match receipt.tx_type() { + TxType::Legacy => ReceiptEnvelope::Legacy(result), + TxType::Eip1559 => ReceiptEnvelope::Eip1559(result), + TxType::Eip2930 => ReceiptEnvelope::Eip2930(result), + TxType::Eip4844 => ReceiptEnvelope::Eip4844(result), + } +} diff --git a/relayer/src/ethereum_checkpoints/mod.rs b/relayer/src/ethereum_checkpoints/mod.rs index 9d608a65..06e899f1 100644 --- a/relayer/src/ethereum_checkpoints/mod.rs +++ b/relayer/src/ethereum_checkpoints/mod.rs @@ -26,7 +26,7 @@ mod tests; mod metrics; mod replay_back; mod sync_update; -mod utils; +pub mod utils; const SIZE_CHANNEL: usize = 100_000; const SIZE_BATCH: u64 = 30 * SLOTS_PER_EPOCH; @@ -50,15 +50,7 @@ pub async fn relay(args: RelayCheckpointsArgs) { }, } = args; - let program_id_no_prefix = match program_id.starts_with("0x") { - true => &program_id[2..], - false => &program_id, - }; - - let program_id = hex::decode(program_id_no_prefix) - .ok() - .and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()) - .expect("Expecting correct ProgramId"); + let program_id = utils::try_from_hex_encoded(&program_id).expect("Expecting correct ProgramId"); let client_http = ClientBuilder::new() .timeout(Duration::from_secs(beacon_timeout)) diff --git a/relayer/src/ethereum_checkpoints/replay_back.rs b/relayer/src/ethereum_checkpoints/replay_back.rs index 4a53e767..3062ac83 100644 --- a/relayer/src/ethereum_checkpoints/replay_back.rs +++ b/relayer/src/ethereum_checkpoints/replay_back.rs @@ -1,6 +1,4 @@ use super::*; -use checkpoint_light_client_io::BeaconBlockHeader; -use utils::ErrorNotFound; #[allow(clippy::too_many_arguments)] pub async fn execute( @@ -153,7 +151,7 @@ async fn replay_back_slots_inner( gas_limit: u64, ) -> AnyResult<()> { let payload = Handle::ReplayBack( - request_headers(client_http, beacon_endpoint, slot_start, slot_end).await?, + utils::request_headers(client_http, beacon_endpoint, slot_start, slot_end).await?, ); let mut listener = client.subscribe().await?; @@ -197,7 +195,7 @@ async fn replay_back_slots_start( let payload = Handle::ReplayBackStart { sync_update, - headers: request_headers(client_http, beacon_endpoint, slot_start, slot_end).await?, + headers: utils::request_headers(client_http, beacon_endpoint, slot_start, slot_end).await?, }; let mut listener = client.subscribe().await?; @@ -224,25 +222,3 @@ async fn replay_back_slots_start( _ => Err(anyhow!("Wrong handle result to ReplayBackStart")), } } - -async fn request_headers( - client_http: &Client, - beacon_endpoint: &str, - slot_start: Slot, - slot_end: Slot, -) -> AnyResult> { - let batch_size = (slot_end - slot_start) as usize; - let mut requests_headers = Vec::with_capacity(batch_size); - for i in slot_start..slot_end { - requests_headers.push(utils::get_block_header(client_http, beacon_endpoint, i)); - } - - futures::future::join_all(requests_headers) - .await - .into_iter() - .filter(|maybe_header| !matches!(maybe_header, Err(e) if e.downcast_ref::().is_some())) - .collect::, _>>() - .map_err(|e| { - anyhow!("Failed to fetch block headers ([{slot_start}; {slot_end})): {e:?}") - }) -} diff --git a/relayer/src/ethereum_checkpoints/utils/mod.rs b/relayer/src/ethereum_checkpoints/utils/mod.rs index 4552d246..383e600f 100644 --- a/relayer/src/ethereum_checkpoints/utils/mod.rs +++ b/relayer/src/ethereum_checkpoints/utils/mod.rs @@ -1,13 +1,16 @@ -use anyhow::{Error as AnyError, Result as AnyResult}; +use anyhow::{anyhow, Error as AnyError, Result as AnyResult}; use ark_serialize::CanonicalDeserialize; use checkpoint_light_client_io::{ ethereum_common::{ base_types::{BytesFixed, FixedArray}, - beacon::{BLSPubKey, Bytes32, SignedBeaconBlockHeader, SyncAggregate, SyncCommittee}, + beacon::{ + BLSPubKey, Block as BeaconBlock, Bytes32, SignedBeaconBlockHeader, SyncAggregate, + SyncCommittee, + }, utils as eth_utils, }, - ArkScale, BeaconBlockHeader, G1TypeInfo, G2TypeInfo, SyncCommitteeKeys, SyncCommitteeUpdate, - G1, G2, SYNC_COMMITTEE_SIZE, + ArkScale, BeaconBlockHeader, G1TypeInfo, G2TypeInfo, Slot, SyncCommitteeKeys, + SyncCommitteeUpdate, G1, G2, SYNC_COMMITTEE_SIZE, }; use reqwest::{Client, RequestBuilder}; use serde::{de::DeserializeOwned, Deserialize}; @@ -40,6 +43,16 @@ pub struct BeaconBlockHeaderData { pub header: SignedBeaconBlockHeader, } +#[derive(Deserialize, Debug)] +struct BeaconBlockResponse { + data: BeaconBlockData, +} + +#[derive(Deserialize, Debug)] +struct BeaconBlockData { + message: BeaconBlock, +} + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Bootstrap { @@ -182,6 +195,42 @@ pub async fn get_block_header( .map(|response| response.data.header.message) } +pub async fn get_block_finalized(client: &Client, rpc_url: &str) -> AnyResult { + let url = format!("{rpc_url}/eth/v2/beacon/blocks/finalized"); + + get::(client.get(&url)) + .await + .map(|response| response.data.message) +} + +pub async fn get_block(client: &Client, rpc_url: &str, slot: u64) -> AnyResult { + let url = format!("{rpc_url}/eth/v2/beacon/blocks/{slot}"); + + get::(client.get(&url)) + .await + .map(|response| response.data.message) +} + +pub async fn get_block_by_hash( + client: &Client, + rpc_url: &str, + hash: &[u8; 32], +) -> AnyResult { + let mut hex_encoded = [0u8; 66]; + hex_encoded[0] = b'0'; + hex_encoded[1] = b'x'; + + hex::encode_to_slice(hash, &mut hex_encoded[2..]).expect("The buffer has the right size"); + let url = format!( + "{rpc_url}/eth/v2/beacon/blocks/{}", + String::from_utf8_lossy(&hex_encoded) + ); + + get::(client.get(&url)) + .await + .map(|response| response.data.message) +} + pub async fn get_finality_update(client: &Client, rpc_url: &str) -> AnyResult { let url = format!("{rpc_url}/eth/v1/beacon/light_client/finality_update"); @@ -259,3 +308,36 @@ pub fn sync_update_from_update(signature: G2, update: Update) -> SyncCommitteeUp .collect::<_>(), } } + +pub fn try_from_hex_encoded>>(hex_encoded: &str) -> Option { + let data = match hex_encoded.starts_with("0x") { + true => &hex_encoded[2..], + false => hex_encoded, + }; + + hex::decode(data) + .ok() + .and_then(|bytes| >>::try_from(bytes).ok()) +} + +pub async fn request_headers( + client_http: &Client, + beacon_endpoint: &str, + slot_start: Slot, + slot_end: Slot, +) -> AnyResult> { + let batch_size = (slot_end - slot_start) as usize; + let mut requests_headers = Vec::with_capacity(batch_size); + for i in slot_start..slot_end { + requests_headers.push(get_block_header(client_http, beacon_endpoint, i)); + } + + futures::future::join_all(requests_headers) + .await + .into_iter() + .filter(|maybe_header| !matches!(maybe_header, Err(e) if e.downcast_ref::().is_some())) + .collect::, _>>() + .map_err(|e| { + anyhow!("Failed to fetch block headers ([{slot_start}; {slot_end})): {e:?}") + }) +} diff --git a/relayer/src/main.rs b/relayer/src/main.rs index 28148745..6b915a31 100644 --- a/relayer/src/main.rs +++ b/relayer/src/main.rs @@ -12,6 +12,7 @@ use proof_storage::{FileSystemProofStorage, GearProofStorage, ProofStorage}; use relay_merkle_roots::MerkleRootRelayer; use utils_prometheus::MetricsBuilder; +mod erc20; mod ethereum_checkpoints; mod genesis_config; mod message_relayer; @@ -45,6 +46,8 @@ enum CliCommands { /// Fetch authority set hash and id at specified block #[clap(visible_alias("fs"))] FetchAuthoritySetState(FetchAuthoritySetStateArgs), + /// Relay the ERC20 tokens to the Vara network + RelayErc20(RelayErc20Args), } #[derive(Args)] @@ -168,6 +171,44 @@ struct RelayCheckpointsArgs { prometheus_args: PrometheusArgs, } +#[derive(Args)] +struct RelayErc20Args { + /// Specify ProgramId of the program + #[arg(long, env = "ADDRESS")] + program_id: String, + + /// Specify an endpoint providing Beacon API + #[arg(long, env = "BEACON_ENDPOINT")] + beacon_endpoint: String, + + /// Domain of the VARA RPC endpoint + #[arg(long, default_value = "ws://127.0.0.1", env = "VARA_DOMAIN")] + vara_domain: String, + + /// Port of the VARA RPC endpoint + #[arg(long, default_value = "9944", env = "VARA_PORT")] + vara_port: u16, + + /// Substrate URI that identifies a user by a mnemonic phrase or + /// provides default users from the keyring (e.g., "//Alice", "//Bob", + /// etc.). The password for URI should be specified in the same `suri`, + /// separated by the ':' char + #[arg(long, default_value = "//Alice", env = "VARA_SURI")] + vara_suri: String, + + /// Address of the ethereum endpoint + #[arg( + long = "ethereum-endpoint", + default_value = DEFAULT_ETH_RPC, + env = "ETH_RPC" + )] + eth_endpoint: String, + + /// Specify the hash of the ERC20-transaction to relay + #[arg(long, env = "TX_HASH")] + tx_hash: String, +} + #[tokio::main] async fn main() { let _ = dotenv::dotenv(); @@ -283,6 +324,7 @@ async fn main() { .await .expect("Failed to fetch authority set state"); } + CliCommands::RelayErc20(args) => erc20::relay(args).await, }; }