From 8878d9b4fa736a132ab938fafcd5c64bb5186f58 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Tue, 17 Dec 2024 19:13:02 +0100 Subject: [PATCH] feat(voyager): introduce berachain consensus plugin --- Cargo.lock | 59 +++- Cargo.toml | 4 + lib/berachain-light-client-types/Cargo.toml | 15 +- lib/chain-utils/src/cosmos_sdk.rs | 2 +- .../modules/consensus/berachain/Cargo.toml | 29 ++ .../modules/consensus/berachain/src/main.rs | 186 ++++++++++++ .../modules/consensus/ethereum/src/main.rs | 16 +- .../modules/consensus/tendermint/src/main.rs | 275 +----------------- .../plugins/client-update/ethereum/Cargo.toml | 2 +- .../client-update/tendermint/Cargo.toml | 2 +- 10 files changed, 297 insertions(+), 293 deletions(-) create mode 100644 voyager/modules/consensus/berachain/Cargo.toml create mode 100644 voyager/modules/consensus/berachain/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 34281ea5a7..ed33f7c729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2280,7 +2280,6 @@ dependencies = [ "ethereum-light-client-types", "protos", "serde", - "tendermint-light-client-types", "thiserror", "unionlabs", ] @@ -13440,6 +13439,35 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-update-plugin-berachain" +version = "0.1.0" +dependencies = [ + "alloy", + "berachain-light-client-types", + "cometbft-rpc", + "cometbft-types", + "dashmap 5.5.3", + "enumorph", + "ethereum-light-client-types", + "futures", + "ics23", + "jsonrpsee", + "macros", + "num-bigint 0.4.6", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-update-plugin-cometbls" version = "0.1.0" @@ -13555,6 +13583,35 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-consensus-module-berachain" +version = "0.1.0" +dependencies = [ + "alloy", + "beacon-api-types", + "berachain-light-client-types", + "cometbft-rpc", + "dashmap 5.5.3", + "enumorph", + "futures", + "ics23", + "jsonrpsee", + "macros", + "num-bigint 0.4.6", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "tendermint-light-client-types", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-consensus-module-cometbls" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 23132af280..7bac7f79a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,11 +114,13 @@ members = [ "voyager/modules/client/movement", "voyager/modules/client/tendermint", + "voyager/modules/consensus/berachain", "voyager/modules/consensus/cometbls", "voyager/modules/consensus/ethereum", "voyager/modules/consensus/movement", "voyager/modules/consensus/tendermint", + "voyager/plugins/client-update/berachain", "voyager/plugins/client-update/cometbls", "voyager/plugins/client-update/ethereum", "voyager/plugins/client-update/movement", @@ -252,6 +254,8 @@ voyager-core = { path = "lib/voyager-core", default-features = false } voyager-message = { path = "lib/voyager-message", default-features = false } voyager-vm = { path = "lib/voyager-vm", default-features = false } +voyager-consensus-module-tendermint = { path = "voyager/modules/consensus/tendermint", default-features = false } + # external dependencies milagro_bls = { git = "https://github.com/Snowfork/milagro_bls", rev = "bc2b5b5e8d48b7e2e1bfaa56dc2d93e13cb32095", default-features = false } diff --git a/lib/berachain-light-client-types/Cargo.toml b/lib/berachain-light-client-types/Cargo.toml index a56f8400d5..a4c1bd7f92 100644 --- a/lib/berachain-light-client-types/Cargo.toml +++ b/lib/berachain-light-client-types/Cargo.toml @@ -4,14 +4,13 @@ name = "berachain-light-client-types" version = "0.1.0" [dependencies] -alloy = { workspace = true, features = ["sol-types"], optional = true } -beacon-api-types = { workspace = true } -ethereum-light-client-types = { workspace = true } -protos = { workspace = true, optional = true, features = ["union+ibc+lightclients+berachain+v1"] } -serde = { workspace = true, optional = true, features = ["derive"] } -tendermint-light-client-types = { workspace = true } -thiserror = { workspace = true } -unionlabs = { workspace = true } +alloy = { workspace = true, features = ["sol-types"], optional = true } +beacon-api-types = { workspace = true } +ethereum-light-client-types = { workspace = true } +protos = { workspace = true, optional = true, features = ["union+ibc+lightclients+berachain+v1"] } +serde = { workspace = true, optional = true, features = ["derive"] } +thiserror = { workspace = true } +unionlabs = { workspace = true } [features] default = [] diff --git a/lib/chain-utils/src/cosmos_sdk.rs b/lib/chain-utils/src/cosmos_sdk.rs index 8810516ed2..e4508436c1 100644 --- a/lib/chain-utils/src/cosmos_sdk.rs +++ b/lib/chain-utils/src/cosmos_sdk.rs @@ -26,7 +26,7 @@ use unionlabs::{ }; use crate::{ - cosmos_sdk::cosmos_sdk_error::{CosmosSdkError, SdkError}, + cosmos_sdk::cosmos_sdk_error::CosmosSdkError, keyring::{ConcurrentKeyring, SignerBalance}, }; diff --git a/voyager/modules/consensus/berachain/Cargo.toml b/voyager/modules/consensus/berachain/Cargo.toml new file mode 100644 index 0000000000..a22fc8f6b1 --- /dev/null +++ b/voyager/modules/consensus/berachain/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition = "2021" +name = "voyager-consensus-module-berachain" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +beacon-api-types = { workspace = true, features = ["serde", "ssz"] } +cometbft-rpc = { workspace = true } +dashmap = { workspace = true } +enumorph = { workspace = true } +futures = { workspace = true } +ics23 = { workspace = true } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +num-bigint = { workspace = true } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tendermint-light-client-types = { workspace = true, features = ["proto", "serde"] } +berachain-light-client-types = { workspace = true, features = ["proto", "serde"] } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +unionlabs = { workspace = true } +voyager-message = { workspace = true } +voyager-vm = { workspace = true } diff --git a/voyager/modules/consensus/berachain/src/main.rs b/voyager/modules/consensus/berachain/src/main.rs new file mode 100644 index 0000000000..fcc9197aae --- /dev/null +++ b/voyager/modules/consensus/berachain/src/main.rs @@ -0,0 +1,186 @@ +use std::fmt::Debug; + +use alloy::{ + providers::{Provider, ProviderBuilder, RootProvider}, + transports::BoxTransport, +}; +use beacon_api_types::{ExecutionPayloadHeaderSsz, Mainnet}; +use berachain_light_client_types::{ClientState, ConsensusState}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::instrument; +use unionlabs::{ + berachain::LATEST_EXECUTION_PAYLOAD_HEADER_PREFIX, + encoding::{DecodeAs, Ssz}, + hash::H160, + ibc::core::client::height::Height, +}; +use voyager_message::{ + core::{ChainId, ConsensusType}, + into_value, + module::{ConsensusModuleInfo, ConsensusModuleServer}, + ConsensusModule, ExtensionsExt, VoyagerClient, +}; +use voyager_vm::BoxDynError; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module { + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub l2_chain_id: ChainId, + pub ibc_handler_address: H160, + pub eth_provider: RootProvider, + pub tm_client: cometbft_rpc::Client, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub ibc_handler_address: H160, + pub eth_rpc_api: String, + pub comet_ws_url: String, +} + +impl ConsensusModule for Module { + type Config = Config; + + async fn new(config: Self::Config, info: ConsensusModuleInfo) -> Result { + let tm_client = cometbft_rpc::Client::new(config.comet_ws_url).await?; + + let eth_provider = ProviderBuilder::new() + .on_builtin(&config.eth_rpc_api) + .await?; + + let l2_chain_id = ChainId::new(eth_provider.get_chain_id().await?.to_string()); + + info.ensure_chain_id(l2_chain_id.as_str())?; + info.ensure_consensus_type(ConsensusType::BEACON_KIT)?; + + Ok(Self { + l1_client_id: config.l1_client_id, + l1_chain_id: config.l1_chain_id, + l2_chain_id, + ibc_handler_address: config.ibc_handler_address, + eth_provider, + tm_client, + }) + } +} + +#[async_trait] +impl ConsensusModuleServer for Module { + /// Query the latest finalized height of this chain. + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn query_latest_height(&self, ext: &Extensions, finalized: bool) -> RpcResult { + let voy_client = ext.try_get::()?; + if finalized { + let l1_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), finalized) + .await?; + + let raw_execution_header = self + .tm_client + .abci_query( + "store/beacon/key", + [LATEST_EXECUTION_PAYLOAD_HEADER_PREFIX], + // proof for height H must be queried at H-1 + Some((l1_height.height() as i64 - 1).try_into().unwrap()), + false, + ) + .await + .unwrap(); + + let execution_header = ExecutionPayloadHeaderSsz::::decode_as::( + raw_execution_header + .response + .value + .expect("big trouble") + .as_ref(), + ) + .unwrap(); + + Ok(Height::new(execution_header.block_number)) + } else { + Ok(Height::new( + self.eth_provider + .get_block_number() + .await + .expect("big trouble"), + )) + } + } + + /// Query the latest finalized timestamp of this chain. + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn query_latest_timestamp(&self, ext: &Extensions, finalized: bool) -> RpcResult { + let latest_height = self.query_latest_height(ext, finalized).await?; + let latest_block = self + .eth_provider + .get_block_by_number( + latest_height.height().into(), + alloy::rpc::types::BlockTransactionsKind::Hashes, + ) + .await + .expect("big trouble") + .expect("big trouble"); + let latest_timestamp: i64 = latest_block + .header + .timestamp + .try_into() + .expect("big trouble"); + // Normalize to nanos in order to be compliant with cosmos + Ok(latest_timestamp * 1_000_000_000) + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn self_client_state(&self, _: &Extensions, height: Height) -> RpcResult { + Ok(into_value(ClientState { + l1_client_id: self.l1_client_id, + chain_id: self + .l2_chain_id + .as_str() + .parse() + .expect("self.chain_id is a valid u256"), + latest_height: height.height(), + ibc_contract_address: self.ibc_handler_address, + })) + } + + /// The consensus state on this chain at the specified `Height`. + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn self_consensus_state(&self, _: &Extensions, height: Height) -> RpcResult { + let block = self + .eth_provider + .get_block_by_number( + height.height().into(), + alloy::rpc::types::BlockTransactionsKind::Hashes, + ) + .await + .expect("big trouble") + .expect("big trouble"); + Ok(into_value(&ConsensusState { + // Normalize to nanos in order to be compliant with cosmos + timestamp: block.header.timestamp * 1_000_000_000, + state_root: block.header.state_root.into(), + storage_root: self + .eth_provider + .get_proof(self.ibc_handler_address.into(), vec![]) + .block_id(height.height().into()) + .await + .unwrap() + .storage_hash + .0 + .into(), + })) + } +} diff --git a/voyager/modules/consensus/ethereum/src/main.rs b/voyager/modules/consensus/ethereum/src/main.rs index b0a4bbe155..d236bcb988 100644 --- a/voyager/modules/consensus/ethereum/src/main.rs +++ b/voyager/modules/consensus/ethereum/src/main.rs @@ -164,9 +164,8 @@ impl ConsensusModuleServer for Module { // TODO: Use a better timestamp type here #[instrument(skip_all, fields(chain_id = %self.chain_id, finalized))] async fn query_latest_timestamp(&self, _: &Extensions, finalized: bool) -> RpcResult { - if finalized { - Ok(self - .beacon_api_client + let latest_timestamp: i64 = if finalized { + self.beacon_api_client .finality_update() .await .map_err(|err| ErrorObject::owned(-1, ErrorReporter(err).to_string(), None::<()>))? @@ -175,10 +174,9 @@ impl ConsensusModuleServer for Module { .execution .timestamp .try_into() - .unwrap()) + .unwrap() } else { - Ok(self - .provider + self.provider .get_block( BlockNumberOrTag::Latest.into(), BlockTransactionsKind::Hashes, @@ -189,8 +187,10 @@ impl ConsensusModuleServer for Module { .header .timestamp .try_into() - .unwrap()) - } + .unwrap() + }; + // Normalize to nanos in order to be compliant with cosmos + Ok(latest_timestamp * 1_000_000_000) } #[instrument(skip_all, fields(chain_id = %self.chain_id, %height))] diff --git a/voyager/modules/consensus/tendermint/src/main.rs b/voyager/modules/consensus/tendermint/src/main.rs index f5e21aac1a..a779cfbc0c 100644 --- a/voyager/modules/consensus/tendermint/src/main.rs +++ b/voyager/modules/consensus/tendermint/src/main.rs @@ -1,277 +1,6 @@ -use std::{ - fmt::Debug, - num::{NonZeroU64, ParseIntError}, -}; - -use ics23::ibc_api::SDK_SPECS; -use jsonrpsee::{ - core::{async_trait, RpcResult}, - types::ErrorObject, - Extensions, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tendermint_light_client_types::{ClientState, ConsensusState, Fraction}; -use tracing::{debug, error, instrument}; -use unionlabs::{ - ibc::core::{client::height::Height, commitment::merkle_root::MerkleRoot}, - option_unwrap, result_unwrap, ErrorReporter, -}; -use voyager_message::{ - core::{ChainId, ConsensusType}, - module::{ConsensusModuleInfo, ConsensusModuleServer}, - rpc::json_rpc_error_to_error_object, - ConsensusModule, -}; -use voyager_vm::BoxDynError; +use voyager_message::ConsensusModule; #[tokio::main(flavor = "multi_thread")] async fn main() { - Module::run().await -} - -#[derive(Debug, Clone)] -pub struct Module { - pub chain_id: ChainId, - - pub tm_client: cometbft_rpc::Client, - pub chain_revision: u64, - pub grpc_url: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub ws_url: String, - pub grpc_url: String, -} - -impl ConsensusModule for Module { - type Config = Config; - - async fn new(config: Self::Config, info: ConsensusModuleInfo) -> Result { - let tm_client = cometbft_rpc::Client::new(config.ws_url).await?; - - let chain_id = tm_client.status().await?.node_info.network.to_string(); - - info.ensure_chain_id(&chain_id)?; - info.ensure_consensus_type(ConsensusType::TENDERMINT)?; - - let chain_revision = chain_id - .split('-') - .last() - .ok_or_else(|| ChainIdParseError { - found: chain_id.clone(), - source: None, - })? - .parse() - .map_err(|err| ChainIdParseError { - found: chain_id.clone(), - source: Some(err), - })?; - - Ok(Self { - tm_client, - chain_id: ChainId::new(chain_id), - chain_revision, - grpc_url: config.grpc_url, - }) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("unable to parse chain id: expected format `-`, found `{found}`")] -pub struct ChainIdParseError { - found: String, - #[source] - source: Option, -} - -impl Module { - #[must_use] - pub fn make_height(&self, height: u64) -> Height { - Height::new_with_revision(self.chain_revision, height) - } - - async fn latest_height(&self, finalized: bool) -> Result { - let commit_response = self.tm_client.commit(None).await?; - - let mut height = commit_response - .signed_header - .header - .height - .inner() - .try_into() - .expect("value is >= 0; qed;"); - - if finalized && !commit_response.canonical { - debug!( - "commit is not canonical and finalized height was requested, \ - latest finalized height is the previous block" - ); - height -= 1; - } - - debug!(height, "latest height"); - - Ok(self.make_height(height)) - } -} - -#[async_trait] -impl ConsensusModuleServer for Module { - /// Query the latest finalized height of this chain. - #[instrument(skip_all, fields(chain_id = %self.chain_id))] - async fn query_latest_height(&self, _: &Extensions, finalized: bool) -> RpcResult { - self.latest_height(finalized) - .await - // TODO: Add more context here - .map_err(|err| ErrorObject::owned(-1, ErrorReporter(err).to_string(), None::<()>)) - } - - /// Query the latest finalized timestamp of this chain. - // TODO: Use a better timestamp type here - #[instrument(skip_all, fields(chain_id = %self.chain_id))] - async fn query_latest_timestamp(&self, _: &Extensions, finalized: bool) -> RpcResult { - let mut commit_response = self - .tm_client - .commit(None) - .await - .map_err(json_rpc_error_to_error_object)?; - - if finalized && commit_response.canonical { - debug!( - "commit is not canonical and finalized timestamp was \ - requested, fetching commit at previous block" - ); - commit_response = self - .tm_client - .commit(Some( - (u64::try_from(commit_response.signed_header.header.height.inner() - 1) - .expect("should be fine")) - .try_into() - .expect("should be fine"), - )) - .await - .map_err(json_rpc_error_to_error_object)?; - - if !commit_response.canonical { - error!( - ?commit_response, - "commit for previous height is not canonical? continuing \ - anyways, but this may cause issues downstream" - ); - } - } - - Ok(commit_response - .signed_header - .header - .time - .as_unix_nanos() - .try_into() - .expect("should be fine")) - } - - #[instrument(skip_all, fields(chain_id = %self.chain_id))] - async fn self_client_state(&self, _: &Extensions, height: Height) -> RpcResult { - let params = protos::cosmos::staking::v1beta1::query_client::QueryClient::connect( - self.grpc_url.clone(), - ) - .await - .unwrap() - .params(protos::cosmos::staking::v1beta1::QueryParamsRequest {}) - .await - .unwrap() - .into_inner() - .params - .unwrap(); - - let commit = self - .tm_client - .commit(Some(height.height().try_into().unwrap())) - .await - .unwrap(); - - let height = commit.signed_header.header.height; - - let unbonding_period = std::time::Duration::new( - params - .unbonding_time - .clone() - .unwrap() - .seconds - .try_into() - .unwrap(), - params - .unbonding_time - .clone() - .unwrap() - .nanos - .try_into() - .unwrap(), - ); - - Ok(serde_json::to_value(ClientState { - chain_id: self.chain_id.to_string(), - // https://github.com/cometbft/cometbft/blob/da0e55604b075bac9e1d5866cb2e62eaae386dd9/light/verifier.go#L16 - trust_level: Fraction { - numerator: 1, - denominator: const { option_unwrap!(NonZeroU64::new(3)) }, - }, - // https://github.com/cosmos/relayer/blob/23d1e5c864b35d133cad6a0ef06970a2b1e1b03f/relayer/chains/cosmos/provider.go#L177 - trusting_period: unionlabs::google::protobuf::duration::Duration::new( - (unbonding_period * 85 / 100).as_secs().try_into().unwrap(), - (unbonding_period * 85 / 100) - .subsec_nanos() - .try_into() - .unwrap(), - ) - .unwrap(), - unbonding_period: unionlabs::google::protobuf::duration::Duration::new( - unbonding_period.as_secs().try_into().unwrap(), - unbonding_period.subsec_nanos().try_into().unwrap(), - ) - .unwrap(), - // https://github.com/cosmos/relayer/blob/23d1e5c864b35d133cad6a0ef06970a2b1e1b03f/relayer/chains/cosmos/provider.go#L177 - max_clock_drift: const { - result_unwrap!(unionlabs::google::protobuf::duration::Duration::new( - 60 * 10, - 0 - )) - }, - frozen_height: None, - latest_height: Height::new_with_revision( - self.chain_revision, - height.inner().try_into().expect("is within bounds; qed;"), - ), - proof_specs: SDK_SPECS.into(), - upgrade_path: vec!["upgrade".into(), "upgradedIBCState".into()], - }) - .unwrap()) - } - - /// The consensus state on this chain at the specified `Height`. - #[instrument(skip_all, fields(chain_id = %self.chain_id))] - async fn self_consensus_state(&self, _: &Extensions, height: Height) -> RpcResult { - let commit = self - .tm_client - .commit(Some(height.height().try_into().unwrap())) - .await - .map_err(|e| { - ErrorObject::owned( - -1, - format!("error fetching commit: {}", ErrorReporter(e)), - None::<()>, - ) - })?; - - Ok(serde_json::to_value(&ConsensusState { - root: MerkleRoot { - hash: commit.signed_header.header.app_hash.into_encoding(), - }, - next_validators_hash: commit.signed_header.header.next_validators_hash, - timestamp: commit.signed_header.header.time, - }) - .unwrap()) - } + voyager_consensus_module_tendermint::Module::run().await } diff --git a/voyager/plugins/client-update/ethereum/Cargo.toml b/voyager/plugins/client-update/ethereum/Cargo.toml index 072f82f412..809546db48 100644 --- a/voyager/plugins/client-update/ethereum/Cargo.toml +++ b/voyager/plugins/client-update/ethereum/Cargo.toml @@ -22,6 +22,6 @@ thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -unionlabs = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi"] } voyager-message = { workspace = true } voyager-vm = { workspace = true } diff --git a/voyager/plugins/client-update/tendermint/Cargo.toml b/voyager/plugins/client-update/tendermint/Cargo.toml index d620f7c35f..e63e69c8de 100644 --- a/voyager/plugins/client-update/tendermint/Cargo.toml +++ b/voyager/plugins/client-update/tendermint/Cargo.toml @@ -17,7 +17,7 @@ prost = { workspace = true } protos = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tendermint-light-client-types = { workspace = true, features = ["proto"] } +tendermint-light-client-types = { workspace = true, features = ["proto", "serde"] } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true }