diff --git a/Cargo.lock b/Cargo.lock index 85a7149c4c..e03478904a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,47 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arbitrum-light-client" +version = "0.1.0" +dependencies = [ + "arbitrum-verifier", + "base64 0.21.7", + "cosmwasm-std", + "ethereum-verifier", + "ethers-core", + "hex", + "ics008-wasm-client", + "protos", + "rlp", + "schemars", + "serde", + "serde-json-wasm 1.0.1", + "serde_json", + "sha3", + "thiserror", + "tiny-keccak", + "unionlabs", +] + +[[package]] +name = "arbitrum-verifier" +version = "0.1.0" +dependencies = [ + "error_reporter", + "ethereum-verifier", + "ethers-core", + "hex", + "hex-literal", + "rlp", + "serde", + "serde-utils", + "serde_json", + "sha3", + "thiserror", + "unionlabs", +] + [[package]] name = "arith" version = "0.2.3" @@ -796,10 +837,12 @@ dependencies = [ "contracts", "crossbeam-queue", "dashmap", + "enumorph", "ethers", "frame-support-procedural", "futures", "hex", + "hex-literal", "ics23", "num_enum", "prost", @@ -2199,6 +2242,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error_reporter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" + [[package]] name = "etcetera" version = "0.8.0" @@ -5031,6 +5080,7 @@ name = "relay-message" version = "0.1.0" dependencies = [ "arbitrary", + "arbitrum-verifier", "beacon-api", "chain-utils", "contracts", @@ -7428,6 +7478,7 @@ dependencies = [ "serde-utils", "serde_json", "sha2 0.10.8", + "sha3", "ssz", "static_assertions", "subtle-encoding", diff --git a/Cargo.toml b/Cargo.toml index fe485fb7c9..8da971da72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,6 @@ members = [ "lib/beacon-api", "lib/block-message", "lib/chain-utils", - "lib/cometbls-groth16-verifier", - "lib/ethereum-verifier", "lib/gnark-key-parser", "lib/ics-008-wasm-client", "lib/ics23", @@ -36,17 +34,21 @@ members = [ "lib/scroll-codec", "lib/scroll-codec/fetch-test-vectors", "lib/scroll-rpc", - "lib/scroll-verifier", "lib/serde-utils", "lib/ssz", "lib/ssz/tests-generator", "lib/ssz-derive", - "lib/tendermint-verifier", "lib/unionlabs", "lib/voyager-message", "lib/zktrie-rs", - "lib/gnark-key-parser", + "lib/arbitrum-verifier", + "lib/cometbls-groth16-verifier", + "lib/ethereum-verifier", + "lib/scroll-verifier", + "lib/tendermint-verifier", + + "light-clients/arbitrum-light-client", "light-clients/cometbls-light-client", "light-clients/ethereum-light-client", "light-clients/scroll-light-client", @@ -75,7 +77,11 @@ disallowed_types = "deny" lto = "thin" opt-level = 3 +[profile.dev] +strip = true + [workspace.dependencies] +arbitrum-verifier = { path = "lib/arbitrum-verifier", default-features = false } beacon-api = { path = "lib/beacon-api", default-features = false } block-message = { path = "lib/block-message", default-features = false } chain-utils = { path = "lib/chain-utils", default-features = false } diff --git a/dictionary.txt b/dictionary.txt index 9aaf82c9ce..758695499f 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -204,6 +204,7 @@ appchain appchains appmodule appparams +arbitrum arbtest arethetypeswrong argjson @@ -393,6 +394,7 @@ domrehype dont downcasting drawio +drpc dudx dydx dyld @@ -412,6 +414,7 @@ ethabicodec ethash ethcall etherbase +ethevent ethkey evidencekeeper evidencetypes diff --git a/flake.nix b/flake.nix index 243463bd1a..5d1767af92 100644 --- a/flake.nix +++ b/flake.nix @@ -212,6 +212,7 @@ ./light-clients/cometbls-light-client/cometbls-light-client.nix ./light-clients/tendermint-light-client/tendermint-light-client.nix ./light-clients/scroll-light-client/scroll-light-client.nix + ./light-clients/arbitrum-light-client/arbitrum-light-client.nix ./lib/cometbls-groth16-verifier/default.nix ./cosmwasm/cosmwasm.nix ./evm/evm.nix diff --git a/generated/rust/protos/Cargo.toml b/generated/rust/protos/Cargo.toml index 02234f2e75..1a71093aa1 100644 --- a/generated/rust/protos/Cargo.toml +++ b/generated/rust/protos/Cargo.toml @@ -270,6 +270,7 @@ proto_full = [ "union+galois+api+v1", "union+galois+api+v2", "union+galois+api+v3", + "union+ibc+lightclients+arbitrum+v1", "union+ibc+lightclients+cometbls+v1", "union+ibc+lightclients+ethereum+v1", "union+ibc+lightclients+scroll+v1", @@ -299,6 +300,7 @@ proto_full = [ "union+galois+api+v1" = ["tendermint+types"] "union+galois+api+v2" = ["tendermint+types"] "union+galois+api+v3" = ["tendermint+types"] +"union+ibc+lightclients+arbitrum+v1" = ["ibc+core+client+v1", "union+ibc+lightclients+ethereum+v1"] "union+ibc+lightclients+cometbls+v1" = [ "google+protobuf", "ibc+core+client+v1", diff --git a/generated/rust/protos/src/lib.rs b/generated/rust/protos/src/lib.rs index 820ef69e53..15e53a498a 100644 --- a/generated/rust/protos/src/lib.rs +++ b/generated/rust/protos/src/lib.rs @@ -843,6 +843,14 @@ pub mod union { } pub mod ibc { pub mod lightclients { + pub mod arbitrum { + #[cfg(feature = "union+ibc+lightclients+arbitrum+v1")] + // @@protoc_insertion_point(attribute:union.ibc.lightclients.arbitrum.v1) + pub mod v1 { + include!("union.ibc.lightclients.arbitrum.v1.rs"); + // @@protoc_insertion_point(union.ibc.lightclients.arbitrum.v1) + } + } pub mod cometbls { #[cfg(feature = "union+ibc+lightclients+cometbls+v1")] // @@protoc_insertion_point(attribute:union.ibc.lightclients.cometbls.v1) diff --git a/generated/rust/protos/src/union.ibc.lightclients.arbitrum.v1.rs b/generated/rust/protos/src/union.ibc.lightclients.arbitrum.v1.rs new file mode 100644 index 0000000000..c4fd990b1e --- /dev/null +++ b/generated/rust/protos/src/union.ibc.lightclients.arbitrum.v1.rs @@ -0,0 +1,154 @@ +// @generated +/// TODO: l2_ instead of rollup_ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ClientState { + #[prost(string, tag = "1")] + pub l1_client_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub chain_id: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub l1_latest_slot: u64, + #[prost(bytes = "vec", tag = "4")] + pub l1_contract_address: ::prost::alloc::vec::Vec, + /// _latestConfirmed + #[prost(bytes = "vec", tag = "5")] + pub l1_latest_confirmed_slot: ::prost::alloc::vec::Vec, + /// _nodes + #[prost(bytes = "vec", tag = "6")] + pub l1_nodes_slot: ::prost::alloc::vec::Vec, + /// _nodes\[_latestConfirmed\].confirmData + #[prost(bytes = "vec", tag = "7")] + pub confirm_data_offset: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "8")] + pub frozen_height: + ::core::option::Option, + #[prost(bytes = "vec", tag = "9")] + pub l2_ibc_contract_address: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "10")] + pub l2_ibc_commitment_slot: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for ClientState { + const NAME: &'static str = "ClientState"; + const PACKAGE: &'static str = "union.ibc.lightclients.arbitrum.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.arbitrum.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConsensusState { + #[prost(bytes = "vec", tag = "1")] + pub ibc_storage_root: ::prost::alloc::vec::Vec, + #[prost(uint64, tag = "2")] + pub timestamp: u64, +} +impl ::prost::Name for ConsensusState { + const NAME: &'static str = "ConsensusState"; + const PACKAGE: &'static str = "union.ibc.lightclients.arbitrum.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.arbitrum.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Header { + #[prost(message, optional, tag = "1")] + pub l1_height: + ::core::option::Option, + /// Proof of the L1 rollup account in the L1 state root. + #[prost(message, optional, tag = "2")] + pub l1_account_proof: ::core::option::Option, + /// Proof of the l2 ibc contract address in the l2 state root. + #[prost(message, optional, tag = "3")] + pub l2_ibc_account_proof: ::core::option::Option, + /// The latest confirmed node number, as stored in `_latestConfirmed`. + /// + /// + #[prost(uint64, tag = "6")] + pub latest_confirmed: u64, + /// Proof of `latest_confirmed`. + #[prost(message, optional, tag = "7")] + pub l1_latest_confirmed_slot_proof: + ::core::option::Option, + /// The proof of the \[`_nodes`\] mapping at `latest_confirmed`, offset to \[`Node.confirmData`\]. + /// + /// \[`_nodes`\]: + /// \[`Node.confirmData`\]: + #[prost(message, optional, tag = "8")] + pub l1_nodes_slot_proof: ::core::option::Option, + /// Arbitrum block header, used to recompute the block hash and verify the timestamp. + #[prost(message, optional, tag = "9")] + pub l2_header: ::core::option::Option, +} +impl ::prost::Name for Header { + const NAME: &'static str = "Header"; + const PACKAGE: &'static str = "union.ibc.lightclients.arbitrum.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.arbitrum.v1.{}", Self::NAME) + } +} +/// The Arbitrum header as returned from `eth_getBlockByNumber`, with all non-standard fields removed. +/// +/// Note that certain fields are different than a typical eth_getBlockByNumber response; see [here]() for more information. +/// +/// +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct L2Header { + /// H256 + #[prost(bytes = "vec", tag = "1")] + pub parent_hash: ::prost::alloc::vec::Vec, + /// H256 + #[prost(bytes = "vec", tag = "2")] + pub sha3_uncles: ::prost::alloc::vec::Vec, + /// H160 + #[prost(bytes = "vec", tag = "3")] + pub miner: ::prost::alloc::vec::Vec, + /// H256 + #[prost(bytes = "vec", tag = "4")] + pub state_root: ::prost::alloc::vec::Vec, + /// H256 + #[prost(bytes = "vec", tag = "5")] + pub transactions_root: ::prost::alloc::vec::Vec, + /// H256 + #[prost(bytes = "vec", tag = "6")] + pub receipts_root: ::prost::alloc::vec::Vec, + /// H2048 + #[prost(bytes = "vec", tag = "7")] + pub logs_bloom: ::prost::alloc::vec::Vec, + /// U256 + #[prost(bytes = "vec", tag = "8")] + pub difficulty: ::prost::alloc::vec::Vec, + /// U256 + #[prost(bytes = "vec", tag = "9")] + pub number: ::prost::alloc::vec::Vec, + #[prost(uint64, tag = "10")] + pub gas_limit: u64, + #[prost(uint64, tag = "11")] + pub gas_used: u64, + #[prost(uint64, tag = "12")] + pub timestamp: u64, + /// This field is equivalent to sendRoot. + /// + /// H256 + #[prost(bytes = "vec", tag = "13")] + pub extra_data: ::prost::alloc::vec::Vec, + /// H256 + #[prost(bytes = "vec", tag = "14")] + pub mix_hash: ::prost::alloc::vec::Vec, + /// H64 + #[prost(bytes = "vec", tag = "15")] + pub nonce: ::prost::alloc::vec::Vec, + /// U256 + #[prost(bytes = "vec", tag = "16")] + pub base_fee_per_gas: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for L2Header { + const NAME: &'static str = "L2Header"; + const PACKAGE: &'static str = "union.ibc.lightclients.arbitrum.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.arbitrum.v1.{}", Self::NAME) + } +} +// @@protoc_insertion_point(module) diff --git a/lib/arbitrum-verifier/Cargo.toml b/lib/arbitrum-verifier/Cargo.toml new file mode 100644 index 0000000000..3cc20fe2cb --- /dev/null +++ b/lib/arbitrum-verifier/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition = { workspace = true } +license-file = { workspace = true } +name = "arbitrum-verifier" +repository = { workspace = true } +version = "0.1.0" + +[lints] +workspace = true + +[package.metadata.crane] +# test-include = ["lib/arbitrum-verifier/tests"] + +[dependencies] +ethereum-verifier = { workspace = true } +ethers-core.workspace = true +hex = { workspace = true } +hex-literal.workspace = true +rlp = { workspace = true } +serde = { workspace = true } +serde-utils = { workspace = true } +serde_json = { workspace = true } +sha3 = { workspace = true } +thiserror = { workspace = true } +unionlabs = { workspace = true } + +[dev-dependencies] +error_reporter = "1.0.0" diff --git a/lib/arbitrum-verifier/arbitrum-verifier.nix b/lib/arbitrum-verifier/arbitrum-verifier.nix new file mode 100644 index 0000000000..122a4cdf06 --- /dev/null +++ b/lib/arbitrum-verifier/arbitrum-verifier.nix @@ -0,0 +1,11 @@ +{ ... }: { + perSystem = { self', pkgs, system, config, crane, stdenv, dbg, lib, ... }: + let + arbitrum-verifier-all = (crane.buildWorkspaceMember { + crateDirFromRoot = "lib/arbitrum-verifier"; + }); + in + { + inherit (arbitrum-verifier-all) checks; + }; +} diff --git a/lib/arbitrum-verifier/scroll-verifier.nix b/lib/arbitrum-verifier/scroll-verifier.nix new file mode 100644 index 0000000000..122a4cdf06 --- /dev/null +++ b/lib/arbitrum-verifier/scroll-verifier.nix @@ -0,0 +1,11 @@ +{ ... }: { + perSystem = { self', pkgs, system, config, crane, stdenv, dbg, lib, ... }: + let + arbitrum-verifier-all = (crane.buildWorkspaceMember { + crateDirFromRoot = "lib/arbitrum-verifier"; + }); + in + { + inherit (arbitrum-verifier-all) checks; + }; +} diff --git a/lib/arbitrum-verifier/src/lib.rs b/lib/arbitrum-verifier/src/lib.rs new file mode 100644 index 0000000000..3e8ff0aa97 --- /dev/null +++ b/lib/arbitrum-verifier/src/lib.rs @@ -0,0 +1,180 @@ +use core::fmt::Debug; + +use ethereum_verifier::{verify_account_storage_root, verify_storage_proof}; +use sha3::{Digest, Keccak256}; +use unionlabs::{ + hash::H256, + ibc::lightclients::arbitrum::{client_state::ClientState, header::Header}, + uint::U256, +}; + +#[derive(thiserror::Error, Debug, PartialEq, Clone)] +pub enum Error { + #[error("invalid contract address proof: {0}")] + InvalidContractAddressProof(#[source] ethereum_verifier::Error), + #[error("invalid _latestConfirmed proof: {0}")] + InvalidLatestConfirmedProof(#[source] ethereum_verifier::Error), + #[error("invalid _nodes[_latestConfirmed].confirmData proof: {0}")] + InvalidNodeConfirmDataProof(#[source] ethereum_verifier::Error), + #[error("invalid l2 proof: {0}")] + InvalidL2Proof(#[source] ethereum_verifier::Error), +} + +pub fn verify_header( + client_state: ClientState, + header: Header, + l1_state_root: H256, +) -> Result<(), Error> { + // Verify that the l1 account root is part of the L1 root + verify_account_storage_root( + l1_state_root, + &client_state.l1_contract_address, + &header.l1_account_proof.proof, + &header.l1_account_proof.storage_root, + ) + .map_err(Error::InvalidContractAddressProof)?; + + // Verify that the l1 _latestConfirmed is part of the l1 account root + verify_storage_proof( + header.l1_account_proof.storage_root, + client_state.l1_latest_confirmed_slot, + &rlp::encode(&header.l1_latest_confirmed_slot_proof.proofs[0].value), + // &rlp::encode(&U256::from(header.latest_confirmed)), + &header.l1_latest_confirmed_slot_proof.proofs[0].proof, + ) + .map_err(Error::InvalidLatestConfirmedProof)?; + + // Verify that the node's confirmData is correct + let expected_confirm_data = H256::from( + Keccak256::new() + .chain_update(header.l2_header.hash()) + .chain_update(header.l2_header.extra_data) + .finalize(), + ); + + dbg!(expected_confirm_data); + + // Verify that the l1 _nodes[_latestConfirmed].confirmData is part of the l1 account root + let key = nodes_confirm_data_mapping_key( + client_state.l1_nodes_slot, + header.latest_confirmed.into(), + client_state.l1_nodes_confirm_data_offset, + ); + + assert_eq!(key, header.l1_nodes_slot_proof.proofs[0].key); + + verify_storage_proof( + header.l1_account_proof.storage_root, + key, + &rlp::encode(&U256::from_be_bytes(expected_confirm_data.0)), + &header.l1_nodes_slot_proof.proofs[0].proof, + ) + .map_err(Error::InvalidNodeConfirmDataProof)?; + + // Verify that the ibc account root is part of the l1 root + verify_account_storage_root( + header.l2_header.state_root, + &client_state.l2_ibc_contract_address, + &header.l2_ibc_account_proof.proof, + &header.l2_ibc_account_proof.storage_root, + ) + .map_err(Error::InvalidL2Proof)?; + + Ok(()) +} + +/// Storage slot of a `mapping(uint64 => Node)` mapping, where the mapping is at slot `slot` and the `uint64` is the `nodeNum`, accessing the storage at the offset of confirm_data_offset. +pub fn nodes_confirm_data_mapping_key( + slot: U256, + node_num: U256, + confirm_data_offset: U256, +) -> U256 { + U256::from_be_bytes( + sha3::Keccak256::new() + .chain_update(node_num.to_be_bytes()) + .chain_update(slot.to_be_bytes()) + .finalize() + .into(), + ) + confirm_data_offset +} + +// #[cfg(test)] +// mod tests { +// use std::path::Path; + +// use hex_literal::hex; +// use serde::de::DeserializeOwned; +// use unionlabs::{ +// encoding::{DecodeAs, EncodeAs, Proto}, +// ibc::lightclients::arbitrum::header::Header, +// }; + +// use crate::verify_header; + +// fn read_json(path: impl AsRef) -> T { +// serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap() +// } + +// // #[test] +// // fn test_update_header() { +// // let arbitrum_client_state = +// // read_json("/home/ben/projects/union/union/arb-client-state.json"); +// // let arbitrum_header: Header = read_json("/home/ben/projects/union/union/arb-header.json"); + +// // let proto_header = arbitrum_header.clone().encode_as::(); +// // let rt_header = Header::decode_as::(&proto_header).unwrap(); + +// // std::fs::write( +// // "/home/ben/projects/union/union/arb-header-rt.json", +// // serde_json::to_string_pretty(&rt_header).unwrap(), +// // ) +// // .unwrap(); + +// // assert_eq!(arbitrum_header, rt_header); + +// // let res = verify_header( +// // arbitrum_client_state, +// // arbitrum_header, +// // hex!("2b06d9a1b1e74dc203face3a78f8b0fbaf2c07aca9d9520cf75ea3b6682bff93").into(), +// // ); + +// // let () = res.map_err(error_reporter::Report::new).unwrap(); + +// // // assert!(matches!(res, Ok(()))); +// // } + +// // #[test] +// // fn test_l2_contract_slot_exist() { +// // let proof: Proof = +// // serde_json::from_str(&std::fs::read_to_string("tests/arbitrum_proof.json").unwrap()) +// // .unwrap(); +// // assert!(matches!( +// // verify_zktrie_storage_proof( +// // H256(hex!( +// // "1b52888cae05bdba27f8470293a7d2bc3b9a9c822d96affe05ef243e0dfd44a0" +// // )), +// // proof.key.to_be_bytes().into(), +// // &proof.value.to_be_bytes(), +// // &proof.proof +// // ), +// // Ok(()) +// // )) +// // } + +// // #[test] +// // fn test_l2_contract_slot_absent() { +// // let proof: Proof = +// // serde_json::from_str(&std::fs::read_to_string("tests/arbitrum_absent.json").unwrap()) +// // .unwrap(); +// // assert!(matches!( +// // verify_zktrie_storage_absence( +// // H256(hex!( +// // "1b52888cae05bdba27f8470293a7d2bc3b9a9c822d96affe05ef243e0dfd44a0" +// // )), +// // proof.key.to_be_bytes().into(), +// // &proof.proof +// // ), +// // Ok(()) +// // )) +// // } +// } diff --git a/lib/block-message/src/aggregate.rs b/lib/block-message/src/aggregate.rs index 7a9205a726..56ecbcc582 100644 --- a/lib/block-message/src/aggregate.rs +++ b/lib/block-message/src/aggregate.rs @@ -25,6 +25,7 @@ pub enum Aggregate { } impl HandleAggregate for AnyChainIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] fn handle( self, data: VecDeque<::Data>, diff --git a/lib/block-message/src/chain_impls.rs b/lib/block-message/src/chain_impls.rs index 2124622538..7418c4d34f 100644 --- a/lib/block-message/src/chain_impls.rs +++ b/lib/block-message/src/chain_impls.rs @@ -110,6 +110,7 @@ macro_rules! try_from_block_poll_msg { }; } +pub mod arbitrum; pub mod cosmos; pub mod ethereum; pub mod scroll; diff --git a/lib/block-message/src/chain_impls/arbitrum.rs b/lib/block-message/src/chain_impls/arbitrum.rs new file mode 100644 index 0000000000..d68205aeac --- /dev/null +++ b/lib/block-message/src/chain_impls/arbitrum.rs @@ -0,0 +1,134 @@ +use std::collections::VecDeque; + +use chain_utils::arbitrum::{Arbitrum, ARBITRUM_REVISION_NUMBER}; +use enumorph::Enumorph; +use queue_msg::{aggregation::do_aggregate, fetch, queue_msg, QueueMsg}; +use unionlabs::{ethereum::config::Mainnet, traits::Chain}; + +use crate::{ + aggregate::{Aggregate, AnyAggregate}, + chain_impls::ethereum::{ + fetch_beacon_block_range, fetch_channel, fetch_connection, fetch_get_logs, + AggregateWithChannel, AggregateWithConnection, ChannelData, ConnectionData, + FetchBeaconBlockRange, FetchChannel, FetchConnection, FetchEvents, FetchGetLogs, + }, + data::{AnyData, ChainEvent, Data}, + fetch::{AnyFetch, DoFetch, DoFetchBlockRange, Fetch, FetchBlockRange}, + id, AnyChainIdentified, BlockMessageTypes, ChainExt, DoAggregate, Identified, IsAggregateData, +}; + +impl ChainExt for Arbitrum { + type Data = ArbitrumData; + type Fetch = ArbitrumFetch; + type Aggregate = ArbitrumAggregate; +} + +impl DoFetchBlockRange for Arbitrum +where + AnyChainIdentified: From>>, +{ + fn fetch_block_range( + c: &Arbitrum, + range: FetchBlockRange, + ) -> QueueMsg { + fetch(id( + c.chain_id(), + Fetch::::specific(FetchEvents { + from_height: range.from_height, + to_height: range.to_height, + }), + )) + } +} + +impl DoFetch for ArbitrumFetch +where + AnyChainIdentified: From>>, + AnyChainIdentified: From>>, + AnyChainIdentified: From>>, +{ + async fn do_fetch(c: &Arbitrum, msg: Self) -> QueueMsg { + match msg { + ArbitrumFetch::FetchEvents(FetchEvents { + from_height, + to_height, + }) => fetch(id( + c.chain_id(), + Fetch::::specific(FetchBeaconBlockRange { + from_slot: from_height.revision_height, + to_slot: to_height.revision_height, + }), + )), + ArbitrumFetch::FetchGetLogs(get_logs) => { + fetch_get_logs(c, get_logs, ARBITRUM_REVISION_NUMBER).await + } + ArbitrumFetch::FetchBeaconBlockRange(beacon_block_range) => { + fetch_beacon_block_range(c, beacon_block_range, &c.l1.beacon_api_client).await + } + ArbitrumFetch::FetchChannel(channel) => fetch_channel(c, channel).await, + ArbitrumFetch::FetchConnection(connection) => fetch_connection(c, connection).await, + } + } +} + +#[queue_msg] +#[derive(Enumorph)] +pub enum ArbitrumFetch { + FetchEvents(FetchEvents), + FetchGetLogs(FetchGetLogs), + FetchBeaconBlockRange(FetchBeaconBlockRange), + + FetchChannel(FetchChannel), + FetchConnection(FetchConnection), +} + +#[queue_msg] +pub struct FetchBatchIndex { + beacon_slot: u64, + batch_index: u64, +} + +#[queue_msg] +#[derive(Enumorph)] +pub enum ArbitrumAggregate { + AggregateWithChannel(AggregateWithChannel), + AggregateWithConnection(AggregateWithConnection), +} + +impl DoAggregate for Identified +where + AnyChainIdentified: From>>, + + Identified>: IsAggregateData, + Identified>: IsAggregateData, +{ + fn do_aggregate( + Identified { chain_id, t }: Self, + data: VecDeque>, + ) -> QueueMsg { + match t { + ArbitrumAggregate::AggregateWithChannel(msg) => do_aggregate(id(chain_id, msg), data), + ArbitrumAggregate::AggregateWithConnection(msg) => { + do_aggregate(id(chain_id, msg), data) + } + } + } +} + +#[queue_msg] +#[derive(Enumorph)] +pub enum ArbitrumData { + Channel(ChannelData), + Connection(ConnectionData), +} + +const _: () = { + try_from_block_poll_msg! { + chain = Arbitrum, + generics = (), + msgs = ArbitrumData( + Channel(ChannelData), + Connection(ConnectionData), + ), + } +}; diff --git a/lib/block-message/src/chain_impls/ethereum.rs b/lib/block-message/src/chain_impls/ethereum.rs index becbbbe246..91f8afad60 100644 --- a/lib/block-message/src/chain_impls/ethereum.rs +++ b/lib/block-message/src/chain_impls/ethereum.rs @@ -127,6 +127,8 @@ where AnyChainIdentified: From>>, AnyChainIdentified: From>>, { + tracing::debug!(%from_slot, %to_slot, "fetching logs in beacon block range"); + let event_height = Height { revision_number, revision_height: to_slot, @@ -135,39 +137,45 @@ where let from_block = c.execution_height_of_beacon_slot(from_slot).await; let to_block = c.execution_height_of_beacon_slot(to_slot).await; - // REVIEW: Surely transactions and events can be fetched in parallel? - conc( - futures::stream::iter( - c.provider() - .get_logs( - &Filter::new() - .address(ethers::types::H160::from(c.ibc_handler_address())) - .from_block(from_block) - // NOTE: This -1 is very important, else events will be double fetched - .to_block(to_block - 1), - ) - .await - .unwrap(), - ) - .filter_map(|log| async { - let tx_hash = log - .transaction_hash - .expect("log should have transaction_hash") - .into(); - - tracing::debug!(?log, "raw log"); - - match IBCHandlerEvents::decode_log(&log.into()) { - Ok(event) => Some(mk_aggregate_event(c, event, event_height, tx_hash).await), - Err(e) => { - tracing::warn!("could not decode evm event {}", e); - None + if from_block == to_block { + tracing::debug!(%from_block, %to_block, %from_slot, %to_slot, "beacon block range is empty"); + QueueMsg::Noop + } else { + tracing::debug!(%from_block, %to_block, "fetching block range"); + // REVIEW: Surely transactions and events can be fetched in parallel? + conc( + futures::stream::iter( + c.provider() + .get_logs( + &Filter::new() + .address(ethers::types::H160::from(c.ibc_handler_address())) + .from_block(from_block) + // NOTE: This -1 is very important, else events will be double fetched + .to_block(to_block - 1), + ) + .await + .unwrap(), + ) + .filter_map(|log| async { + let tx_hash = log + .transaction_hash + .expect("log should have transaction_hash") + .into(); + + tracing::debug!(?log, "raw log"); + + match IBCHandlerEvents::decode_log(&log.into()) { + Ok(event) => Some(mk_aggregate_event(c, event, event_height, tx_hash).await), + Err(e) => { + tracing::warn!("could not decode evm event {}", e); + None + } } - } - }) - .collect::>() - .await, - ) + }) + .collect::>() + .await, + ) + } } pub(crate) async fn fetch_beacon_block_range( @@ -181,6 +189,8 @@ where AnyChainIdentified: From>>, { + tracing::debug!(%from_slot, %to_slot, "fetching beacon block range"); + assert!(from_slot < to_slot); if to_slot - from_slot == 1 { @@ -190,7 +200,7 @@ where )) } else { // attempt to shrink from..to - // note that this is *exclusive* on the `to` + // note that this is *exclusive* on `to` for slot in (from_slot + 1)..to_slot { tracing::info!("querying slot {slot}"); match beacon_api_client @@ -246,6 +256,8 @@ where AnyChainIdentified: From>>, { + tracing::debug!(%height, %path, "fetching channel"); + data(id( c.chain_id(), Data::::specific(ChannelData { @@ -277,6 +289,8 @@ where Hc: EthereumChainExt>>, AnyChainIdentified: From>>, { + tracing::debug!(%height, %path, "fetching connection"); + data(id( c.chain_id(), Data::::specific(ConnectionData( diff --git a/lib/block-message/src/data.rs b/lib/block-message/src/data.rs index e9c983792a..38420c7a02 100644 --- a/lib/block-message/src/data.rs +++ b/lib/block-message/src/data.rs @@ -17,6 +17,7 @@ pub enum Data { // Passthrough since we don't want to handle any top-level data, just bubble it up to the top level. impl HandleData for AnyChainIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] fn handle( self, _store: &::Store, diff --git a/lib/block-message/src/fetch.rs b/lib/block-message/src/fetch.rs index ff7017e559..dbc461cda8 100644 --- a/lib/block-message/src/fetch.rs +++ b/lib/block-message/src/fetch.rs @@ -27,6 +27,7 @@ pub enum Fetch { } impl HandleFetch for AnyChainIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] async fn handle( self, store: &::Store, diff --git a/lib/block-message/src/lib.rs b/lib/block-message/src/lib.rs index dd0d4e5dd7..142e74fabe 100644 --- a/lib/block-message/src/lib.rs +++ b/lib/block-message/src/lib.rs @@ -2,7 +2,9 @@ use std::{collections::VecDeque, fmt::Debug}; -use chain_utils::{cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, Chains}; +use chain_utils::{ + arbitrum::Arbitrum, cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, Chains, +}; use frame_support_procedural::{CloneNoBound, DebugNoBound, PartialEqNoBound}; use queue_msg::{QueueMessageTypes, QueueMsg, QueueMsgTypesTraits}; use serde::{Deserialize, Serialize}; @@ -61,6 +63,17 @@ pub enum AnyChainIdentified { EthMainnet(Identified, InnerOf>>), EthMinimal(Identified, InnerOf>>), Scroll(Identified>), + Arbitrum(Identified>), +} + +impl AnyChainIdentified { + fn chain_id(&self) -> String { + let i = self; + + any_chain! { + |i| i.chain_id.to_string() + } + } } pub trait AnyChain { @@ -216,6 +229,7 @@ macro_rules! any_chain { AnyChainIdentified::Union($msg) => $expr, AnyChainIdentified::Cosmos($msg) => $expr, AnyChainIdentified::Scroll($msg) => $expr, + AnyChainIdentified::Arbitrum($msg) => $expr, } }; } diff --git a/lib/block-message/src/wait.rs b/lib/block-message/src/wait.rs index e8741e3983..734d33f6c1 100644 --- a/lib/block-message/src/wait.rs +++ b/lib/block-message/src/wait.rs @@ -56,6 +56,7 @@ where } impl HandleWait for AnyChainIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] async fn handle(self, store: &Chains) -> Result, QueueError> { let wait = self; diff --git a/lib/chain-utils/Cargo.toml b/lib/chain-utils/Cargo.toml index 895c5d970a..b92c4672b3 100644 --- a/lib/chain-utils/Cargo.toml +++ b/lib/chain-utils/Cargo.toml @@ -19,6 +19,7 @@ bip32 = { workspace = true, features = ["secp256k1"] } chrono = { workspace = true, features = ["alloc"] } crossbeam-queue = { workspace = true, features = ["std"] } dashmap = { workspace = true } +enumorph = { workspace = true } ethers = { workspace = true, features = ["rustls", "ws"] } frame-support-procedural = { workspace = true } futures = { workspace = true } @@ -42,4 +43,5 @@ typenum = { workspace = true, features = ["const-generics", "no arbitrary = ["dep:arbitrary"] [dev-dependencies] +hex-literal = { workspace = true } tracing-subscriber = "0.3.18" diff --git a/lib/chain-utils/src/arbitrum.rs b/lib/chain-utils/src/arbitrum.rs new file mode 100644 index 0000000000..e7b4fe2966 --- /dev/null +++ b/lib/chain-utils/src/arbitrum.rs @@ -0,0 +1,380 @@ +use std::sync::Arc; + +use bip32::secp256k1::ecdsa; +use contracts::ibc_handler::IBCHandler; +use ethers::{ + contract::EthEvent, + providers::{Middleware, Provider, ProviderError, Ws, WsClientError}, +}; +use serde::{Deserialize, Serialize}; +use unionlabs::{ + encoding::EthAbi, + ethereum::config::Mainnet, + google::protobuf::any::Any, + hash::{H160, H256}, + ibc::{ + core::client::height::Height, + lightclients::{arbitrum, ethereum::storage_proof::StorageProof}, + }, + id::ClientId, + traits::{Chain, ChainIdOf, ClientIdOf, FromStrExact}, + uint::U256, + ByteArrayExt, +}; + +use crate::{ + ethereum::{ + self, get_proof, Ethereum, EthereumChain, EthereumInitError, EthereumSignerMiddleware, + EthereumSignersConfig, ReadWrite, Readonly, + }, + private_key::PrivateKey, + union::Union, + wasm::Wasm, + Pool, +}; + +pub const ARBITRUM_REVISION_NUMBER: u64 = 0; + +#[derive(Debug, Clone)] +pub struct Arbitrum { + chain_id: U256, + + pub ibc_handlers: Pool>, + + pub provider: Arc>, + pub ibc_handler_address: H160, + pub ibc_commitment_slot: U256, + + pub l1: Ethereum, + pub l1_contract_address: H160, + pub l1_latest_confirmed_slot: U256, + pub l1_nodes_slot: U256, + pub l1_nodes_confirm_data_offset: U256, + + pub l1_client_id: ClientIdOf>, + /// GRPC url of Union, used to query the L1 state with [`Self::l1_client_id`]. + pub union_grpc_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Config { + /// The address of the `IBCHandler` smart contract. + pub ibc_handler_address: H160, + pub ibc_commitment_slot: U256, + + /// The signer that will be used to submit transactions by voyager. + pub signers: Vec>, + + /// The RPC endpoint for the execution (scroll) chain. + pub l2_eth_rpc_api: String, + + pub l1_contract_address: H160, + pub l1_latest_confirmed_slot: U256, + pub l1_nodes_slot: U256, + pub l1_nodes_confirm_data_offset: U256, + + pub l1_client_id: ClientIdOf>, + pub l1: ethereum::Config, + + pub union_grpc_url: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum ArbitrumInitError { + #[error("unable to initialize L1")] + Ethereum(#[from] EthereumInitError), + #[error("unable to connect to websocket")] + Ws(#[from] WsClientError), + #[error("provider error")] + Provider(#[from] ProviderError), +} + +impl Arbitrum { + pub async fn new(config: Config) -> Result { + let provider = Provider::new(Ws::connect(config.l2_eth_rpc_api.clone()).await?); + + let chain_id = provider.get_chainid().await?; + tracing::info!(?chain_id); + + Ok(Self { + chain_id: U256(chain_id), + ibc_handlers: ReadWrite::new( + config.signers, + config.ibc_handler_address, + chain_id.as_u64(), + provider.clone(), + ), + ibc_handler_address: config.ibc_handler_address, + provider: Arc::new(provider), + l1: Ethereum::new(config.l1).await?, + l1_client_id: config.l1_client_id, + union_grpc_url: config.union_grpc_url, + l1_contract_address: config.l1_contract_address, + l1_latest_confirmed_slot: config.l1_latest_confirmed_slot, + ibc_commitment_slot: config.ibc_commitment_slot, + l1_nodes_slot: config.l1_nodes_slot, + l1_nodes_confirm_data_offset: config.l1_nodes_confirm_data_offset, + }) + } + + pub async fn latest_confirmed_at_beacon_slot(&self, slot: u64) -> u64 { + let l1_height = self.l1.execution_height_of_beacon_slot(slot).await; + + let latest_confirmed = u64::from_be_bytes( + self.l1 + .provider() + .get_storage_at( + ethers::types::H160::from(self.l1_contract_address), + ethers::types::H256(self.l1_latest_confirmed_slot.to_be_bytes()), + Some(ethers::types::BlockNumber::Number(l1_height.into()).into()), + ) + .await + .unwrap() + .0 + .array_slice::<24, 8>(), + ); + + tracing::debug!("l1_height {l1_height} is _latestConfirmed {latest_confirmed}"); + + latest_confirmed + } +} + +impl EthereumChain for Arbitrum { + async fn execution_height_of_beacon_slot(&self, slot: u64) -> u64 { + // read `_latestConfirmed` at l1.execution_height(beacon_slot), then from there filter for `NodeConfirmed` + let latest_confirmed = self.latest_confirmed_at_beacon_slot(slot).await; + + let [event] = self + .l1 + .provider() + .get_logs( + ðers::types::Filter::new() + .select( + ethers::types::BlockNumber::Earliest..ethers::types::BlockNumber::Latest, + ) + .address(ethers::types::H160(self.l1_contract_address.0)) + .topic0(NodeConfirmed::signature()) + .topic1(ethers::types::H256( + U256::from(latest_confirmed).to_be_bytes(), + )), + ) + .await + .unwrap() + .try_into() + .unwrap(); + + let event: NodeConfirmed = + NodeConfirmed::decode_log(ðers::abi::RawLog::from(event)).unwrap(); + + tracing::debug!("_latestConfirmed {latest_confirmed}: {event}"); + + let block = self + .provider + .get_block(ethers::types::H256(event.block_hash.0)) + .await + .unwrap() + .unwrap(); + + block.number.unwrap().0[0] + } + + fn provider(&self) -> Arc> { + self.provider.clone() + } + + fn ibc_handler_address(&self) -> H160 { + self.ibc_handler_address + } + + async fn get_proof(&self, address: H160, location: U256, block: u64) -> StorageProof { + get_proof(self, address, location, block).await + } +} + +#[derive(Debug, ethers::contract::EthEvent, ethers::contract::EthDisplay)] +#[ethevent( + name = "NodeConfirmed", + abi = "NodeConfirmed(uint64 indexed, bytes32, bytes32)" +)] +struct NodeConfirmed { + node_num: u64, + block_hash: H256, + send_root: H256, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ArbitrumChainType; +impl FromStrExact for ArbitrumChainType { + const EXPECTING: &'static str = "arbitrum"; +} + +impl Chain for Arbitrum { + type ChainType = ArbitrumChainType; + + type SelfClientState = arbitrum::client_state::ClientState; + type SelfConsensusState = arbitrum::consensus_state::ConsensusState; + type Header = arbitrum::header::Header; + + type StoredClientState = Tr::SelfClientState; + type StoredConsensusState = Tr::SelfConsensusState; + + type Height = Height; + + type ClientId = ClientId; + + type IbcStateEncoding = EthAbi; + + type StateProof = StorageProof; + + type ClientType = String; + + type Error = Box; + + fn chain_id(&self) -> ChainIdOf { + self.chain_id + } + + async fn query_latest_height(&self) -> Result { + // the latest height of arbitrum is the latest height of the l1 light client on union + + let l1_client_state = protos::ibc::core::client::v1::query_client::QueryClient::connect( + self.union_grpc_url.clone(), + ) + .await? + .client_state(protos::ibc::core::client::v1::QueryClientStateRequest { + client_id: self.l1_client_id.to_string(), + }) + .await? + .into_inner() + .client_state + .ok_or("client state missing???")?; + + // don't worry about it + let Any(l1_client_state) = + < as Chain>::StoredClientState>>::try_from( + l1_client_state, + ) + .unwrap(); + + Ok(self.l1.make_height(l1_client_state.data.latest_slot)) + } + + async fn query_latest_height_as_destination(&self) -> Result { + todo!() + } + + async fn query_latest_timestamp(&self) -> Result { + todo!() + } + + async fn self_client_state(&self, height: Self::Height) -> Self::SelfClientState { + arbitrum::client_state::ClientState { + l1_client_id: self.l1_client_id.clone(), + chain_id: self.chain_id, + l1_latest_slot: height.revision_height, + l1_contract_address: self.l1_contract_address, + l1_latest_confirmed_slot: self.l1_latest_confirmed_slot, + l1_nodes_slot: self.l1_nodes_slot, + l1_nodes_confirm_data_offset: self.l1_nodes_confirm_data_offset, + frozen_height: Height { + revision_number: 0, + revision_height: 0, + }, + l2_ibc_contract_address: self.ibc_handler_address, + l2_ibc_commitment_slot: self.ibc_commitment_slot, + } + } + + async fn self_consensus_state(&self, height: Self::Height) -> Self::SelfConsensusState { + let arbitrum_height = ethers::types::BlockId::Number(ethers::types::BlockNumber::Number( + self.execution_height_of_beacon_slot(height.revision_height) + .await + .into(), + )); + + let storage_root = self + .provider + .get_storage_at( + ethers::types::H160(self.l1_contract_address.0), + H256::from(self.ibc_commitment_slot.to_be_bytes()).into(), + Some(arbitrum_height), + ) + .await + .unwrap(); + + arbitrum::consensus_state::ConsensusState { + ibc_storage_root: storage_root.0.into(), + timestamp: self + .provider + .get_block(arbitrum_height) + .await + .unwrap() + .unwrap() + .timestamp + .as_u64(), + } + } + + async fn read_ack( + &self, + tx_hash: unionlabs::hash::H256, + destination_channel_id: unionlabs::id::ChannelId, + destination_port_id: unionlabs::id::PortId, + sequence: std::num::NonZeroU64, + ) -> Vec { + todo!() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use hex_literal::hex; + + use super::*; + use crate::ethereum; + + #[tokio::test] + async fn fetch_block() { + // tracing_subscriber::fmt::init(); + + // let l1 = Ethereum::new(ethereum::Config { + // ibc_handler_address: H160(hex!("6b6b60a68b8dcbb170f25045974d10098917f816")), + // eth_rpc_api: "wss://eth-sepolia.g.alchemy.com/v2/6PCr1n8dJeYbE2Z9LrXScs05hLTYiVFl" + // .to_string(), + // eth_beacon_rpc_api: "https://lodestar-sepolia.chainsafe.io".to_string(), + // signers: (), + // }) + // .await + // .unwrap(); + + // let slot = l1.query_latest_height().await.unwrap(); + + // dbg!(slot); + + // let arbitrum = Arbitrum { + // chain_id: U256::from(421614), + // provider: Arc::new(Provider::new( + // Ws::connect("wss://arbitrum-sepolia.drpc.org") + // .await + // .unwrap(), + // )), + // ibc_handler_address: H160(hex!("61ba1780ecce6513872beb7ce698b49168010416")), + // l1, + // l1_contract_address: H160(hex!("d80810638dbDF9081b72C1B33c65375e807281C8")), + // l1_latest_confirmed_slot: U256::from(0x75), + // l1_client_id: "08-wasm-0".parse().unwrap(), + // union_grpc_url: "https://grpc.testnet.bonlulu.uno:443".to_string(), + // ibc_handlers: Pool::new(), + // }; + + // let block = arbitrum + // .execution_height_of_beacon_slot(slot.revision_height) + // .await; + + // dbg!(block); + } +} diff --git a/lib/chain-utils/src/ethereum.rs b/lib/chain-utils/src/ethereum.rs index d8a31e0c9a..73fbaef7d1 100644 --- a/lib/chain-utils/src/ethereum.rs +++ b/lib/chain-utils/src/ethereum.rs @@ -80,9 +80,10 @@ pub trait EthereumChain: address: H160, location: U256, block: u64, - ) -> impl Future; + ) -> impl Future; } +#[diagnostic::on_unimplemented(message = "{Self} does not implement `EthereumChain`")] pub trait EthereumChainExt: EthereumChain { /// Convenience method to construct an [`IBCHandler`] instance for this chain. fn ibc_handler(&self) -> IBCHandler> { @@ -108,41 +109,8 @@ impl EthereumChain for Ethereum { self.ibc_handler_address } - async fn get_proof( - &self, - address: H160, - location: U256, - block: u64, - ) -> unionlabs::ibc::lightclients::ethereum::storage_proof::StorageProof { - let proof = self - .provider - .get_proof( - ethers::types::H160::from(address), - vec![location.to_be_bytes().into()], - Some(block.into()), - ) - .await - .unwrap(); - - let proof = match <[_; 1]>::try_from(proof.storage_proof) { - Ok([proof]) => proof, - Err(invalid) => { - panic!("received invalid response from eth_getProof, expected length of 1 but got `{invalid:#?}`"); - } - }; - - unionlabs::ibc::lightclients::ethereum::storage_proof::StorageProof { - proofs: [unionlabs::ibc::lightclients::ethereum::proof::Proof { - key: U256::from_be_bytes(proof.key.to_fixed_bytes()), - value: proof.value.into(), - proof: proof - .proof - .into_iter() - .map(|bytes| bytes.to_vec()) - .collect(), - }] - .to_vec(), - } + async fn get_proof(&self, address: H160, location: U256, block: u64) -> StorageProof { + get_proof(self, address, location, block).await } } @@ -347,7 +315,7 @@ impl Chain for Ethereum { type Error = beacon_api::errors::Error; - type StateProof = unionlabs::ibc::lightclients::ethereum::storage_proof::StorageProof; + type StateProof = StorageProof; fn chain_id(&self) -> ::ChainId { self.chain_id @@ -542,6 +510,44 @@ impl Ethereum { } } +/// Fetch an eth_getProof call on an Ethereum-based chain that has the exact same response type as Ethereum. +pub async fn get_proof( + hc: &Hc, + address: H160, + location: U256, + block: u64, +) -> StorageProof { + let proof = hc + .provider() + .get_proof( + ethers::types::H160::from(address), + vec![location.to_be_bytes().into()], + Some(block.into()), + ) + .await + .unwrap(); + + let proof = match <[_; 1]>::try_from(proof.storage_proof) { + Ok([proof]) => proof, + Err(invalid) => { + panic!("received invalid response from eth_getProof, expected length of 1 but got `{invalid:#?}`"); + } + }; + + StorageProof { + proofs: [unionlabs::ibc::lightclients::ethereum::proof::Proof { + key: U256::from_be_bytes(proof.key.to_fixed_bytes()), + value: proof.value.into(), + proof: proof + .proof + .into_iter() + .map(|bytes| bytes.to_vec()) + .collect(), + }] + .to_vec(), + } +} + pub(crate) async fn read_ack( c: &Hc, tx_hash: H256, diff --git a/lib/chain-utils/src/lib.rs b/lib/chain-utils/src/lib.rs index eb2179e9e2..2f1eee6600 100644 --- a/lib/chain-utils/src/lib.rs +++ b/lib/chain-utils/src/lib.rs @@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::Arc}; use bip32::secp256k1::ecdsa; use crossbeam_queue::ArrayQueue; +use enumorph::Enumorph; use futures::Future; use serde::{Deserialize, Serialize}; use unionlabs::{ @@ -15,6 +16,7 @@ use unionlabs::{ }; use crate::{ + arbitrum::{Arbitrum, ArbitrumInitError}, cosmos::{Cosmos, CosmosInitError}, ethereum::{Ethereum, EthereumInitError}, private_key::PrivateKey, @@ -23,6 +25,7 @@ use crate::{ wasm::Wasm, }; +pub mod arbitrum; pub mod cosmos; pub mod ethereum; pub mod scroll; @@ -125,6 +128,7 @@ pub struct Chains { pub union: ChainMap, pub cosmos: ChainMap, pub scroll: ChainMap, + pub arbitrum: ChainMap, } impl GetChain for Chains { @@ -145,6 +149,12 @@ impl GetChain for Chains { } } +impl GetChain for Chains { + fn get_chain(&self, chain_id: &ChainIdOf) -> Option { + self.arbitrum.get(chain_id).cloned() + } +} + impl GetChain> for Chains { fn get_chain(&self, chain_id: &ChainIdOf>) -> Option> { self.union.get(chain_id).cloned().map(Wasm) @@ -176,6 +186,7 @@ pub enum ChainConfigType { Cosmos(cosmos::Config), Ethereum(EthereumChainConfig), Scroll(scroll::Config), + Arbitrum(arbitrum::Config), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -195,12 +206,14 @@ pub struct EthereumChainConfig { pub eth_beacon_rpc_api: String, } +#[derive(Debug, Enumorph)] pub enum AnyChain { Union(Union), Cosmos(Cosmos), EthereumMainnet(Ethereum), EthereumMinimal(Ethereum), Scroll(Scroll), + Arbitrum(Arbitrum), } #[derive(Debug, thiserror::Error)] @@ -213,6 +226,8 @@ pub enum AnyChainTryFromConfigError { Ethereum(#[from] EthereumInitError), #[error("error initializing a scroll chain")] Scroll(#[from] ScrollInitError), + #[error("error initializing an arbitrum chain")] + Arbitrum(#[from] ArbitrumInitError), } impl AnyChain { @@ -239,6 +254,7 @@ impl AnyChain { } } ChainConfigType::Scroll(scroll) => Self::Scroll(Scroll::new(scroll).await?), + ChainConfigType::Arbitrum(arbitrum) => Self::Arbitrum(Arbitrum::new(arbitrum).await?), }) } } @@ -272,6 +288,14 @@ impl LightClientType for Wasm { const TYPE: ClientType = ClientType::Wasm(WasmClientType::Scroll); } +impl LightClientType> for Arbitrum { + const TYPE: ClientType = ClientType::Cometbls; +} + +impl LightClientType for Wasm { + const TYPE: ClientType = ClientType::Wasm(WasmClientType::Arbitrum); +} + impl LightClientType> for Wasm { const TYPE: ClientType = ClientType::Wasm(WasmClientType::EthereumMainnet); } diff --git a/lib/chain-utils/src/scroll.rs b/lib/chain-utils/src/scroll.rs index 4032cbc086..4933d97cae 100644 --- a/lib/chain-utils/src/scroll.rs +++ b/lib/chain-utils/src/scroll.rs @@ -292,6 +292,7 @@ impl Chain for Scroll { scroll::client_state::ClientState { l1_client_id: self.l1_client_id.to_string(), chain_id: self.chain_id(), + // REVIEW: Should we query the l1 latest slot here? latest_slot: height.revision_height, latest_batch_index_slot: self.rollup_last_finalized_batch_index_slot, frozen_height: Height { diff --git a/lib/ethereum-verifier/src/error.rs b/lib/ethereum-verifier/src/error.rs index 33051f0737..b73022b292 100644 --- a/lib/ethereum-verifier/src/error.rs +++ b/lib/ethereum-verifier/src/error.rs @@ -45,7 +45,7 @@ pub enum Error { "irrelevant update since the order of the slots in the update data, and stored data is not correct" )] IrrelevantUpdate, - #[error("the order of the slots in the update data, and stored data is not correct")] + #[error("the order of the slots in the update data and stored data is not correct")] InvalidSlots, #[error( "signature period ({signature_period}) must be equal to `store_period` \ @@ -84,9 +84,10 @@ pub enum Error { ValueMissing { value: Vec }, #[error("trie error ({0:?})")] Trie(Box>), - #[error("rlp decoding failed ({0:?})")] + // we us debug here because the display implementation for rlp::DecoderError is stupid + #[error("rlp decoding failed: {0:?}")] RlpDecode(#[from] rlp::DecoderError), - #[error("custom query error")] + #[error("custom query error: {0}")] CustomQuery(#[from] unionlabs::cosmwasm::wasm::union::custom_query::Error), // boxed as this variant is significantly larger than the rest of the variants (due to the BlsSignature contained within) #[error(transparent)] diff --git a/lib/relay-message/Cargo.toml b/lib/relay-message/Cargo.toml index 4e5fca5084..e5e2c2fa5c 100644 --- a/lib/relay-message/Cargo.toml +++ b/lib/relay-message/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] arbitrary = { workspace = true, optional = true, features = ["derive"] } +arbitrum-verifier = { workspace = true } beacon-api = { workspace = true } chain-utils = { workspace = true } contracts = { workspace = true, features = ["providers"] } diff --git a/lib/relay-message/src/aggregate.rs b/lib/relay-message/src/aggregate.rs index d57a0bd3e0..7651da663c 100644 --- a/lib/relay-message/src/aggregate.rs +++ b/lib/relay-message/src/aggregate.rs @@ -115,6 +115,7 @@ pub enum Aggregate { } impl HandleAggregate for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] fn handle( self, data: VecDeque<::Data>, diff --git a/lib/relay-message/src/chain_impls.rs b/lib/relay-message/src/chain_impls.rs index fb3d1129b3..29d98e3f1a 100644 --- a/lib/relay-message/src/chain_impls.rs +++ b/lib/relay-message/src/chain_impls.rs @@ -119,5 +119,6 @@ pub mod cosmos_sdk; pub mod cosmos; pub mod union; +pub mod arbitrum; pub mod ethereum; pub mod scroll; diff --git a/lib/relay-message/src/chain_impls/arbitrum.rs b/lib/relay-message/src/chain_impls/arbitrum.rs new file mode 100644 index 0000000000..708f7297c7 --- /dev/null +++ b/lib/relay-message/src/chain_impls/arbitrum.rs @@ -0,0 +1,599 @@ +use std::{collections::VecDeque, marker::PhantomData}; + +use chain_utils::{ + arbitrum::Arbitrum, + ethereum::{EthereumChain, EthereumChainExt, IbcHandlerExt}, +}; +use ethers::providers::Middleware; +use frunk::{hlist_pat, HList}; +use queue_msg::{ + aggregate, + aggregation::{do_aggregate, UseAggregate}, + data, effect, fetch, queue_msg, QueueMsg, +}; +use unionlabs::{ + encoding::{Decode, Encode, EthAbi}, + hash::H160, + ibc::{ + core::client::msg_update_client::MsgUpdateClient, + lightclients::{ + arbitrum, + ethereum::{account_proof::AccountProof, storage_proof::StorageProof}, + }, + }, + ics24::ClientStatePath, + traits::{Chain, ClientStateOf, HeightOf, IbcStateEncodingOf}, + uint::U256, +}; + +use crate::{ + aggregate::{Aggregate, AnyAggregate}, + chain_impls::ethereum::{ + do_msg, fetch_get_proof, fetch_ibc_state, EthereumConfig, FetchIbcState, GetProof, + TxSubmitError, + }, + data::{AnyData, Data}, + effect::{AnyEffect, Effect, MsgUpdateClientData}, + fetch::{AnyFetch, DoFetch, Fetch, FetchUpdateHeaders}, + id, identified, + use_aggregate::IsAggregateData, + AnyLightClientIdentified, ChainExt, DoAggregate, DoFetchProof, DoFetchState, + DoFetchUpdateHeaders, DoMsg, Identified, PathOf, RelayMessageTypes, +}; + +impl ChainExt for Arbitrum { + type Data = ArbitrumData; + type Fetch = ArbitrumFetch; + type Aggregate = ArbitrumAggregate; + + type MsgError = TxSubmitError; + + type Config = EthereumConfig; +} + +impl DoMsg for Arbitrum +where + ClientStateOf: Encode, + Tr: ChainExt< + SelfConsensusState: Encode, + SelfClientState: Encode, + Header: Encode, + StoredClientState: Encode, + StateProof: Encode, + >, +{ + async fn msg(&self, msg: Effect) -> Result<(), Self::MsgError> { + do_msg(&self.ibc_handlers, msg, false).await + } +} + +impl DoFetchProof for Arbitrum +where + AnyLightClientIdentified: From)>, +{ + fn proof( + c: &Self, + at: HeightOf, + path: PathOf, + ) -> QueueMsg { + fetch(id::( + c.chain_id(), + Fetch::::specific(GetProof { path, height: at }), + )) + } +} + +// REVIEW: This can probably be generic over Hc: EthereumChain, instead of being duplicated between ethereum and arbitrum +impl DoFetchState for Arbitrum +where + Tr: ChainExt> + Encode>, + + AnyLightClientIdentified: From)>, +{ + fn state( + hc: &Self, + at: HeightOf, + path: PathOf, + ) -> QueueMsg { + fetch(id::( + hc.chain_id(), + Fetch::::specific(FetchIbcState { path, height: at }), + )) + } + + async fn query_unfinalized_trusted_client_state( + hc: &Self, + client_id: Self::ClientId, + ) -> Self::StoredClientState { + let latest_arbitrum_height = hc.provider.get_block_number().await.unwrap().as_u64(); + + hc.ibc_handler() + .ibc_state_read::<_, Arbitrum, Tr>( + latest_arbitrum_height, + ClientStatePath { client_id }, + ) + .await + .unwrap() + } +} + +impl DoFetchUpdateHeaders for Arbitrum +where + AnyLightClientIdentified: From)>, + AnyLightClientIdentified: From)>, + Tr: ChainExt, +{ + fn fetch_update_headers( + c: &Self, + update_info: FetchUpdateHeaders, + ) -> QueueMsg { + aggregate( + [ + fetch(id( + c.chain_id(), + Fetch::specific(FetchL1ContractRootProof { + height: update_info.update_to, + l1_contract_address: c.l1_contract_address, + }), + )), + fetch(id( + c.chain_id(), + Fetch::specific(FetchIbcContractRootProof { + height: update_info.update_to, + ibc_contract_address: c.ibc_handler_address, + }), + )), + fetch(id( + c.chain_id(), + Fetch::specific(FetchLatestConfirmedProofs { + height: update_info.update_to, + l1_latest_confirmed_slot: c.l1_latest_confirmed_slot, + l1_nodes_slot: c.l1_nodes_slot, + l1_contract_address: c.l1_contract_address, + }), + )), + fetch(id( + c.chain_id(), + Fetch::specific(FetchL2Header { + height: update_info.update_to, + }), + )), + ], + [], + id( + c.chain_id(), + Aggregate::::specific(AggregateHeader { req: update_info }), + ), + ) + } +} + +impl DoFetch for ArbitrumFetch +where + AnyLightClientIdentified: From)>, + Tr: ChainExt< + SelfClientState: Decode>, + SelfConsensusState: Decode> + Encode, + >, +{ + async fn do_fetch(arbitrum: &Arbitrum, msg: Self) -> QueueMsg { + let msg = match msg { + Self::FetchGetProof(get_proof) => fetch_get_proof(arbitrum, get_proof).await, + Self::FetchIbcState(ibc_state) => fetch_ibc_state(arbitrum, ibc_state).await, + Self::FetchL1ContractRootProof(FetchL1ContractRootProof { + height, + l1_contract_address, + }) => { + let account_proof = arbitrum + .l1 + .provider() + .get_proof( + ethers::types::H160::from(l1_contract_address), + vec![], + Some(ethers::types::BlockId::Number( + arbitrum + .l1 + .execution_height_of_beacon_slot(height.revision_height) + .await + .into(), + )), + ) + .await + .unwrap(); + + Data::specific(L1ContractRootProof { + height, + proof: AccountProof { + storage_root: account_proof.storage_hash.into(), + proof: account_proof + .account_proof + .into_iter() + .map(|x| x.to_vec()) + .collect(), + }, + __marker: PhantomData, + }) + } + Self::FetchIbcContractRootProof(FetchIbcContractRootProof { + height, + ibc_contract_address, + }) => { + let arbitrum_height = arbitrum + .execution_height_of_beacon_slot(height.revision_height) + .await; + + let proof = arbitrum + .provider + .get_proof( + ethers::types::H160(ibc_contract_address.0), + vec![], + Some(ethers::types::BlockNumber::Number(arbitrum_height.into()).into()), + ) + .await + .unwrap(); + + Data::specific(IbcContractRootProof { + height, + proof: AccountProof { + storage_root: proof.storage_hash.0.into(), + proof: proof + .account_proof + .into_iter() + .map(|x| x.to_vec()) + .collect(), + }, + __marker: PhantomData, + }) + } + Self::FetchLatestConfirmedProofs(FetchLatestConfirmedProofs { + height, + l1_latest_confirmed_slot, + l1_nodes_slot, + l1_contract_address, + }) => { + let l1_height = arbitrum + .l1 + .execution_height_of_beacon_slot(height.revision_height) + .await; + + let latest_confirmed = arbitrum + .latest_confirmed_at_beacon_slot(height.revision_height) + .await; + + let [latest_confirmed_slot_proof, nodes_slot_proof] = arbitrum + .l1 + .provider + .get_proof( + ethers::types::H160(l1_contract_address.0), + vec![ + l1_latest_confirmed_slot.to_be_bytes().into(), + arbitrum_verifier::nodes_confirm_data_mapping_key( + l1_nodes_slot, + latest_confirmed.into(), + arbitrum.l1_nodes_confirm_data_offset, + ) + .to_be_bytes() + .into(), + ], + Some(ethers::types::BlockNumber::Number(l1_height.into()).into()), + ) + .await + .unwrap() + .storage_proof + .try_into() + .unwrap(); + + Data::specific(LatestConfirmedProofs { + height, + latest_confirmed, + // TODO: Extract this logic into a fn, we do it all over the place + latest_confirmed_slot_proof: StorageProof { + proofs: [unionlabs::ibc::lightclients::ethereum::proof::Proof { + key: U256::from_be_bytes(latest_confirmed_slot_proof.key.0), + value: latest_confirmed_slot_proof.value.into(), + proof: latest_confirmed_slot_proof + .proof + .into_iter() + .map(|bytes| bytes.to_vec()) + .collect(), + }] + .to_vec(), + }, + nodes_slot_proof: StorageProof { + proofs: [unionlabs::ibc::lightclients::ethereum::proof::Proof { + key: U256::from_be_bytes(nodes_slot_proof.key.0), + value: nodes_slot_proof.value.into(), + proof: nodes_slot_proof + .proof + .into_iter() + .map(|bytes| bytes.to_vec()) + .collect(), + }] + .to_vec(), + }, + __marker: PhantomData, + }) + } + Self::FetchL2Header(FetchL2Header { height }) => { + let arbitrum_height = arbitrum + .execution_height_of_beacon_slot(height.revision_height) + .await; + + let block = arbitrum + .provider + .get_block(ethers::types::BlockNumber::Number(arbitrum_height.into())) + .await + .unwrap() + .unwrap(); + + let l2_header = arbitrum::l2_header::L2Header { + parent_hash: block.parent_hash.0.into(), + sha3_uncles: block.uncles_hash.0.into(), + miner: block.author.unwrap().0.into(), + state_root: block.state_root.0.into(), + transactions_root: block.transactions_root.0.into(), + receipts_root: block.receipts_root.0.into(), + logs_bloom: Box::new(block.logs_bloom.unwrap().0.into()), + difficulty: block.difficulty.into(), + number: block.number.unwrap().as_u64().into(), + gas_limit: block.gas_limit.as_u64(), + gas_used: block.gas_used.as_u64(), + timestamp: block.timestamp.as_u64(), + extra_data: block.extra_data.try_into().unwrap(), + mix_hash: block.mix_hash.unwrap().0.into(), + nonce: block.nonce.unwrap().0.into(), + base_fee_per_gas: block.base_fee_per_gas.unwrap().into(), + }; + + Data::specific(L2Header { + height, + l2_header, + __marker: PhantomData, + }) + } + }; + + data(id::(arbitrum.chain_id(), msg)) + } +} + +#[queue_msg] +#[derive(enumorph::Enumorph)] +pub enum ArbitrumFetch { + FetchGetProof(GetProof), + FetchIbcState(FetchIbcState), + + // - arbitrum rollup contract root proof + FetchL1ContractRootProof(FetchL1ContractRootProof), + // - ibc contract root against finalized root on L2 + FetchIbcContractRootProof(FetchIbcContractRootProof), + /// Fetch the latest confirmed node and the relevant proofs. + FetchLatestConfirmedProofs(FetchLatestConfirmedProofs), + /// Fetch the Arbitrum header. + FetchL2Header(FetchL2Header), +} + +#[queue_msg] +pub struct FetchL1ContractRootProof { + // the height to update to + pub height: HeightOf, + pub l1_contract_address: H160, +} + +#[queue_msg] +pub struct FetchIbcContractRootProof { + // the height to update to + pub height: HeightOf, + pub ibc_contract_address: H160, +} + +#[queue_msg] +pub struct FetchLatestConfirmedProofs { + pub height: HeightOf, + pub l1_latest_confirmed_slot: U256, + pub l1_nodes_slot: U256, + pub l1_contract_address: H160, +} + +#[queue_msg] +pub struct FetchL2Header { + // the height to update to + pub height: HeightOf, +} + +#[queue_msg] +#[derive(enumorph::Enumorph)] +pub enum ArbitrumData { + L1ContractRootProof(L1ContractRootProof), + IbcContractRootProof(IbcContractRootProof), + LatestConfirmedProofs(LatestConfirmedProofs), + L2Header(L2Header), +} + +try_from_relayer_msg! { + chain = Arbitrum, + generics = (Tr: ChainExt), + msgs = ArbitrumData( + L1ContractRootProof(L1ContractRootProof), + IbcContractRootProof(IbcContractRootProof), + LatestConfirmedProofs(LatestConfirmedProofs), + L2Header(L2Header), + ), +} + +#[queue_msg] +pub struct L1ContractRootProof<#[cover] Tr: ChainExt> { + pub height: HeightOf, + pub proof: AccountProof, +} + +#[queue_msg] +pub struct IbcContractRootProof<#[cover] Tr: ChainExt> { + pub height: HeightOf, + pub proof: AccountProof, +} + +#[queue_msg] +pub struct LatestConfirmedProofs<#[cover] Tr: ChainExt> { + pub height: HeightOf, + pub latest_confirmed: u64, + pub latest_confirmed_slot_proof: StorageProof, + pub nodes_slot_proof: StorageProof, +} + +#[queue_msg] +pub struct L2Header<#[cover] Tr: ChainExt> { + pub height: HeightOf, + pub l2_header: arbitrum::l2_header::L2Header, +} + +#[queue_msg] +#[derive(enumorph::Enumorph)] +pub enum ArbitrumAggregate { + AggregateHeader(AggregateHeader), +} + +#[queue_msg] +pub struct AggregateHeader { + pub req: FetchUpdateHeaders, +} + +impl DoAggregate for Identified> +where + Identified>: IsAggregateData, + Identified>: IsAggregateData, + Identified>: IsAggregateData, + Identified>: IsAggregateData, + + AnyLightClientIdentified: From)>, + AnyLightClientIdentified: From)>, +{ + fn do_aggregate( + Identified { + chain_id, + t, + __marker, + }: Self, + data: VecDeque>, + ) -> QueueMsg { + match t { + ArbitrumAggregate::AggregateHeader(msg) => do_aggregate(id(chain_id, msg), data), + } + } +} + +impl UseAggregate for Identified> +where + Tr: ChainExt, + Identified>: IsAggregateData, + Identified>: IsAggregateData, + Identified>: IsAggregateData, + Identified>: IsAggregateData, + + AnyLightClientIdentified: From)>, +{ + type AggregatedData = HList![ + Identified>, + Identified>, + Identified>, + Identified>, + ]; + + fn aggregate( + Identified { + chain_id, + t: AggregateHeader { req }, + __marker: _, + }: Self, + hlist_pat![ + Identified { + chain_id: l1_contract_root_proof_chain_id, + t: L1ContractRootProof { + height: l1_contract_root_proof_height, + proof: l1_contract_root_proof, + __marker: _ + }, + __marker: _, + }, + Identified { + chain_id: ibc_contract_root_proof_chain_id, + t: IbcContractRootProof { + height: ibc_contract_root_proof_height, + proof: ibc_contract_root_proof, + __marker: _ + }, + __marker: _, + }, + Identified { + chain_id: latest_confirmed_proofs_chain_id, + t: LatestConfirmedProofs { + height: latest_confirmed_proofs_height, + latest_confirmed, + latest_confirmed_slot_proof, + nodes_slot_proof, + __marker: _ + }, + __marker: _, + }, + Identified { + chain_id: l2_header_proof_chain_id, + t: L2Header { + height: l2_header_proof_height, + l2_header, + __marker: _, + }, + __marker: _, + }, + ]: Self::AggregatedData, + ) -> QueueMsg { + // assert_eq!(rollup_contract_root_proof_chain_id, chain_id); + // assert_eq!(latest_batch_index_proof_chain_id, chain_id); + // assert_eq!(arbitrum_finalized_root_proof_chain_id, chain_id); + // assert_eq!(ibc_contract_root_proof_chain_id, chain_id); + // assert_eq!(batch_hash_proof_chain_id, chain_id); + // assert_eq!(commit_batch_transaction_input_chain_id, chain_id); + + // assert_eq!( + // rollup_contract_root_proof_height, + // latest_batch_index_proof_height + // ); + // assert_eq!( + // rollup_contract_root_proof_height, + // arbitrum_finalized_root_proof_height + // ); + // assert_eq!( + // rollup_contract_root_proof_height, + // ibc_contract_root_proof_height + // ); + // assert_eq!(rollup_contract_root_proof_height, batch_hash_proof_height); + // assert_eq!( + // rollup_contract_root_proof_height, + // commit_batch_transaction_input_height + // ); + + // assert_eq!( + // arbitrum_finalized_root_proof_batch_index, + // batch_hash_proof_batch_index + // ); + // assert_eq!( + // arbitrum_finalized_root_proof_batch_index, + // commit_batch_transaction_input_batch_index + // ); + + effect(id::( + req.counterparty_chain_id, + MsgUpdateClientData(MsgUpdateClient { + client_id: req.counterparty_client_id, + client_message: arbitrum::header::Header { + l1_height: req.update_to, + l1_account_proof: l1_contract_root_proof, + l2_ibc_account_proof: ibc_contract_root_proof, + latest_confirmed, + l1_latest_confirmed_slot_proof: latest_confirmed_slot_proof, + l1_nodes_slot_proof: nodes_slot_proof, + l2_header, + }, + }), + )) + } +} diff --git a/lib/relay-message/src/chain_impls/ethereum.rs b/lib/relay-message/src/chain_impls/ethereum.rs index 1749b53c69..bbd028335b 100644 --- a/lib/relay-message/src/chain_impls/ethereum.rs +++ b/lib/relay-message/src/chain_impls/ethereum.rs @@ -17,7 +17,7 @@ use ethers::{ abi::{AbiDecode, AbiEncode}, contract::{ContractError, EthCall}, providers::{Middleware, ProviderError}, - types::Bytes, + types::{transaction::eip2718::TypedTransaction, Bytes}, utils::keccak256, }; use frunk::{hlist_pat, HList}; @@ -312,6 +312,14 @@ where ), }; + // match msg.tx { + // TypedTransaction::Legacy(ref mut tx) => { + // // + // tx + // } + // _ => {} + // } + let msg = if legacy { msg.legacy() } else { msg }; tracing::debug!(?msg, "submitting evm tx"); diff --git a/lib/relay-message/src/data.rs b/lib/relay-message/src/data.rs index 0cec33ba8e..93fa492087 100644 --- a/lib/relay-message/src/data.rs +++ b/lib/relay-message/src/data.rs @@ -62,6 +62,7 @@ pub enum Data { // Passthrough since we don't want to handle any top-level data, just bubble it up to the top level. impl HandleData for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] fn handle( self, _: &::Store, diff --git a/lib/relay-message/src/effect.rs b/lib/relay-message/src/effect.rs index e96f6c2d15..55c0162197 100644 --- a/lib/relay-message/src/effect.rs +++ b/lib/relay-message/src/effect.rs @@ -45,6 +45,7 @@ pub enum Effect { } impl HandleEffect for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] async fn handle( self, store: &::Store, diff --git a/lib/relay-message/src/event.rs b/lib/relay-message/src/event.rs index b8fb226195..be1e9d9d55 100644 --- a/lib/relay-message/src/event.rs +++ b/lib/relay-message/src/event.rs @@ -37,6 +37,7 @@ pub enum Event { } impl HandleEvent for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] fn handle( self, store: &::Store, diff --git a/lib/relay-message/src/fetch.rs b/lib/relay-message/src/fetch.rs index acf54df7eb..8706dcfeb9 100644 --- a/lib/relay-message/src/fetch.rs +++ b/lib/relay-message/src/fetch.rs @@ -45,6 +45,7 @@ pub enum Fetch { } impl HandleFetch for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] async fn handle( self, store: &::Store, diff --git a/lib/relay-message/src/lib.rs b/lib/relay-message/src/lib.rs index ea30243a1e..e4ddc7f5e4 100644 --- a/lib/relay-message/src/lib.rs +++ b/lib/relay-message/src/lib.rs @@ -4,7 +4,8 @@ use std::{collections::VecDeque, fmt::Debug, future::Future, marker::PhantomData}; use chain_utils::{ - cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, wasm::Wasm, Chains, + arbitrum::Arbitrum, cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, + wasm::Wasm, Chains, }; use frame_support_procedural::{CloneNoBound, DebugNoBound, PartialEqNoBound}; use queue_msg::{seq, QueueMessageTypes, QueueMsg, QueueMsgTypesTraits}; @@ -229,6 +230,11 @@ pub enum AnyLightClientIdentified { /// The solidity client on Scroll tracking the state of Wasm. UnionOnScroll(lc!(Wasm => Scroll)), + /// The 08-wasm client tracking the state of Arbitrum. + ArbitrumOnUnion(lc!(Arbitrum => Wasm)), + /// The solidity client on Arbitrum tracking the state of Wasm. + UnionOnArbitrum(lc!(Wasm => Arbitrum)), + /// The 08-wasm client tracking the state of Cosmos. CosmosOnUnion(lc!(Wasm => Union)), /// The solidity client on Cosmos tracking the state of Wasm. @@ -238,6 +244,16 @@ pub enum AnyLightClientIdentified { CosmosOnCosmos(lc!(Cosmos => Cosmos)), } +impl AnyLightClientIdentified { + fn chain_id(&self) -> String { + let i = self; + + any_lc! { + |i| i.chain_id.to_string() + } + } +} + #[derive(Serialize, Deserialize)] #[serde(bound(serialize = "", deserialize = ""), untagged, deny_unknown_fields)] #[allow(clippy::large_enum_variant)] @@ -259,6 +275,9 @@ enum AnyLightClientIdentifiedSerde { ScrollOnUnion(Inner, Scroll, lc!(Scroll => Wasm)>), UnionOnScroll(Inner, lc!(Wasm => Scroll)>), + ArbitrumOnUnion(Inner, Arbitrum, lc!(Arbitrum => Wasm)>), + UnionOnArbitrum(Inner, lc!(Wasm => Arbitrum)>), + CosmosOnUnion(Inner, lc!(Wasm => Union)>), UnionOnCosmos(Inner, Union, lc!(Union => Wasm)>), CosmosOnCosmos(Inner Cosmos)>), @@ -281,6 +300,8 @@ impl From> for AnyLightClientIden } AnyLightClientIdentified::ScrollOnUnion(t) => Self::ScrollOnUnion(Inner::new(t)), AnyLightClientIdentified::UnionOnScroll(t) => Self::UnionOnScroll(Inner::new(t)), + AnyLightClientIdentified::ArbitrumOnUnion(t) => Self::ArbitrumOnUnion(Inner::new(t)), + AnyLightClientIdentified::UnionOnArbitrum(t) => Self::UnionOnArbitrum(Inner::new(t)), AnyLightClientIdentified::CosmosOnUnion(t) => Self::CosmosOnUnion(Inner::new(t)), AnyLightClientIdentified::UnionOnCosmos(t) => Self::UnionOnCosmos(Inner::new(t)), AnyLightClientIdentified::CosmosOnCosmos(t) => Self::CosmosOnCosmos(Inner::new(t)), @@ -305,6 +326,8 @@ impl From> for AnyLightClien } AnyLightClientIdentifiedSerde::ScrollOnUnion(t) => Self::ScrollOnUnion(t.inner), AnyLightClientIdentifiedSerde::UnionOnScroll(t) => Self::UnionOnScroll(t.inner), + AnyLightClientIdentifiedSerde::ArbitrumOnUnion(t) => Self::ArbitrumOnUnion(t.inner), + AnyLightClientIdentifiedSerde::UnionOnArbitrum(t) => Self::UnionOnArbitrum(t.inner), AnyLightClientIdentifiedSerde::CosmosOnUnion(t) => Self::CosmosOnUnion(t.inner), AnyLightClientIdentifiedSerde::UnionOnCosmos(t) => Self::UnionOnCosmos(t.inner), AnyLightClientIdentifiedSerde::CosmosOnCosmos(t) => Self::CosmosOnCosmos(t.inner), @@ -493,6 +516,23 @@ macro_rules! any_lc { $expr } + AnyLightClientIdentified::ArbitrumOnUnion($msg) => { + #[allow(dead_code)] + type Hc = chain_utils::wasm::Wasm; + #[allow(dead_code)] + type Tr = chain_utils::arbitrum::Arbitrum; + + $expr + } + AnyLightClientIdentified::UnionOnArbitrum($msg) => { + #[allow(dead_code)] + type Hc = chain_utils::arbitrum::Arbitrum; + #[allow(dead_code)] + type Tr = chain_utils::wasm::Wasm; + + $expr + } + AnyLightClientIdentified::CosmosOnUnion($msg) => { #[allow(dead_code)] type Hc = chain_utils::union::Union; diff --git a/lib/relay-message/src/wait.rs b/lib/relay-message/src/wait.rs index b56d3669e0..f3e70d82d1 100644 --- a/lib/relay-message/src/wait.rs +++ b/lib/relay-message/src/wait.rs @@ -30,6 +30,7 @@ pub enum Wait { } impl HandleWait for AnyLightClientIdentified { + #[tracing::instrument(skip_all, fields(chain_id = %self.chain_id()))] async fn handle( self, store: &::Store, diff --git a/lib/unionlabs/Cargo.toml b/lib/unionlabs/Cargo.toml index 211fa21fdd..2e811e6403 100644 --- a/lib/unionlabs/Cargo.toml +++ b/lib/unionlabs/Cargo.toml @@ -44,6 +44,7 @@ serde = { workspace = true, features = ["derive"] } serde-utils = { workspace = true } serde_json = { workspace = true, optional = true } sha2 = { workspace = true } +sha3.workspace = true ssz = { workspace = true } static_assertions = "1.1.0" subtle-encoding = { workspace = true, features = ["bech32-preview"] } diff --git a/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs b/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs index 1863ee211d..ab98b917c8 100644 --- a/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs +++ b/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs @@ -85,6 +85,7 @@ use { pub fn query_consensus_state( deps: Deps, env: &Env, + // TODO: Use ClientId here client_id: String, height: Height, ) -> Result diff --git a/lib/unionlabs/src/hash.rs b/lib/unionlabs/src/hash.rs index c7cff8b367..a148a4ec30 100644 --- a/lib/unionlabs/src/hash.rs +++ b/lib/unionlabs/src/hash.rs @@ -1,10 +1,12 @@ use crate::macros::hex_string_array_wrapper; hex_string_array_wrapper! { + pub struct H64(pub [u8; 8]); pub struct H160(pub [u8; 20]); pub struct H256(pub [u8; 32]); pub struct H384(pub [u8; 48]); pub struct H512(pub [u8; 64]); + pub struct H2048(pub [u8; 256]); } impl H256 { diff --git a/lib/unionlabs/src/ibc/lightclients.rs b/lib/unionlabs/src/ibc/lightclients.rs index 1c30d8982e..5efa8408b1 100644 --- a/lib/unionlabs/src/ibc/lightclients.rs +++ b/lib/unionlabs/src/ibc/lightclients.rs @@ -1,3 +1,4 @@ +pub mod arbitrum; pub mod cometbls; pub mod ethereum; pub mod scroll; diff --git a/lib/unionlabs/src/ibc/lightclients/arbitrum.rs b/lib/unionlabs/src/ibc/lightclients/arbitrum.rs new file mode 100644 index 0000000000..f568d8c80d --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/arbitrum.rs @@ -0,0 +1,4 @@ +pub mod client_state; +pub mod consensus_state; +pub mod header; +pub mod l2_header; diff --git a/lib/unionlabs/src/ibc/lightclients/arbitrum/client_state.rs b/lib/unionlabs/src/ibc/lightclients/arbitrum/client_state.rs new file mode 100644 index 0000000000..56d659bfc9 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/arbitrum/client_state.rs @@ -0,0 +1,104 @@ +use macros::model; +use uint::FromDecStrErr; + +use crate::{ + errors::{required, InvalidLength, MissingField}, + hash::H160, + ibc::core::client::height::Height, + id::{ClientId, ClientIdValidator}, + uint::U256, + validated::{Validate, ValidateT}, +}; + +#[model(proto( + raw(protos::union::ibc::lightclients::arbitrum::v1::ClientState), + from, + into +))] +pub struct ClientState { + pub l1_client_id: ClientId, + pub chain_id: U256, + pub l1_latest_slot: u64, + pub l1_contract_address: H160, + pub l1_latest_confirmed_slot: U256, + pub l1_nodes_slot: U256, + // TODO: Rename this in the protos + pub l1_nodes_confirm_data_offset: U256, + pub frozen_height: Height, + pub l2_ibc_contract_address: H160, + pub l2_ibc_commitment_slot: U256, +} + +impl TryFrom for ClientState { + type Error = TryFromClientStateError; + + fn try_from( + value: protos::union::ibc::lightclients::arbitrum::v1::ClientState, + ) -> Result { + Ok(Self { + l1_client_id: value + .l1_client_id + .validate() + .map_err(TryFromClientStateError::L1ClientId)?, + chain_id: value + .chain_id + .parse() + .map_err(TryFromClientStateError::ChainId)?, + l1_latest_slot: value.l1_latest_slot, + l1_contract_address: value + .l1_contract_address + .try_into() + .map_err(TryFromClientStateError::L1ContractAddress)?, + l1_latest_confirmed_slot: U256::try_from_be_bytes(&value.l1_latest_confirmed_slot) + .map_err(TryFromClientStateError::L1LatestConfirmedSlot)?, + l1_nodes_slot: U256::try_from_be_bytes(&value.l1_nodes_slot) + .map_err(TryFromClientStateError::L1NodesSlot)?, + l1_nodes_confirm_data_offset: U256::try_from_be_bytes(&value.confirm_data_offset) + .map_err(TryFromClientStateError::ConfirmDataOffset)?, + frozen_height: required!(value.frozen_height)?.into(), + l2_ibc_contract_address: value + .l2_ibc_contract_address + .try_into() + .map_err(TryFromClientStateError::L2IbcContractAddress)?, + l2_ibc_commitment_slot: U256::try_from_be_bytes(&value.l2_ibc_commitment_slot) + .map_err(TryFromClientStateError::L2IbcContractAddress)?, + }) + } +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum TryFromClientStateError { + #[error(transparent)] + MissingField(MissingField), + #[error("invalid l1 client id")] + L1ClientId(#[source] >::Error), + #[error("invalid l1 contract address")] + ChainId(#[source] FromDecStrErr), + #[error("invalid l1 latest confirmed slot")] + L1ContractAddress(#[source] InvalidLength), + #[error("invalid l1 nodes slot")] + L1LatestConfirmedSlot(#[source] InvalidLength), + #[error("invalid confirm data offset")] + L1NodesSlot(#[source] InvalidLength), + #[error("invalid frozen height")] + ConfirmDataOffset(#[source] InvalidLength), + #[error("invalid l2 ibc commitment slot")] + L2IbcContractAddress(#[source] InvalidLength), +} + +impl From for protos::union::ibc::lightclients::arbitrum::v1::ClientState { + fn from(value: ClientState) -> Self { + Self { + l1_client_id: value.l1_client_id.to_string(), + chain_id: value.chain_id.to_string(), + l1_latest_slot: value.l1_latest_slot, + l1_contract_address: value.l1_contract_address.into(), + l1_latest_confirmed_slot: value.l1_latest_confirmed_slot.to_be_bytes().to_vec(), + l1_nodes_slot: value.l1_nodes_slot.to_be_bytes().to_vec(), + confirm_data_offset: value.l1_nodes_confirm_data_offset.to_be_bytes().to_vec(), + frozen_height: Some(value.frozen_height.into()), + l2_ibc_contract_address: value.l2_ibc_contract_address.into(), + l2_ibc_commitment_slot: value.l2_ibc_commitment_slot.to_be_bytes().to_vec(), + } + } +} diff --git a/lib/unionlabs/src/ibc/lightclients/arbitrum/consensus_state.rs b/lib/unionlabs/src/ibc/lightclients/arbitrum/consensus_state.rs new file mode 100644 index 0000000000..f371150b43 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/arbitrum/consensus_state.rs @@ -0,0 +1,44 @@ +use macros::model; + +use crate::{errors::InvalidLength, hash::H256}; + +#[model(proto( + raw(protos::union::ibc::lightclients::arbitrum::v1::ConsensusState), + into, + from +))] +pub struct ConsensusState { + pub ibc_storage_root: H256, + pub timestamp: u64, +} + +impl TryFrom for ConsensusState { + type Error = TryFromConsensusStateError; + + fn try_from( + value: protos::union::ibc::lightclients::arbitrum::v1::ConsensusState, + ) -> Result { + Ok(Self { + ibc_storage_root: value + .ibc_storage_root + .try_into() + .map_err(TryFromConsensusStateError::IbcStorageRoot)?, + timestamp: value.timestamp, + }) + } +} + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum TryFromConsensusStateError { + #[error("invalid ibc storage root")] + IbcStorageRoot(#[source] InvalidLength), +} + +impl From for protos::union::ibc::lightclients::arbitrum::v1::ConsensusState { + fn from(value: ConsensusState) -> Self { + Self { + ibc_storage_root: value.ibc_storage_root.into(), + timestamp: value.timestamp, + } + } +} diff --git a/lib/unionlabs/src/ibc/lightclients/arbitrum/header.rs b/lib/unionlabs/src/ibc/lightclients/arbitrum/header.rs new file mode 100644 index 0000000000..f3770e3815 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/arbitrum/header.rs @@ -0,0 +1,88 @@ +use macros::model; + +use crate::{ + errors::{required, MissingField}, + ibc::{ + core::client::height::Height, + lightclients::{ + arbitrum::l2_header::{L2Header, TryFromL2HeaderError}, + ethereum::{ + account_proof::{AccountProof, TryFromAccountProofError}, + storage_proof::{StorageProof, TryFromStorageProofError}, + }, + }, + }, +}; + +#[model(proto( + raw(protos::union::ibc::lightclients::arbitrum::v1::Header), + into, + from +))] +pub struct Header { + pub l1_height: Height, + pub l1_account_proof: AccountProof, + pub l2_ibc_account_proof: AccountProof, + pub latest_confirmed: u64, + pub l1_latest_confirmed_slot_proof: StorageProof, + pub l1_nodes_slot_proof: StorageProof, + pub l2_header: L2Header, +} + +impl TryFrom for Header { + type Error = TryFromHeaderError; + + fn try_from( + value: protos::union::ibc::lightclients::arbitrum::v1::Header, + ) -> Result { + Ok(Self { + l1_height: required!(value.l1_height)?.into(), + l1_account_proof: required!(value.l1_account_proof)? + .try_into() + .map_err(TryFromHeaderError::L1AccountProof)?, + l2_ibc_account_proof: required!(value.l2_ibc_account_proof)? + .try_into() + .map_err(TryFromHeaderError::L2IbcAccountProof)?, + latest_confirmed: value.latest_confirmed, + l1_latest_confirmed_slot_proof: required!(value.l1_latest_confirmed_slot_proof)? + .try_into() + .map_err(TryFromHeaderError::L1LatestConfirmedSlotProof)?, + l1_nodes_slot_proof: required!(value.l1_nodes_slot_proof)? + .try_into() + .map_err(TryFromHeaderError::L1NodesSlotProof)?, + l2_header: required!(value.l2_header)? + .try_into() + .map_err(TryFromHeaderError::L2Header)?, + }) + } +} + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum TryFromHeaderError { + #[error(transparent)] + MissingField(MissingField), + #[error("invalid l1 account proof")] + L1AccountProof(TryFromAccountProofError), + #[error("invalid l2 ibc account proof")] + L2IbcAccountProof(TryFromAccountProofError), + #[error("invalid l1 latest confirmed slot proof")] + L1LatestConfirmedSlotProof(TryFromStorageProofError), + #[error("invalid l1 nodes slot proof")] + L1NodesSlotProof(TryFromStorageProofError), + #[error("invalid l2 header")] + L2Header(TryFromL2HeaderError), +} + +impl From
for protos::union::ibc::lightclients::arbitrum::v1::Header { + fn from(value: Header) -> Self { + Self { + l1_height: Some(value.l1_height.into()), + l1_account_proof: Some(value.l1_account_proof.into()), + l2_ibc_account_proof: Some(value.l2_ibc_account_proof.into()), + latest_confirmed: value.latest_confirmed, + l1_latest_confirmed_slot_proof: Some(value.l1_latest_confirmed_slot_proof.into()), + l1_nodes_slot_proof: Some(value.l1_nodes_slot_proof.into()), + l2_header: Some(value.l2_header.into()), + } + } +} diff --git a/lib/unionlabs/src/ibc/lightclients/arbitrum/l2_header.rs b/lib/unionlabs/src/ibc/lightclients/arbitrum/l2_header.rs new file mode 100644 index 0000000000..370c247f46 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/arbitrum/l2_header.rs @@ -0,0 +1,222 @@ +use macros::model; +use rlp::Encodable; +use sha2::Digest; +use sha3::Keccak256; + +use crate::{ + errors::InvalidLength, + hash::{H160, H2048, H256, H64}, + uint::U256, +}; + +#[model(proto( + raw(protos::union::ibc::lightclients::arbitrum::v1::L2Header), + into, + from +))] +#[derive(rlp::RlpEncodable)] +pub struct L2Header { + pub parent_hash: H256, + pub sha3_uncles: H256, + pub miner: H160, + pub state_root: H256, + pub transactions_root: H256, + pub receipts_root: H256, + // Box since 256 bytes is quite large + pub logs_bloom: Box, + pub difficulty: U256, + pub number: U256, + pub gas_limit: u64, + pub gas_used: u64, + pub timestamp: u64, + pub extra_data: H256, + pub mix_hash: H256, + pub nonce: H64, + pub base_fee_per_gas: U256, +} + +impl L2Header { + #[must_use] + pub fn hash(&self) -> H256 { + H256::from(Keccak256::new().chain_update(self.rlp_bytes()).finalize()) + } +} + +impl TryFrom for L2Header { + type Error = TryFromL2HeaderError; + + fn try_from( + value: protos::union::ibc::lightclients::arbitrum::v1::L2Header, + ) -> Result { + Ok(Self { + parent_hash: value + .parent_hash + .try_into() + .map_err(TryFromL2HeaderError::ParentHash)?, + sha3_uncles: value + .sha3_uncles + .try_into() + .map_err(TryFromL2HeaderError::Sha3Uncles)?, + miner: value + .miner + .try_into() + .map_err(TryFromL2HeaderError::Miner)?, + state_root: value + .state_root + .try_into() + .map_err(TryFromL2HeaderError::StateRoot)?, + transactions_root: value + .transactions_root + .try_into() + .map_err(TryFromL2HeaderError::TransactionsRoot)?, + receipts_root: value + .receipts_root + .try_into() + .map_err(TryFromL2HeaderError::ReceiptRoot)?, + logs_bloom: value + .logs_bloom + .try_into() + .map(Box::new) + .map_err(TryFromL2HeaderError::LogsBloom)?, + difficulty: U256::try_from_be_bytes(&value.difficulty) + .map_err(TryFromL2HeaderError::Difficulty)?, + number: U256::try_from_be_bytes(&value.number).map_err(TryFromL2HeaderError::Number)?, + gas_limit: value.gas_limit, + gas_used: value.gas_used, + timestamp: value.timestamp, + extra_data: value + .extra_data + .try_into() + .map_err(TryFromL2HeaderError::ExtraData)?, + mix_hash: value + .mix_hash + .try_into() + .map_err(TryFromL2HeaderError::MixHash)?, + nonce: value + .nonce + .try_into() + .map_err(TryFromL2HeaderError::Nonce)?, + base_fee_per_gas: U256::try_from_be_bytes(&value.base_fee_per_gas) + .map_err(TryFromL2HeaderError::BaseFeePerGas)?, + }) + } +} + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum TryFromL2HeaderError { + #[error("invalid parent hash")] + ParentHash(#[source] InvalidLength), + #[error("invalid sha3 uncles")] + Sha3Uncles(#[source] InvalidLength), + #[error("invalid miner")] + Miner(#[source] InvalidLength), + #[error("invalid state root")] + StateRoot(#[source] InvalidLength), + #[error("invalid transactions root")] + TransactionsRoot(#[source] InvalidLength), + #[error("invalid receipt root")] + ReceiptRoot(#[source] InvalidLength), + #[error("invalid logs bloom")] + LogsBloom(#[source] InvalidLength), + #[error("invalid difficulty")] + Difficulty(#[source] InvalidLength), + #[error("invalid number")] + Number(#[source] InvalidLength), + #[error("invalid extra data")] + ExtraData(#[source] InvalidLength), + #[error("invalid mix hash")] + MixHash(#[source] InvalidLength), + #[error("invalid nonce")] + Nonce(#[source] InvalidLength), + #[error("invalid base fee per gas")] + BaseFeePerGas(#[source] InvalidLength), +} + +impl From for protos::union::ibc::lightclients::arbitrum::v1::L2Header { + fn from(value: L2Header) -> Self { + Self { + parent_hash: value.parent_hash.into(), + sha3_uncles: value.sha3_uncles.into(), + miner: value.miner.into(), + state_root: value.state_root.into(), + transactions_root: value.transactions_root.into(), + receipts_root: value.receipts_root.into(), + logs_bloom: (*value.logs_bloom).into(), + difficulty: value.difficulty.to_be_bytes().to_vec(), + number: value.number.to_be_bytes().to_vec(), + gas_limit: value.gas_limit, + gas_used: value.gas_used, + timestamp: value.timestamp, + extra_data: value.extra_data.into(), + mix_hash: value.mix_hash.into(), + nonce: value.nonce.into(), + base_fee_per_gas: value.base_fee_per_gas.to_be_bytes().to_vec(), + } + } +} + +#[cfg(test)] +mod tests { + use ethers::utils::keccak256; + use hex_literal::hex; + use rlp::Encodable; + + use super::*; + + #[test] + fn rlp() { + // "hash": "0xa548151261174cf854534934ca88e68220e328be563c01915fc11c740a543489", + + let header = L2Header { + difficulty: U256::try_from_be_bytes(&hex!("01")).unwrap(), + extra_data: H256(hex!( + "327fc6b6bcdc7febddc41453d9f5c3703942ec221da53078a91e0b2dbfc02756" + )), + gas_limit: 0x0004_0000_0000_0000, + gas_used: 0x703bc, + logs_bloom: Box::new( + hex!( + "0400000080000002000002000020000000000000002001000400001000000000 \ + 0000002002000000000000001000000000080001000000080000000000100000 \ + 0000000000000000000000080000000000000000000000000000000001000040 \ + 0000000000000040000000000000000000000000800100040000085000004000 \ + 0800000000000000000000000000000000014000000000000000000000000000 \ + 0000010000001000100002000000000000000020000000000000000004000000 \ + 0002200200002000000000800000000000000000000000000000000000000008 \ + 0000000000000010000000000800200000000001000000000000000010010000" + ) + .into(), + ), + miner: hex!("a4b000000000000000000073657175656e636572").into(), + mix_hash: H256(hex!( + "000000000001cbb600000000012de36600000000000000140000000000000000" + )), + nonce: hex!("000000000016eb6d").into(), + number: U256::try_from_be_bytes(&hex!("0c590339")).unwrap(), + parent_hash: H256(hex!( + "9ef9a044f15f12bcefd25572fd7600ae4dcc9a90fab9ad98f78abfb221d5731b" + )), + receipts_root: H256(hex!( + "e3fcff2e9ddc6b6a38889ad0997b566a6ba2574ae85aebba4205da14659c175d" + )), + sha3_uncles: H256(hex!( + "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + )), + state_root: H256(hex!( + "82467a71088bdab7e89d8fe077710172df602d417a77fc813235bb0ca2d3a6c5" + )), + timestamp: 0x6633_eab2, + transactions_root: H256(hex!( + "9361c0130edfe07e3943d06310c69d5d680d77d571724cd1de0d52f399966107" + )), + base_fee_per_gas: U256::try_from_be_bytes(&hex!("989680")).unwrap(), + }; + + assert_eq!( + H256(keccak256(&header.rlp_bytes())), + H256(hex!( + "a548151261174cf854534934ca88e68220e328be563c01915fc11c740a543489" + )) + ); + } +} diff --git a/lib/unionlabs/src/lib.rs b/lib/unionlabs/src/lib.rs index a1eeb45b79..1cb4239551 100644 --- a/lib/unionlabs/src/lib.rs +++ b/lib/unionlabs/src/lib.rs @@ -141,6 +141,7 @@ pub enum WasmClientType { Cometbls, Tendermint, Scroll, + Arbitrum, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -172,6 +173,7 @@ impl FromStr for WasmClientType { "Cometbls" => Ok(WasmClientType::Cometbls), "Tendermint" => Ok(WasmClientType::Tendermint), "Scroll" => Ok(WasmClientType::Scroll), + "Arbitrum" => Ok(WasmClientType::Arbitrum), _ => Err(WasmClientTypeParseError::UnknownType(s.to_string())), } } diff --git a/lib/unionlabs/src/macros.rs b/lib/unionlabs/src/macros.rs index 0eaebf5a74..fcead5b76b 100644 --- a/lib/unionlabs/src/macros.rs +++ b/lib/unionlabs/src/macros.rs @@ -31,6 +31,10 @@ macro_rules! hex_string_array_wrapper { pub fn to_string_unprefixed(&self) -> String { hex::encode(&self) } + + pub fn iter(&self) -> core::slice::Iter { + (&self).into_iter() + } } impl core::str::FromStr for $Struct { @@ -47,6 +51,24 @@ macro_rules! hex_string_array_wrapper { } } + impl<'a> IntoIterator for &'a $Struct { + type Item = &'a u8; + type IntoIter = core::slice::Iter<'a, u8>; + + fn into_iter(self) -> core::slice::Iter<'a, u8> { + self.0.iter() + } + } + + impl IntoIterator for $Struct { + type Item = u8; + type IntoIter = core::array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } + } + impl TryFrom> for $Struct { type Error = crate::errors::InvalidLength; diff --git a/lib/unionlabs/src/traits.rs b/lib/unionlabs/src/traits.rs index 975a6f3b14..b249c94ff3 100644 --- a/lib/unionlabs/src/traits.rs +++ b/lib/unionlabs/src/traits.rs @@ -16,7 +16,7 @@ use crate::{ hash::H256, ibc::{ core::client::height::{Height, IsHeight}, - lightclients::{cometbls, ethereum, scroll, tendermint, wasm}, + lightclients::{arbitrum, cometbls, ethereum, scroll, tendermint, wasm}, }, id::{ChannelId, ClientId, PortId}, uint::U256, @@ -200,6 +200,23 @@ impl ClientState for scroll::client_state::ClientState { } } +impl ClientState for arbitrum::client_state::ClientState { + type ChainId = U256; + type Height = Height; + + fn height(&self) -> Self::Height { + Height { + // TODO: Make ETHEREUM_REVISION_NUMBER a constant in this crate + revision_number: 0, + revision_height: self.l1_latest_slot, + } + } + + fn chain_id(&self) -> Self::ChainId { + self.chain_id + } +} + impl ClientState for wasm::client_state::ClientState { type ChainId = Data::ChainId; type Height = Data::Height; @@ -271,6 +288,12 @@ impl Header for scroll::header::Header { } } +impl Header for arbitrum::header::Header { + fn trusted_height(&self) -> Height { + self.l1_height + } +} + impl Header for wasm::client_message::ClientMessage { fn trusted_height(&self) -> Height { self.data.trusted_height() @@ -305,6 +328,12 @@ impl ConsensusState for scroll::consensus_state::ConsensusState { } } +impl ConsensusState for arbitrum::consensus_state::ConsensusState { + fn timestamp(&self) -> u64 { + self.timestamp + } +} + impl ConsensusState for wasm::consensus_state::ConsensusState { fn timestamp(&self) -> u64 { self.data.timestamp() diff --git a/lib/voyager-message/src/lib.rs b/lib/voyager-message/src/lib.rs index 91db315707..fd17089199 100644 --- a/lib/voyager-message/src/lib.rs +++ b/lib/voyager-message/src/lib.rs @@ -5,7 +5,8 @@ use std::{collections::VecDeque, fmt::Debug, str::FromStr}; use block_message::BlockMessageTypes; use chain_utils::{ - cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, wasm::Wasm, Chains, + arbitrum::Arbitrum, cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, + wasm::Wasm, Chains, }; use queue_msg::{ event, queue_msg, HandleAggregate, HandleData, HandleEffect, HandleEvent, HandleFetch, @@ -322,6 +323,18 @@ impl HandleData for VoyagerData { }, )) } + unionlabs::ClientType::Wasm(unionlabs::WasmClientType::Arbitrum) => { + event(relay_message::id::, Arbitrum, _>( + chain_id, + relay_message::event::IbcEvent { + tx_hash: ibc_event.tx_hash, + height: ibc_event.height, + event: chain_event_to_lc_event::( + ibc_event.event, + ), + }, + )) + } unionlabs::ClientType::Tendermint => { event(relay_message::id::, _>( chain_id, @@ -403,6 +416,28 @@ impl HandleData for VoyagerData { _ => unimplemented!(), }, ), + QueueMsg::Data(block_message::AnyChainIdentified::Arbitrum( + block_message::Identified { + chain_id, + t: block_message::data::Data::IbcEvent(ibc_event), + }, + )) => >::from_queue_msg( + match ibc_event.client_type { + unionlabs::ClientType::Cometbls => { + event(relay_message::id::, _>( + chain_id, + relay_message::event::IbcEvent { + tx_hash: ibc_event.tx_hash, + height: ibc_event.height, + event: chain_event_to_lc_event::>( + ibc_event.event, + ), + }, + )) + } + _ => unimplemented!(), + }, + ), msg => { >::from_queue_msg(msg) } diff --git a/light-clients/arbitrum-light-client/Cargo.toml b/light-clients/arbitrum-light-client/Cargo.toml new file mode 100644 index 0000000000..ed26ec43c8 --- /dev/null +++ b/light-clients/arbitrum-light-client/Cargo.toml @@ -0,0 +1,35 @@ +[package] +authors = ["Union Labs"] +edition = "2021" +license-file = { workspace = true } +name = "arbitrum-light-client" +publish = false +version = "0.1.0" + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +arbitrum-verifier = { workspace = true } +cosmwasm-std = { workspace = true, features = ["abort"] } +ethereum-verifier = { workspace = true } +ethers-core.workspace = true +hex = { workspace = true } +ics008-wasm-client = { workspace = true } +protos = { workspace = true } +rlp = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde-json-wasm = { workspace = true } +sha3 = { workspace = true } +thiserror = { workspace = true } +tiny-keccak = { workspace = true, features = ["keccak"] } +unionlabs = { workspace = true, features = ["ethabi", "stargate"] } + +[dev-dependencies] +base64 = { workspace = true } +hex = { workspace = true } +serde_json = { workspace = true } diff --git a/light-clients/arbitrum-light-client/arbitrum-light-client.nix b/light-clients/arbitrum-light-client/arbitrum-light-client.nix new file mode 100644 index 0000000000..1559728cb4 --- /dev/null +++ b/light-clients/arbitrum-light-client/arbitrum-light-client.nix @@ -0,0 +1,19 @@ +{ ... }: { + perSystem = { crane, lib, ensure-wasm-client-type, ... }: + let + workspace = (crane.buildWasmContract { + crateDirFromRoot = "light-clients/arbitrum-light-client"; + checks = [ + (file_path: '' + ${ensure-wasm-client-type { + inherit file_path; + type = "Arbitrum"; + }} + '') + ]; + }); + in + { + inherit (workspace) packages checks; + }; +} diff --git a/light-clients/arbitrum-light-client/src/client.rs b/light-clients/arbitrum-light-client/src/client.rs new file mode 100644 index 0000000000..fdf3335c99 --- /dev/null +++ b/light-clients/arbitrum-light-client/src/client.rs @@ -0,0 +1,334 @@ +use cosmwasm_std::{Deps, DepsMut, Env}; +use ics008_wasm_client::{ + storage_utils::{ + read_client_state, read_consensus_state, save_client_state, save_consensus_state, + update_client_state, + }, + IbcClient, IbcClientError, Status, StorageState, +}; +use sha3::Digest; +use unionlabs::{ + cosmwasm::wasm::union::custom_query::{query_consensus_state, UnionCustomQuery}, + encoding::{Decode, EncodeAs, EthAbi, Proto}, + google::protobuf::any::Any, + hash::H256, + ibc::{ + core::{ + client::{genesis_metadata::GenesisMetadata, height::Height}, + commitment::merkle_path::MerklePath, + }, + lightclients::{ + arbitrum::{ + client_state::ClientState, consensus_state::ConsensusState, header::Header, + }, + cometbls, + ethereum::{proof::Proof, storage_proof::StorageProof}, + wasm, + }, + }, + ics24::Path, + uint::U256, +}; + +use crate::{errors::Error, eth_encoding::generate_commitment_key}; + +type WasmClientState = unionlabs::ibc::lightclients::wasm::client_state::ClientState; +type WasmConsensusState = + unionlabs::ibc::lightclients::wasm::consensus_state::ConsensusState; +type WasmL1ConsensusState = unionlabs::ibc::lightclients::wasm::consensus_state::ConsensusState< + unionlabs::ibc::lightclients::ethereum::consensus_state::ConsensusState, +>; + +pub struct ArbitrumLightClient; + +impl IbcClient for ArbitrumLightClient { + type Error = Error; + + type CustomQuery = UnionCustomQuery; + + type Header = Header; + + type Misbehaviour = Header; + + type ClientState = ClientState; + + type ConsensusState = ConsensusState; + + type Encoding = Proto; + + fn verify_membership( + deps: Deps, + height: Height, + _delay_time_period: u64, + _delay_block_period: u64, + proof: Vec, + mut path: MerklePath, + value: ics008_wasm_client::StorageState, + ) -> Result<(), IbcClientError> { + let consensus_state: WasmConsensusState = + read_consensus_state(deps, &height)?.ok_or(Error::ConsensusStateNotFound(height))?; + let client_state: WasmClientState = read_client_state(deps)?; + + let path = path.key_path.pop().ok_or(Error::EmptyIbcPath)?; + + // This storage root is verified during the header update, so we don't need to verify it again. + let storage_root = consensus_state.data.ibc_storage_root; + + let storage_proof = { + let mut proofs = StorageProof::decode(&proof) + .map_err(Error::StorageProofDecode)? + .proofs; + if proofs.len() > 1 { + return Err(Error::BatchingProofsNotSupported.into()); + } + proofs.pop().ok_or(Error::EmptyProof)? + }; + + match value { + StorageState::Occupied(value) => do_verify_membership( + path, + storage_root, + client_state.data.l2_ibc_commitment_slot, + storage_proof, + value, + )?, + StorageState::Empty => do_verify_non_membership( + path, + storage_root, + client_state.data.l2_ibc_commitment_slot, + storage_proof, + )?, + } + + Ok(()) + } + + fn verify_header( + deps: Deps, + env: Env, + header: Self::Header, + ) -> Result<(), IbcClientError> { + let client_state: WasmClientState = read_client_state(deps)?; + let l1_consensus_state = query_consensus_state::( + deps, + &env, + client_state.data.l1_client_id.clone().to_string(), + header.l1_height, + ) + .map_err(Error::CustomQuery)?; + arbitrum_verifier::verify_header( + client_state.data, + header, + l1_consensus_state.data.state_root, + ) + .map_err(Error::HeaderVerify)?; + Ok(()) + } + + fn verify_misbehaviour( + _deps: Deps, + _env: Env, + _misbehaviour: Self::Misbehaviour, + ) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn update_state( + mut deps: DepsMut, + _env: Env, + header: Self::Header, + ) -> Result, IbcClientError> { + let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + + let updated_height = Height { + revision_number: client_state.latest_height.revision_number, + revision_height: header.l1_height.revision_height, + }; + + if client_state.latest_height < header.l1_height { + client_state.data.l1_latest_slot = updated_height.revision_height; + update_client_state::( + deps.branch(), + client_state, + updated_height.revision_height, + ); + } + + let consensus_state = WasmConsensusState { + data: ConsensusState { + ibc_storage_root: header.l2_ibc_account_proof.storage_root, + // must be nanos + timestamp: 1_000_000_000 * header.l2_header.timestamp, + }, + }; + save_consensus_state::(deps, consensus_state, &updated_height); + Ok(vec![updated_height]) + } + + fn update_state_on_misbehaviour( + deps: DepsMut, + env: Env, + _client_message: Vec, + ) -> Result<(), IbcClientError> { + let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + client_state.data.frozen_height = Height { + revision_number: client_state.latest_height.revision_number, + revision_height: env.block.height, + }; + save_client_state::(deps, client_state); + Ok(()) + } + + fn check_for_misbehaviour_on_header( + _deps: Deps, + _header: Self::Header, + ) -> Result> { + Ok(false) + } + + fn check_for_misbehaviour_on_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result> { + Err(Error::Unimplemented.into()) + } + + fn verify_upgrade_and_update_state( + _deps: DepsMut, + _upgrade_client_state: Self::ClientState, + _upgrade_consensus_state: Self::ConsensusState, + _proof_upgrade_client: Vec, + _proof_upgrade_consensus_state: Vec, + ) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn migrate_client_store(_deps: DepsMut) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn status(deps: Deps, _env: &Env) -> Result> { + let client_state: WasmClientState = read_client_state(deps)?; + + if client_state.data.frozen_height != Height::default() { + return Ok(Status::Frozen); + } + + let Some(_) = read_consensus_state::(deps, &client_state.latest_height)? else { + return Ok(Status::Expired); + }; + + Ok(Status::Active) + } + + fn export_metadata( + _deps: Deps, + _env: &Env, + ) -> Result, IbcClientError> { + Ok(Vec::new()) + } + + fn timestamp_at_height( + deps: Deps, + height: Height, + ) -> Result> { + Ok(read_consensus_state::(deps, &height)? + .ok_or(Error::ConsensusStateNotFound(height))? + .data + .timestamp) + } +} + +fn do_verify_membership( + path: String, + storage_root: H256, + ibc_commitment_slot: U256, + storage_proof: Proof, + raw_value: Vec, +) -> Result<(), Error> { + check_commitment_key( + &path, + ibc_commitment_slot, + H256(storage_proof.key.to_be_bytes()), + )?; + + let path = path + .parse::>() + .map_err(Error::PathParse)?; + + let canonical_value = match path { + Path::ClientState(_) => { + Any::::decode(raw_value.as_ref()) + .map_err(Error::CometblsClientStateDecode)? + .0 + .encode_as::() + } + Path::ClientConsensusState(_) => Any::< + wasm::consensus_state::ConsensusState, + >::decode(raw_value.as_ref()) + .map_err(Error::CometblsConsensusStateDecode)? + .0 + .data + .encode_as::(), + _ => raw_value, + }; + + // We store the hash of the data, not the data itself to the commitments map. + let expected_value_hash = H256::from( + sha3::Keccak256::new() + .chain_update(canonical_value) + .finalize(), + ); + + let proof_value = H256::from(storage_proof.value.to_be_bytes()); + + if expected_value_hash != proof_value { + return Err(Error::StoredValueMismatch { + expected: expected_value_hash, + stored: proof_value, + }); + } + + ethereum_verifier::verify_storage_proof( + storage_root, + storage_proof.key, + storage_proof.value.to_be_bytes().as_ref(), + &storage_proof.proof, + )?; + + Ok(()) +} + +/// Verifies that no value is committed at `path` in the counterparty light client's storage. +fn do_verify_non_membership( + path: String, + storage_root: H256, + ibc_commitment_slot: U256, + storage_proof: Proof, +) -> Result<(), Error> { + check_commitment_key( + &path, + ibc_commitment_slot, + H256(storage_proof.key.to_be_bytes()), + )?; + ethereum_verifier::verify_storage_absence( + storage_root, + storage_proof.key, + &storage_proof.proof, + )?; + Ok(()) +} + +fn check_commitment_key(path: &str, ibc_commitment_slot: U256, key: H256) -> Result<(), Error> { + let expected_commitment_key = generate_commitment_key(path, ibc_commitment_slot); + + // Data MUST be stored to the commitment path that is defined in ICS23. + if expected_commitment_key != key { + Err(Error::InvalidCommitmentKey { + expected: expected_commitment_key, + found: key, + }) + } else { + Ok(()) + } +} diff --git a/light-clients/arbitrum-light-client/src/contract.rs b/light-clients/arbitrum-light-client/src/contract.rs new file mode 100644 index 0000000000..9b11bda85e --- /dev/null +++ b/light-clients/arbitrum-light-client/src/contract.rs @@ -0,0 +1,54 @@ +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; +use ics008_wasm_client::{ + define_cosmwasm_light_client_contract, + storage_utils::{save_proto_client_state, save_proto_consensus_state}, + CustomQueryOf, InstantiateMsg, +}; +use protos::ibc::lightclients::wasm::v1::{ + ClientState as ProtoClientState, ConsensusState as ProtoConsensusState, +}; +use unionlabs::{ + encoding::{DecodeAs, Proto}, + ibc::{core::client::height::Height, lightclients::arbitrum::client_state::ClientState}, +}; + +use crate::{client::ArbitrumLightClient, errors::Error}; + +#[entry_point] +pub fn instantiate( + mut deps: DepsMut>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let client_state = + ClientState::decode_as::(&msg.client_state).map_err(Error::ClientStateDecode)?; + + save_proto_consensus_state::( + deps.branch(), + ProtoConsensusState { + data: msg.consensus_state.into(), + }, + &Height { + revision_number: 0, + revision_height: client_state.l1_latest_slot, + }, + ); + save_proto_client_state::( + deps, + ProtoClientState { + data: msg.client_state.into(), + checksum: msg.checksum.into(), + latest_height: Some( + Height { + revision_number: 0, + revision_height: client_state.l1_latest_slot, + } + .into(), + ), + }, + ); + Ok(Response::default()) +} + +define_cosmwasm_light_client_contract!(ArbitrumLightClient, Arbitrum); diff --git a/light-clients/arbitrum-light-client/src/errors.rs b/light-clients/arbitrum-light-client/src/errors.rs new file mode 100644 index 0000000000..4282fad73c --- /dev/null +++ b/light-clients/arbitrum-light-client/src/errors.rs @@ -0,0 +1,92 @@ +use ics008_wasm_client::IbcClientError; +use unionlabs::{ + encoding::{DecodeErrorOf, Proto}, + google::protobuf::any::Any, + hash::H256, + ibc::{core::client::height::Height, lightclients::wasm}, + ics24::PathParseError, +}; + +use crate::client::ArbitrumLightClient; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum Error { + #[error("unable to decode storage proof")] + StorageProofDecode( + #[source] + DecodeErrorOf, + ), + #[error("unable to decode counterparty's stored cometbls client state")] + CometblsClientStateDecode( + #[source] + DecodeErrorOf< + Proto, + Any, + >, + ), + #[error("unable to decode counterparty's stored cometbls consensus state")] + CometblsConsensusStateDecode( + #[source] + DecodeErrorOf< + Proto, + Any< + wasm::consensus_state::ConsensusState< + unionlabs::ibc::lightclients::cometbls::consensus_state::ConsensusState, + >, + >, + >, + ), + #[error("unable to decode client state")] + ClientStateDecode( + #[source] + DecodeErrorOf, + ), + #[error("unable to decode consensus state")] + ConsensusStateDecode( + #[source] + DecodeErrorOf< + Proto, + unionlabs::ibc::lightclients::arbitrum::consensus_state::ConsensusState, + >, + ), + + // REVIEW: Move this variant to IbcClientError? + #[error("consensus state not found at height {0}")] + ConsensusStateNotFound(Height), + + #[error("IBC path is empty")] + EmptyIbcPath, + + #[error("invalid commitment key, expected ({expected}) but found ({found})")] + InvalidCommitmentKey { expected: H256, found: H256 }, + + #[error("proof is empty")] + EmptyProof, + + #[error("batching proofs are not supported")] + BatchingProofsNotSupported, + + #[error("expected value ({expected}) and stored value ({stored}) don't match")] + StoredValueMismatch { expected: H256, stored: H256 }, + + #[error("unable to parse ics24 path")] + PathParse(#[from] PathParseError), + + #[error("failed to verify arbitrum header: {0}")] + HeaderVerify(#[from] arbitrum_verifier::Error), + + #[error("failed to verify storage: {0}")] + StorageVerify(#[from] ethereum_verifier::Error), + + #[error("the operation has not been implemented yet")] + Unimplemented, + + #[error("error while calling custom query: {0}")] + CustomQuery(#[from] unionlabs::cosmwasm::wasm::union::custom_query::Error), +} + +impl From for IbcClientError { + fn from(value: Error) -> Self { + IbcClientError::ClientSpecific(value) + } +} diff --git a/light-clients/arbitrum-light-client/src/eth_encoding.rs b/light-clients/arbitrum-light-client/src/eth_encoding.rs new file mode 100644 index 0000000000..d6197099d3 --- /dev/null +++ b/light-clients/arbitrum-light-client/src/eth_encoding.rs @@ -0,0 +1,15 @@ +use sha3::Digest; +use unionlabs::{hash::H256, uint::U256}; + +// TODO: move to unionlabs as it can be reused by any chain hosting our EVM IBC + +/// Calculates the slot for a `path` at saved in the commitment map in `slot` +/// +/// key: keccak256(keccak256(abi.encode_packed(path)) || slot) +pub fn generate_commitment_key(path: &str, slot: U256) -> H256 { + sha3::Keccak256::new() + .chain_update(sha3::Keccak256::new().chain_update(path).finalize()) + .chain_update(slot.to_be_bytes()) + .finalize() + .into() +} diff --git a/light-clients/arbitrum-light-client/src/lib.rs b/light-clients/arbitrum-light-client/src/lib.rs new file mode 100644 index 0000000000..b26d0adbfe --- /dev/null +++ b/light-clients/arbitrum-light-client/src/lib.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod contract; +pub mod errors; +pub mod eth_encoding; diff --git a/networks/devnet.nix b/networks/devnet.nix index f9c3485afc..23feb8c515 100644 --- a/networks/devnet.nix +++ b/networks/devnet.nix @@ -37,6 +37,11 @@ validatorCount = 4; genesisOverwrites = { app_state = { + gov.params = { + max_deposit_period = "12s"; + voting_period = "30s"; + expedited_voting_period = "6s"; + }; staking.params = { epoch_length = "8"; jailed_validator_threshold = "10"; diff --git a/tools/wasm-light-client.nix b/tools/wasm-light-client.nix index 02097f599f..2d099ffeb2 100644 --- a/tools/wasm-light-client.nix +++ b/tools/wasm-light-client.nix @@ -1,16 +1,18 @@ { ... }: { perSystem = { dbg, pkgs, crane, ... }: let - parse-wasm-client-type = pkgs.lib.getExe ( - (crane.buildWorkspaceMember { - crateDirFromRoot = "tools/parse-wasm-client-type"; - }).packages.parse-wasm-client-type - ); + parse-wasm-client-type = (crane.buildWorkspaceMember { + crateDirFromRoot = "tools/parse-wasm-client-type"; + }).packages.parse-wasm-client-type; in { + packages = { + parse-wasm-client-type = parse-wasm-client-type; + }; + _module.args.ensure-wasm-client-type = { type, file_path }: '' - ${parse-wasm-client-type} ${file_path} ${type} + ${pkgs.lib.getExe parse-wasm-client-type} ${file_path} ${type} ''; }; } diff --git a/uniond/proto/union/ibc/lightclients/arbitrum/v1/arbitrum.proto b/uniond/proto/union/ibc/lightclients/arbitrum/v1/arbitrum.proto new file mode 100644 index 0000000000..85b7a6cf23 --- /dev/null +++ b/uniond/proto/union/ibc/lightclients/arbitrum/v1/arbitrum.proto @@ -0,0 +1,97 @@ +syntax = "proto3"; +package union.ibc.lightclients.arbitrum.v1; + +option go_package = "union/ibc/lightclients/arbitrum"; +import "ibc/core/client/v1/client.proto"; +import "union/ibc/lightclients/ethereum/v1/ethereum.proto"; + +// TODO: l2_ instead of rollup_ +message ClientState { + string l1_client_id = 1; + string chain_id = 2; + uint64 l1_latest_slot = 3; + + bytes l1_contract_address = 4; + + // _latestConfirmed + bytes l1_latest_confirmed_slot = 5; + // _nodes + bytes l1_nodes_slot = 6; + // _nodes[_latestConfirmed].confirmData + bytes confirm_data_offset = 7; + + .ibc.core.client.v1.Height frozen_height = 8; + + bytes l2_ibc_contract_address = 9; + bytes l2_ibc_commitment_slot = 10; +} + +message ConsensusState { + bytes ibc_storage_root = 1; + uint64 timestamp = 2; +} + +message Header { + .ibc.core.client.v1.Height l1_height = 1; + + // Proof of the L1 rollup account in the L1 state root. + .union.ibc.lightclients.ethereum.v1.AccountProof l1_account_proof = 2; + + // Proof of the l2 ibc contract address in the l2 state root. + .union.ibc.lightclients.ethereum.v1.AccountProof l2_ibc_account_proof = 3; + + // The latest confirmed node number, as stored in `_latestConfirmed`. + // + // https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/rollup/RollupCore.sol#L60 + uint64 latest_confirmed = 6; + + // Proof of `latest_confirmed`. + .union.ibc.lightclients.ethereum.v1.StorageProof l1_latest_confirmed_slot_proof = 7; + // The proof of the [`_nodes`] mapping at `latest_confirmed`, offset to [`Node.confirmData`]. + // + // [`_nodes`]: https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/rollup/RollupCore.sol#L64 + // [`Node.confirmData`]: https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/rollup/Node.sol#L27 + .union.ibc.lightclients.ethereum.v1.StorageProof l1_nodes_slot_proof = 8; + + // Arbitrum block header, used to recompute the block hash and verify the timestamp. + L2Header l2_header = 9; +} + +// The Arbitrum header as returned from `eth_getBlockByNumber`, with all non-standard fields removed. +// +// Note that certain fields are different than a typical eth_getBlockByNumber response; see [here](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/rpc-methods#existing-fields-with-different-behavior-1) for more information. +// +// https://github.com/OffchainLabs/go-ethereum/blob/f94174378de6ea7cf02963d99489e69b6671d1aa/core/types/block.go#L66-L80 +message L2Header { + // H256 + bytes parent_hash = 1; + // H256 + bytes sha3_uncles = 2; + // H160 + bytes miner = 3; + // H256 + bytes state_root = 4; + // H256 + bytes transactions_root = 5; + // H256 + bytes receipts_root = 6; + // H2048 + bytes logs_bloom = 7; + // U256 + bytes difficulty = 8; + // U256 + bytes number = 9; + uint64 gas_limit = 10; + uint64 gas_used = 11; + uint64 timestamp = 12; + // This field is equivalent to sendRoot. + // + // H256 + bytes extra_data = 13; + // H256 + bytes mix_hash = 14; + // H64 + bytes nonce = 15; + // U256 + bytes base_fee_per_gas = 16; +} diff --git a/uniond/proto/union/ibc/lightclients/scroll/v1/scroll.proto b/uniond/proto/union/ibc/lightclients/scroll/v1/scroll.proto index 500c22de31..433183f1d8 100644 --- a/uniond/proto/union/ibc/lightclients/scroll/v1/scroll.proto +++ b/uniond/proto/union/ibc/lightclients/scroll/v1/scroll.proto @@ -43,4 +43,3 @@ message IdentifiedL1MessageHash { uint64 queue_index = 1; bytes message_hash = 2; } - diff --git a/voyager-config-testnet.json b/voyager-config-testnet.json deleted file mode 100644 index 0a43d0e6f0..0000000000 --- a/voyager-config-testnet.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "chain": { - "scroll-testnet": { - "enabled": true, - "chain_type": "scroll", - "ibc_handler_address": "0x6a2c5e2b519b07e6939363f44d9df4e23af73b86", - "signers": [ - { - "raw": "0x227bfab7b601429981d3fbffb1b7625fb13464965cd4368ae48090c8d44b417e" - } - ], - "scroll_eth_rpc_api": "wss://sepolia-rpc.scroll.io", - "rollup_contract_address": "0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0", - "rollup_finalized_state_roots_slot": "158", - "rollup_last_finalized_batch_index_slot": "156", - "l1_client_id": "08-wasm-0", - "l1": { - "chain_type": "ethereum", - "preset_base": "mainnet", - "ibc_handler_address": "0x4466196F00F10E633789ac7f054a54a82e4b78C7", - "eth_rpc_api": "wss://eth-sepolia.g.alchemy.com/v2/Xn_VBUDyUtXUYb9O6b5ZmuBNDaSlH-BB", - "eth_beacon_rpc_api": "https://lodestar-sepolia.chainsafe.io" - }, - "scroll_api": "https://sepolia-api-re.scroll.io/", - "union_grpc_url": "http://localhost:9090" - }, - "sepolia": { - "enabled": true, - "chain_type": "ethereum", - "preset_base": "mainnet", - "ibc_handler_address": "0x4466196F00F10E633789ac7f054a54a82e4b78C7", - "signers": [ - { - "raw": "0x227bfab7b601429981d3fbffb1b7625fb13464965cd4368ae48090c8d44b417e" - } - ], - "eth_rpc_api": "wss://eth-sepolia.g.alchemy.com/v2/Xn_VBUDyUtXUYb9O6b5ZmuBNDaSlH-BB", - "eth_beacon_rpc_api": "https://lodestar-sepolia.chainsafe.io" - }, - "union-devnet": { - "chain_type": "union", - "enabled": true, - "signers": [ - { - "raw": "0xaa820fa947beb242032a41b6dc9a8b9c37d8f5fbcda0966b1ec80335b10a7d6f" - }, - { - "raw": "0xf562d20f0a4ffd8814d262f7023f33971cbcd14a96d60027585777f174b9cdeb" - }, - { - "raw": "0xa1f713e0f36404586085a599a45ca8233e23709e23cd54bc8d5452ef8f7bc1e6" - }, - { - "raw": "0xedc165ff1ebc27044ddc284c9cf5da656dcbff324f6ecbb9d3203cf5f4738d6d" - }, - { - "raw": "0x40c30853b7f3e6d7ec997fc72c78aef65fce2e82d5b71032a98cb8efaa4710ca" - }, - { - "raw": "0xaeff1a3cf6e96d1551c95677fff8399b1ee0c3ed2f610928520897202e5ae690" - } - ], - "fee_denom": "muno", - "ws_url": "ws://localhost:26657/websocket", - "prover_endpoint": "http://localhost:9999", - "grpc_url": "http://localhost:9090" - } - }, - "voyager": { - "num_workers": 20, - "queue": { - "type": "pg-queue", - "database_url": "postgres://postgres:postgrespassword@127.0.0.1:5432/default", - "max_connections": 20, - "min_connections": 20, - "idle_timeout": null, - "max_lifetime": null - } - } -} diff --git a/voyager-config.json b/voyager-config.json index cea0c2d638..1080bf35f0 100644 --- a/voyager-config.json +++ b/voyager-config.json @@ -1,5 +1,27 @@ { "chain": { + "arbitrum-testnet": { + "enabled": true, + "chain_type": "arbitrum", + "ibc_handler_address": "0x61ba1780ecce6513872beb7ce698b49168010416", + "signers": [ + { + "raw": "0x227bfab7b601429981d3fbffb1b7625fb13464965cd4368ae48090c8d44b417e" + } + ], + "l1_client_id": "08-wasm-0", + "l2_eth_rpc_api": "wss://arb-sepolia.g.alchemy.com/v2/6PCr1n8dJeYbE2Z9LrXScs05hLTYiVFl", + "l1_contract_address": "0xd80810638dbDF9081b72C1B33c65375e807281C8", + "l1_latest_confirmed_slot": "117", + "l1": { + "chain_type": "ethereum", + "preset_base": "mainnet", + "ibc_handler_address": "0xbd3f2BCD8f7FbB11B3Ae4fe83451A9A1bF3B1Dc0", + "eth_rpc_api": "wss://eth-sepolia.g.alchemy.com/v2/6PCr1n8dJeYbE2Z9LrXScs05hLTYiVFl", + "eth_beacon_rpc_api": "https://lodestar-sepolia.chainsafe.io" + }, + "union_grpc_url": "https://grpc.testnet.bonlulu.uno:443" + }, "scroll-testnet": { "enabled": false, "chain_type": "scroll", @@ -27,7 +49,7 @@ "union_grpc_url": "https://grpc.testnet.bonlulu.uno:443" }, "ethereum-devnet": { - "enabled": true, + "enabled": false, "chain_type": "ethereum", "preset_base": "minimal", "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", @@ -53,7 +75,7 @@ }, "union-devnet": { "chain_type": "union", - "enabled": true, + "enabled": false, "signers": [ { "raw": "0xaa820fa947beb242032a41b6dc9a8b9c37d8f5fbcda0966b1ec80335b10a7d6f" @@ -162,7 +184,7 @@ } }, "voyager": { - "num_workers": 20, + "num_workers": 1, "queue": { "type": "pg-queue", "database_url": "postgres://postgres:postgrespassword@127.0.0.1:5432/default", diff --git a/voyager/src/cli.rs b/voyager/src/cli.rs index 8e19d23278..c53095c23f 100644 --- a/voyager/src/cli.rs +++ b/voyager/src/cli.rs @@ -1,6 +1,6 @@ use std::{ffi::OsString, marker::PhantomData, str::FromStr, sync::Arc}; -use chain_utils::Chains; +use chain_utils::{arbitrum::Arbitrum, Chains}; use clap::{ error::{ContextKind, ContextValue, ErrorKind}, Args, FromArgMatches, Parser, Subcommand, @@ -23,7 +23,7 @@ use unionlabs::{ }, id::{ClientId, ConnectionId, PortId}, result_unwrap, - traits::HeightOf, + traits::{ChainIdOf, HeightOf}, QueryHeight, }; @@ -326,6 +326,8 @@ pub enum Command { Relay, #[command(subcommand)] Queue(QueueCmd), + #[command(subcommand)] + Util(UtilCmd), Query { #[arg(long)] on: String, @@ -349,7 +351,7 @@ pub async fn any_state_proof_to_json( path: ics24::Path, c: Hc, height: QueryHeight>, -) -> String +) -> serde_json::Value where Hc: ChainExt + DoFetchState + DoFetchProof, Tr: ChainExt, @@ -392,7 +394,7 @@ where Identified>: IsAggregateData, Identified>: IsAggregateData, { - use serde_json::to_string_pretty as json; + use serde_json::to_value as json; let height = match height { QueryHeight::Latest => c.query_latest_height().await.unwrap(), @@ -654,6 +656,21 @@ pub enum QueueCmd { }, } +#[derive(Debug, Subcommand)] +pub enum UtilCmd { + QueryLatestHeight { + on: String, + }, + #[command(subcommand)] + Arbitrum(ArbitrumCmd), +} + +#[derive(Debug, Subcommand)] +pub enum ArbitrumCmd { + LatestConfirmedAtBeaconSlot { on: String, slot: u64 }, + ExecutionHeightOfBeaconSlot { on: String, slot: u64 }, +} + #[derive(Debug, Subcommand)] pub enum SubmitPacketCmd { Transfer { diff --git a/voyager/src/main.rs b/voyager/src/main.rs index b1f77553f7..397f9e14e1 100644 --- a/voyager/src/main.rs +++ b/voyager/src/main.rs @@ -13,8 +13,13 @@ use std::{ }; use chain_utils::{ - cosmos::Cosmos, ethereum::Ethereum, scroll::Scroll, union::Union, wasm::Wasm, AnyChain, - ChainConfigType, Chains, EthereumChainConfig, LightClientType, + arbitrum::Arbitrum, + cosmos::Cosmos, + ethereum::{Ethereum, EthereumChain}, + scroll::Scroll, + union::Union, + wasm::Wasm, + AnyChain, ChainConfigType, Chains, EthereumChainConfig, LightClientType, }; use clap::Parser; use queue_msg::{ @@ -29,6 +34,7 @@ use relay_message::{ data::IbcState, RelayMessageTypes, }; +use serde::Serialize; use sqlx::{query_as, PgPool}; use tikv_jemallocator::Jemalloc; use tracing_subscriber::EnvFilter; @@ -50,7 +56,10 @@ use voyager_message::{FromQueueMsg, VoyagerFetch, VoyagerMessageTypes}; static GLOBAL: Jemalloc = Jemalloc; use crate::{ - cli::{any_state_proof_to_json, AppArgs, Command, Handshake, HandshakeType, QueryCmd}, + cli::{ + any_state_proof_to_json, AppArgs, ArbitrumCmd, Command, Handshake, HandshakeType, QueryCmd, + UtilCmd, + }, config::{Config, GetChainError}, queue::{ chains_from_config, AnyQueueConfig, PgQueueConfig, RunError, Voyager, VoyagerInitError, @@ -121,6 +130,8 @@ pub enum VoyagerError { Migrations(#[from] MigrationsError), #[error("fatal error encountered")] Run(#[from] RunError), + #[error("unable to run command")] + Command(#[source] Box), } #[derive(Debug, thiserror::Error)] @@ -168,11 +179,7 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { .map_err(MigrationsError::Migrate)?; } Command::PrintConfig => { - println!( - "{}", - serde_json::to_string_pretty(&voyager_config) - .expect("config serialization is infallible; qed;") - ); + print_json(&voyager_config); } Command::Relay => { let queue = Voyager::new(voyager_config.clone()).await?; @@ -226,6 +233,15 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { ) .await } + (AnyChain::Union(union), ChainConfigType::Arbitrum(_)) => { + any_state_proof_to_json::, Arbitrum>( + chains, + path, + Wasm(union), + at, + ) + .await + } ( AnyChain::Union(union), ChainConfigType::Ethereum(EthereumChainConfig { @@ -265,6 +281,18 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { .await } + (AnyChain::Scroll(scroll), ChainConfigType::Union(_)) => { + any_state_proof_to_json::>(chains, path, scroll, at) + .await + } + + (AnyChain::Arbitrum(arbitrum), ChainConfigType::Union(_)) => { + any_state_proof_to_json::>( + chains, path, arbitrum, at, + ) + .await + } + (AnyChain::Cosmos(cosmos), ChainConfigType::Cosmos(_)) => { any_state_proof_to_json::(chains, path, cosmos, at) .await @@ -273,7 +301,7 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { _ => panic!("unsupported"), }; - println!("{json}"); + print_json(&json); } } } @@ -324,7 +352,7 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { .await .unwrap(); - println!("{}", serde_json::to_string_pretty(&results).unwrap()); + print_json(&results); } } } @@ -363,6 +391,9 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { (AnyChain::Union(union), AnyChain::Scroll(scroll)) => { mk_handshake::, Scroll>(&Wasm(union), &scroll, ty, chains).await } + (AnyChain::Union(union), AnyChain::Arbitrum(scroll)) => { + mk_handshake::, Arbitrum>(&Wasm(union), &scroll, ty, chains).await + } (AnyChain::Cosmos(cosmos), AnyChain::Union(union)) => { mk_handshake::, Union>(&Wasm(cosmos), &union, ty, chains).await } @@ -390,10 +421,13 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { (AnyChain::Scroll(scroll), AnyChain::Union(union)) => { mk_handshake::>(&scroll, &Wasm(union), ty, chains).await } + (AnyChain::Arbitrum(scroll), AnyChain::Union(union)) => { + mk_handshake::>(&scroll, &Wasm(union), ty, chains).await + } _ => panic!("invalid"), }; - println!("{}", serde_json::to_string(&all_msgs).unwrap()); + print_json(&all_msgs); } Command::InitFetch { on } => { let on = voyager_config.get_chain(&on).await?; @@ -404,15 +438,68 @@ async fn do_main(args: cli::AppArgs) -> Result<(), VoyagerError> { AnyChain::EthereumMainnet(on) => mk_init_fetch::>(&on).await, AnyChain::EthereumMinimal(on) => mk_init_fetch::>(&on).await, AnyChain::Scroll(on) => mk_init_fetch::(&on).await, + AnyChain::Arbitrum(on) => mk_init_fetch::(&on).await, }; - println!("{}", serde_json::to_string(&msg).unwrap()); + print_json(&msg); } + Command::Util(util) => match util { + UtilCmd::QueryLatestHeight { on } => { + let on = voyager_config.get_chain(&on).await?; + + let height = match on { + AnyChain::Union(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(Box::new(e)))?, + AnyChain::Cosmos(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(Box::new(e)))?, + AnyChain::EthereumMainnet(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(Box::new(e)))?, + AnyChain::EthereumMinimal(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(Box::new(e)))?, + AnyChain::Scroll(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(e))?, + AnyChain::Arbitrum(on) => on + .query_latest_height() + .await + .map_err(|e| VoyagerError::Command(e))?, + }; + + print_json(&height); + } + UtilCmd::Arbitrum(arb_cmd) => match arb_cmd { + ArbitrumCmd::LatestConfirmedAtBeaconSlot { on, slot } => print_json( + &Arbitrum::try_from(voyager_config.get_chain(&on.to_string()).await.unwrap()) + .expect("chain not found in config") + .latest_confirmed_at_beacon_slot(slot) + .await, + ), + ArbitrumCmd::ExecutionHeightOfBeaconSlot { on, slot } => print_json( + &Arbitrum::try_from(voyager_config.get_chain(&on.to_string()).await.unwrap()) + .expect("chain not found in config") + .execution_height_of_beacon_slot(slot) + .await, + ), + }, + }, } Ok(()) } +fn print_json(t: &T) { + println!("{}", serde_json::to_string(&t).unwrap()) +} + async fn mk_handshake( a: &A, b: &B, diff --git a/voyager/src/queue.rs b/voyager/src/queue.rs index 929d9b5494..eb0fb610cc 100644 --- a/voyager/src/queue.rs +++ b/voyager/src/queue.rs @@ -15,7 +15,7 @@ use axum::{ }; use chain_utils::{AnyChain, AnyChainTryFromConfigError, Chains}; use frame_support_procedural::{CloneNoBound, DebugNoBound}; -use futures::{channel::mpsc::UnboundedSender, Future, SinkExt, StreamExt}; +use futures::{channel::mpsc::UnboundedSender, Future, SinkExt, StreamExt, TryStreamExt}; use queue_msg::{Engine, InMemoryQueue, Queue, QueueMessageTypes, QueueMsg}; use relay_message::RelayMessageTypes; use reqwest::StatusCode; @@ -274,13 +274,12 @@ impl Voyager { join_set.spawn(async move { reactor .run(&mut q) - .for_each(|x| async { - let msg = x.unwrap(); + .try_for_each(|data| async move { + tracing::info!(data = %serde_json::to_string(&data).unwrap(), "received data outside of an aggregation"); - tracing::info!(data = %serde_json::to_string(&msg).unwrap(), "received data outside of an aggregation"); + Ok(()) }) - .await; - Ok(()) + .await }); } @@ -288,7 +287,17 @@ impl Voyager { // TODO: figure out while let Some(res) = join_set.join_next().await { - res.unwrap().unwrap(); + match res { + Ok(Ok(())) => {} + Ok(Err(err)) => { + tracing::error!(%err, "error processing message"); + panic!(); + } + Err(err) => { + tracing::error!(%err, "error processing message"); + panic!(); + } + } } // while let Some(res) = join_set.join_next().await { @@ -317,6 +326,7 @@ pub async fn chains_from_config( let mut ethereum_minimal = HashMap::new(); let mut ethereum_mainnet = HashMap::new(); let mut scroll = HashMap::new(); + let mut arbitrum = HashMap::new(); fn insert_into_chain_map( map: &mut HashMap<<::SelfClientState as ClientState>::ChainId, C>, @@ -356,6 +366,9 @@ pub async fn chains_from_config( AnyChain::Scroll(c) => { insert_into_chain_map(&mut scroll, c); } + AnyChain::Arbitrum(c) => { + insert_into_chain_map(&mut arbitrum, c); + } } } @@ -365,6 +378,7 @@ pub async fn chains_from_config( ethereum_mainnet, union, cosmos, + arbitrum, }) }