diff --git a/Cargo.lock b/Cargo.lock index 0cbd30e23c..4a7f121a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5011,6 +5011,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "evm-state-lens-light-client-types" +version = "0.1.0" +dependencies = [ + "alloy", + "protos", + "serde", + "thiserror", + "unionlabs", +] + [[package]] name = "evm-storage-verifier" version = "0.1.0" @@ -13425,6 +13436,31 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-module-state-lens-evm" +version = "0.1.0" +dependencies = [ + "beacon-api-types", + "chain-utils", + "enumorph", + "ethereum-light-client-types", + "evm-state-lens-light-client-types", + "futures", + "jsonrpsee", + "macros", + "prost 0.12.6", + "serde", + "serde-utils", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-module-tendermint" version = "0.1.0" @@ -13537,6 +13573,35 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-update-plugin-state-lens-evm" +version = "0.1.0" +dependencies = [ + "alloy", + "cometbft-rpc", + "cometbft-types", + "dashmap 5.5.3", + "enumorph", + "evm-state-lens-light-client-types", + "futures", + "ibc-union-spec", + "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-tendermint" version = "0.1.0" @@ -13647,6 +13712,34 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-consensus-module-state-lens-evm" +version = "0.1.0" +dependencies = [ + "alloy", + "cometbft-rpc", + "dashmap 5.5.3", + "enumorph", + "ethereum-light-client-types", + "evm-state-lens-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-consensus-module-tendermint" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fdde73e1ae..69427beb1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ members = [ "lib/tendermint-light-client-types", "lib/linea-light-client-types", "lib/movement-light-client-types", + "lib/state-lens/evm-light-client-types", # these will all be re enabled and updated once ethereum-light-client is updated @@ -114,16 +115,19 @@ members = [ "voyager/modules/client/ethereum", "voyager/modules/client/movement", "voyager/modules/client/tendermint", + "voyager/modules/client/state-lens/evm", "voyager/modules/consensus/cometbls", "voyager/modules/consensus/ethereum", "voyager/modules/consensus/movement", "voyager/modules/consensus/tendermint", + "voyager/modules/consensus/state-lens/evm", "voyager/plugins/client-update/cometbls", "voyager/plugins/client-update/ethereum", "voyager/plugins/client-update/movement", "voyager/plugins/client-update/tendermint", + "voyager/plugins/client-update/state-lens/evm", "voyager/plugins/periodic-client-update", @@ -204,6 +208,8 @@ scroll-rpc = { path = "lib/scroll-rpc", default-features = false } berachain-light-client = { path = "cosmwasm/union-ibc/light-clients/light-clients/berachain-light-client", default-features = false } berachain-light-client-types = { path = "lib/berachain-light-client-types", default-features = false } +evm-state-lens-light-client-types = { path = "lib/state-lens/evm-light-client-types", default-features = false } + tendermint-light-client = { path = "cosmwasm/union-ibc/light-clients/tendermint", default-features = false } tendermint-light-client-types = { path = "lib/tendermint-light-client-types", default-features = false } tendermint-verifier = { path = "lib/tendermint-verifier", default-features = false } diff --git a/evm/contracts/clients/EvmInCosmosClient.sol b/evm/contracts/clients/EvmInCosmosClient.sol index 4f69878ad2..7bbdbd8440 100644 --- a/evm/contracts/clients/EvmInCosmosClient.sol +++ b/evm/contracts/clients/EvmInCosmosClient.sol @@ -10,7 +10,6 @@ import "../core/02-client/ILightClient.sol"; import "../core/24-host/IBCStore.sol"; import "../core/24-host/IBCCommitment.sol"; import "../lib/ICS23.sol"; -import "../lib/Common.sol"; import "../lib/MPTVerifier.sol"; struct Header { @@ -22,9 +21,8 @@ struct Header { struct ClientState { uint32 l1ClientId; - uint32 l2ChainId; uint32 l2ClientId; - uint64 latestHeight; + uint64 l2LatestHeight; uint16 timestampOffset; uint16 stateRootOffset; uint16 storageRootOffset; @@ -44,7 +42,7 @@ library EvmInCosmosLib { error ErrClientFrozen(); error ErrInvalidL1Proof(); error ErrInvalidInitialConsensusState(); - error ErrInvalidMisbehaviour(); + error ErrUnsupported(); function encode( ConsensusState memory consensusState @@ -69,6 +67,15 @@ library EvmInCosmosLib { ) internal pure returns (bytes32) { return keccak256(encode(clientState)); } + + function extract( + bytes calldata input, + uint16 offset + ) internal pure returns (bytes32 val) { + assembly { + val := calldataload(add(input.offset, offset)) + } + } } contract EvmInCosmosClient is @@ -84,8 +91,6 @@ contract EvmInCosmosClient is mapping(uint32 => ClientState) private clientStates; mapping(uint32 => mapping(uint64 => ConsensusState)) private consensusStates; - mapping(uint32 => mapping(uint64 => ProcessedMoment)) private - processedMoments; constructor() { _disableInitializers(); @@ -112,20 +117,15 @@ contract EvmInCosmosClient is assembly { consensusState := consensusStateBytes.offset } - if (clientState.latestHeight == 0 || consensusState.timestamp == 0) { + if (clientState.l2LatestHeight == 0 || consensusState.timestamp == 0) { revert EvmInCosmosLib.ErrInvalidInitialConsensusState(); } clientStates[clientId] = clientState; - consensusStates[clientId][clientState.latestHeight] = consensusState; - // Normalize to nanosecond because ibc-go recvPacket expects nanos... - processedMoments[clientId][clientState.latestHeight] = ProcessedMoment({ - timestamp: block.timestamp * 1e9, - height: block.number - }); + consensusStates[clientId][clientState.l2LatestHeight] = consensusState; return ConsensusStateUpdate({ clientStateCommitment: clientState.commit(), consensusStateCommitment: consensusState.commit(), - height: clientState.latestHeight + height: clientState.l2LatestHeight }); } @@ -141,7 +141,7 @@ contract EvmInCosmosClient is assembly { header := clientMessageBytes.offset } - ClientState memory clientState = clientStates[clientId]; + ClientState storage clientState = clientStates[clientId]; ILightClient l1Client = IBCStore(ibcHandler).getClient(clientState.l1ClientId); // L₂[H₂] ∈ L₁[H₁] @@ -162,23 +162,22 @@ contract EvmInCosmosClient is } bytes calldata rawL2ConsensusState = header.l2ConsensusState; - uint64 timestampOffset = clientState.timestampOffset; - uint64 stateRootOffset = clientState.stateRootOffset; - uint64 storageRootOffset = clientState.storageRootOffset; - uint64 l2Timestamp; - bytes32 l2StateRoot; - bytes32 l2StorageRoot; - assembly { - l2Timestamp := - calldataload(add(rawL2ConsensusState.offset, timestampOffset)) - l2StateRoot := - calldataload(add(rawL2ConsensusState.offset, stateRootOffset)) - l2StorageRoot := - calldataload(add(rawL2ConsensusState.offset, storageRootOffset)) - } + uint64 l2Timestamp = uint64( + uint256( + EvmInCosmosLib.extract( + rawL2ConsensusState, clientState.timestampOffset + ) + ) + ); + bytes32 l2StateRoot = EvmInCosmosLib.extract( + rawL2ConsensusState, clientState.stateRootOffset + ); + bytes32 l2StorageRoot = EvmInCosmosLib.extract( + rawL2ConsensusState, clientState.storageRootOffset + ); - if (header.l2Height > clientState.latestHeight) { - clientState.latestHeight = header.l2Height; + if (header.l2Height > clientState.l2LatestHeight) { + clientState.l2LatestHeight = header.l2Height; } // L₂[H₂] = S₂ @@ -189,12 +188,6 @@ contract EvmInCosmosClient is consensusState.stateRoot = l2StateRoot; consensusState.storageRoot = l2StorageRoot; - // P[H₂] = now() - ProcessedMoment storage processed = - processedMoments[clientId][header.l2Height]; - processed.timestamp = block.timestamp * 1e9; - processed.height = block.number; - // commit(S₂) return ConsensusStateUpdate({ clientStateCommitment: clientState.commit(), @@ -207,7 +200,7 @@ contract EvmInCosmosClient is uint32 clientId, bytes calldata clientMessageBytes ) external override onlyIBC { - revert EvmInCosmosLib.ErrInvalidMisbehaviour(); + revert EvmInCosmosLib.ErrUnsupported(); } function verifyMembership( @@ -227,9 +220,8 @@ contract EvmInCosmosClient is EvmInCosmosLib.EVM_IBC_COMMITMENT_SLOT ) ); - (bool exists, bytes calldata provenValue) = MPTVerifier.verifyTrieValue( - proof, keccak256(abi.encodePacked(slot)), storageRoot - ); + (bool exists, bytes calldata provenValue) = + MPTVerifier.verifyTrieValue(proof, slot, storageRoot); return exists && keccak256(value) == keccak256(provenValue); } @@ -249,9 +241,7 @@ contract EvmInCosmosClient is EvmInCosmosLib.EVM_IBC_COMMITMENT_SLOT ) ); - (bool exists, bytes calldata provenValue) = MPTVerifier.verifyTrieValue( - proof, keccak256(abi.encodePacked(slot)), storageRoot - ); + (bool exists,) = MPTVerifier.verifyTrieValue(proof, slot, storageRoot); return !exists; } @@ -278,7 +268,7 @@ contract EvmInCosmosClient is function getLatestHeight( uint32 clientId ) external view override returns (uint64) { - return clientStates[clientId].latestHeight; + return clientStates[clientId].l2LatestHeight; } function isFrozen( diff --git a/evm/tests/src/02-client/CosmosInCosmosClient.t.sol b/evm/tests/src/02-client/CosmosInCosmosClient.t.sol index 767d81b347..275ced1c32 100644 --- a/evm/tests/src/02-client/CosmosInCosmosClient.t.sol +++ b/evm/tests/src/02-client/CosmosInCosmosClient.t.sol @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.27; import "forge-std/Test.sol"; diff --git a/evm/tests/src/02-client/EvmInCosmosClient.t.sol b/evm/tests/src/02-client/EvmInCosmosClient.t.sol new file mode 100644 index 0000000000..7355f98bfc --- /dev/null +++ b/evm/tests/src/02-client/EvmInCosmosClient.t.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "../core/IBCHandler.sol"; +import "../core/Relay.sol"; +import "../../../contracts/clients/EvmInCosmosClient.sol"; +import "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; + +contract EvmInCosmosClientTest is Test { + EvmInCosmosClient client; + address admin = address(0xABCD); + address ibcHandler; + + function setUp() public { + ibcHandler = address(0xC0DE); + EvmInCosmosClient implementation = new EvmInCosmosClient(); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeWithSelector( + EvmInCosmosClient.initialize.selector, ibcHandler, admin + ) + ); + client = EvmInCosmosClient(address(proxy)); + } + + function test_initialize_ok() public { + assertEq(client.owner(), admin); + } +} diff --git a/lib/voyager-core/src/lib.rs b/lib/voyager-core/src/lib.rs index 089bab218b..4585200a4d 100644 --- a/lib/voyager-core/src/lib.rs +++ b/lib/voyager-core/src/lib.rs @@ -113,6 +113,12 @@ impl ClientType { /// [Movement]: https://github.com/movementlabsxyz/movement pub const MOVEMENT: &'static str = "movement"; + /// A client tracking the Ethereum beacon chain consensus verified through the + /// [Ethereum Proof-of-Stake Consensus Specifications](spec). As an L2 extracted from [CometBLS]. + /// + /// [spec]: https://github.com/ethereum/consensus-specs + pub const STATE_LENS_EVM: &'static str = "state-lens/evm"; + // lots more to come - near, linea, polygon - stay tuned } diff --git a/lib/voyager-message/src/lib.rs b/lib/voyager-message/src/lib.rs index 766322002a..b5b9aaa35a 100644 --- a/lib/voyager-message/src/lib.rs +++ b/lib/voyager-message/src/lib.rs @@ -22,6 +22,7 @@ use jsonrpsee::{ }; use macros::model; use reth_ipc::{client::IpcClientBuilder, server::RpcServiceBuilder}; +use rpc::{SelfClientState, SelfConsensusState}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use tracing::{ @@ -524,6 +525,46 @@ impl ClientT for IdThreadClient { } impl VoyagerClient { + pub async fn query_latest_timestamp( + &self, + chain_id: ChainId, + finalized: bool, + ) -> RpcResult { + let latest_timestamp = self + .0 + .query_latest_timestamp(chain_id, finalized) + .await + .map_err(json_rpc_error_to_error_object)?; + + Ok(latest_timestamp) + } + + pub async fn self_client_state( + &self, + chain_id: ChainId, + height: QueryHeight, + ) -> RpcResult { + let client_state = self + .0 + .self_client_state(chain_id, height) + .await + .map_err(json_rpc_error_to_error_object)?; + Ok(client_state) + } + + pub async fn self_consensus_state( + &self, + chain_id: ChainId, + height: QueryHeight, + ) -> RpcResult { + let consensus_state = self + .0 + .self_consensus_state(chain_id, height) + .await + .map_err(json_rpc_error_to_error_object)?; + Ok(consensus_state) + } + pub async fn query_latest_height( &self, chain_id: ChainId, diff --git a/voyager/modules/client/state-lens/evm/Cargo.toml b/voyager/modules/client/state-lens/evm/Cargo.toml new file mode 100644 index 0000000000..50dfaf3b16 --- /dev/null +++ b/voyager/modules/client/state-lens/evm/Cargo.toml @@ -0,0 +1,25 @@ +[package] +edition = "2021" +name = "voyager-client-module-state-lens-evm" +version = "0.1.0" + +[dependencies] +beacon-api-types = { workspace = true, features = ["serde"] } +chain-utils = { workspace = true } +enumorph = { workspace = true } +ethereum-light-client-types = { workspace = true, features = ["serde", "ethabi"] } +evm-state-lens-light-client-types = { workspace = true, features = ["serde", "ethabi"] } +futures = { workspace = true } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +prost = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde-utils = { workspace = true } +serde_json = { workspace = true } +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/client/state-lens/evm/src/main.rs b/voyager/modules/client/state-lens/evm/src/main.rs new file mode 100644 index 0000000000..a8007247e2 --- /dev/null +++ b/voyager/modules/client/state-lens/evm/src/main.rs @@ -0,0 +1,215 @@ +use ethereum_light_client_types::StorageProof; +use evm_state_lens_light_client_types::{ClientState, ConsensusState, Header}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::ErrorObject, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tracing::instrument; +use unionlabs::{ + self, + bytes::Bytes, + encoding::{Bincode, DecodeAs, EncodeAs, EthAbi}, + ibc::core::client::height::Height, + ErrorReporter, +}; +use voyager_message::{ + core::{ChainId, ClientStateMeta, ClientType, ConsensusStateMeta}, + module::{ClientModuleInfo, ClientModuleServer}, + ClientModule, FATAL_JSONRPC_ERROR_CODE, +}; +use voyager_vm::BoxDynError; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config {} + +impl ClientModule for Module { + type Config = Config; + + async fn new(_: Self::Config, info: ClientModuleInfo) -> Result { + info.ensure_client_type(ClientType::STATE_LENS_EVM)?; + Ok(Self {}) + } +} + +type SelfConsensusState = ConsensusState; +type SelfClientState = ClientState; + +impl Module { + pub fn decode_consensus_state(consensus_state: &[u8]) -> RpcResult { + SelfConsensusState::decode_as::(consensus_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode consensus state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + } + + pub fn decode_client_state(client_state: &[u8]) -> RpcResult { + ::decode_as::(client_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode client state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + } + + pub fn make_height(revision_height: u64) -> Height { + Height::new(revision_height) + } +} + +#[async_trait] +impl ClientModuleServer for Module { + #[instrument] + async fn decode_client_state_meta( + &self, + _: &Extensions, + client_state: Bytes, + ) -> RpcResult { + let cs = Module::decode_client_state(&client_state)?; + + Ok(ClientStateMeta { + chain_id: ChainId::new(cs.l2_chain_id.to_string()), + height: Module::make_height(cs.l2_latest_height), + }) + } + + #[instrument] + async fn decode_consensus_state_meta( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + let cs = Module::decode_consensus_state(&consensus_state)?; + + Ok(ConsensusStateMeta { + timestamp_nanos: cs.timestamp, + }) + } + + #[instrument] + async fn decode_client_state(&self, _: &Extensions, client_state: Bytes) -> RpcResult { + Ok(serde_json::to_value(Module::decode_client_state(&client_state)?).unwrap()) + } + + #[instrument] + async fn decode_consensus_state( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + Ok(serde_json::to_value(Module::decode_consensus_state(&consensus_state)?).unwrap()) + } + + #[instrument] + async fn encode_client_state( + &self, + _: &Extensions, + client_state: Value, + metadata: Value, + ) -> RpcResult { + if !metadata.is_null() { + return Err(ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + "metadata was provided, but this client type does not require \ + metadata for client state encoding", + Some(json!({ + "provided_metadata": metadata, + })), + )); + } + + serde_json::from_value::(client_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize client state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_consensus_state( + &self, + _: &Extensions, + consensus_state: Value, + ) -> RpcResult { + serde_json::from_value::(consensus_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!( + "unable to deserialize consensus state: {}", + ErrorReporter(err) + ), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument(skip_all)] + async fn reencode_counterparty_client_state( + &self, + _: &Extensions, + _client_state: Bytes, + _client_type: ClientType, + ) -> RpcResult { + todo!() + } + + #[instrument(skip_all)] + async fn reencode_counterparty_consensus_state( + &self, + _: &Extensions, + _consensus_state: Bytes, + _client_type: ClientType, + ) -> RpcResult { + todo!() + } + + #[instrument] + async fn encode_header(&self, _: &Extensions, header: Value) -> RpcResult { + serde_json::from_value::
(header) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize header: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|header| header.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_proof(&self, _: &Extensions, proof: Value) -> RpcResult { + serde_json::from_value::(proof) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize proof: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|storage_proof| storage_proof.encode_as::()) + .map(Into::into) + } +} diff --git a/voyager/modules/consensus/state-lens/evm/Cargo.toml b/voyager/modules/consensus/state-lens/evm/Cargo.toml new file mode 100644 index 0000000000..c437992977 --- /dev/null +++ b/voyager/modules/consensus/state-lens/evm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition = "2021" +name = "voyager-consensus-module-state-lens-evm" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +cometbft-rpc = { workspace = true } +dashmap = { workspace = true } +enumorph = { workspace = true } +ethereum-light-client-types = { workspace = true, features = ["serde"] } +evm-state-lens-light-client-types = { workspace = true, features = ["proto", "serde"] } +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 } +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/state-lens/evm/src/main.rs b/voyager/modules/consensus/state-lens/evm/src/main.rs new file mode 100644 index 0000000000..2883d39215 --- /dev/null +++ b/voyager/modules/consensus/state-lens/evm/src/main.rs @@ -0,0 +1,132 @@ +use std::fmt::Debug; + +use alloy::providers::{Provider, ProviderBuilder}; +use ethereum_light_client_types::ConsensusState as EthConsensusState; +use evm_state_lens_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::ibc::core::client::height::Height; +use voyager_message::{ + core::{ChainId, ConsensusType, QueryHeight}, + 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_chain_id: ChainId, + pub l2_chain_id: ChainId, + pub l1_client_id: u32, + pub l2_client_id: u32, + pub timestamp_offset: u16, + pub state_root_offset: u16, + pub storage_root_offset: u16, + pub l1_tm_client: cometbft_rpc::Client, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub l1_client_id: u32, + pub l2_client_id: u32, + pub timestamp_offset: u16, + pub state_root_offset: u16, + pub storage_root_offset: u16, + pub l1_comet_ws_url: String, + pub l2_eth_rpc_url: String, +} + +impl ConsensusModule for Module { + type Config = Config; + + async fn new(config: Self::Config, info: ConsensusModuleInfo) -> Result { + let l1_tm_client = cometbft_rpc::Client::new(config.l1_comet_ws_url).await?; + + let l1_chain_id = l1_tm_client.status().await?.node_info.network.to_string(); + + info.ensure_chain_id(&l1_chain_id)?; + info.ensure_consensus_type(ConsensusType::COMETBLS)?; + + let provider = ProviderBuilder::new() + .on_builtin(&config.l2_eth_rpc_url) + .await?; + + let l2_chain_id = ChainId::new(provider.get_chain_id().await?.to_string()); + + info.ensure_chain_id(l2_chain_id.to_string())?; + info.ensure_consensus_type(ConsensusType::ETHEREUM)?; + + Ok(Self { + l1_tm_client, + l1_chain_id: ChainId::new(l1_chain_id), + l2_chain_id: ChainId::new(l2_chain_id.to_string()), + l1_client_id: config.l1_client_id, + l2_client_id: config.l2_client_id, + timestamp_offset: config.timestamp_offset, + state_root_offset: config.state_root_offset, + storage_root_offset: config.storage_root_offset, + }) + } +} + +#[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::()?; + voy_client + .query_latest_height(self.l2_chain_id.clone(), finalized) + .await + } + + /// 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 voy_client = ext.try_get::()?; + voy_client + .query_latest_timestamp(self.l2_chain_id.clone(), finalized) + .await + } + + #[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, + l2_chain_id: self.l2_chain_id.to_string(), + l2_client_id: self.l2_client_id, + l2_latest_height: height.height(), + timestamp_offset: self.timestamp_offset, + state_root_offset: self.state_root_offset, + storage_root_offset: self.storage_root_offset, + })) + } + + /// 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, ext: &Extensions, height: Height) -> RpcResult { + let voy_client = ext.try_get::()?; + let state = voy_client + .self_consensus_state(self.l2_chain_id.clone(), QueryHeight::Specific(height)) + .await? + .state; + let consensus_state = + serde_json::from_value::(state).expect("big trouble"); + Ok(into_value(&ConsensusState { + timestamp: consensus_state.timestamp, + state_root: consensus_state.state_root, + storage_root: consensus_state.storage_root, + })) + } +} diff --git a/voyager/plugins/client-update/state-lens/evm/Cargo.toml b/voyager/plugins/client-update/state-lens/evm/Cargo.toml new file mode 100644 index 0000000000..0231242804 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition = "2021" +name = "voyager-client-update-plugin-state-lens-evm" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +cometbft-rpc = { workspace = true } +cometbft-types.workspace = true +dashmap = { workspace = true } +enumorph = { workspace = true } +evm-state-lens-light-client-types = { workspace = true, features = ["proto", "serde"] } +futures = { workspace = true } +ibc-union-spec.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 } +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/plugins/client-update/state-lens/evm/src/call.rs b/voyager/plugins/client-update/state-lens/evm/src/call.rs new file mode 100644 index 0000000000..3d9897aafd --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/src/call.rs @@ -0,0 +1,25 @@ +use enumorph::Enumorph; +use macros::model; +use unionlabs::ibc::core::client::height::Height; +use voyager_message::core::ChainId; + +#[model] +#[derive(Enumorph)] +pub enum ModuleCall { + FetchUpdate(FetchUpdate), + FetchUpdateAfterL1Update(FetchUpdateAfterL1Update), +} + +#[model] +pub struct FetchUpdate { + pub counterparty_chain_id: ChainId, + pub update_from: Height, + pub update_to: Height, +} + +#[model] +pub struct FetchUpdateAfterL1Update { + pub counterparty_chain_id: ChainId, + pub update_from: Height, + pub update_to: Height, +} diff --git a/voyager/plugins/client-update/state-lens/evm/src/callback.rs b/voyager/plugins/client-update/state-lens/evm/src/callback.rs new file mode 100644 index 0000000000..a332e95f9a --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/src/callback.rs @@ -0,0 +1,6 @@ +use enumorph::Enumorph; +use macros::model; + +#[model] +#[derive(Enumorph)] +pub enum ModuleCallback {} diff --git a/voyager/plugins/client-update/state-lens/evm/src/data.rs b/voyager/plugins/client-update/state-lens/evm/src/data.rs new file mode 100644 index 0000000000..f52f66d6b6 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/src/data.rs @@ -0,0 +1,4 @@ +use macros::model; + +#[model] +pub enum ModuleData {} diff --git a/voyager/plugins/client-update/state-lens/evm/src/main.rs b/voyager/plugins/client-update/state-lens/evm/src/main.rs new file mode 100644 index 0000000000..70245bdf95 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/src/main.rs @@ -0,0 +1,306 @@ +use std::{collections::VecDeque, fmt::Debug}; + +use alloy::{ + providers::{Provider, ProviderBuilder, RootProvider}, + transports::BoxTransport, +}; +use call::FetchUpdateAfterL1Update; +use evm_state_lens_light_client_types::Header; +use ibc_union_spec::{ConsensusStatePath, IbcUnion}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use unionlabs::ibc::core::commitment::merkle_proof::MerkleProof; +use voyager_message::{ + call::{Call, FetchUpdateHeaders, WaitForTrustedHeight}, + callback::AggregateMsgUpdateClientsFromOrderedHeaders, + core::{ChainId, ClientType, IbcSpec, IbcSpecId, QueryHeight}, + data::{Data, DecodedHeaderMeta, OrderedHeaders}, + hook::UpdateHook, + into_value, + module::{PluginInfo, PluginServer}, + DefaultCmd, ExtensionsExt, Plugin, PluginMessage, RawClientId, VoyagerClient, VoyagerMessage, +}; +use voyager_vm::{call, conc, data, pass::PassResult, promise, seq, BoxDynError, Op, Visit}; + +use crate::{ + call::{FetchUpdate, ModuleCall}, + callback::ModuleCallback, +}; + +pub mod call; +pub mod callback; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module { + pub l0_client_id: u32, + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub l2_chain_id: ChainId, + + pub l2_eth_provider: RootProvider, + pub l1_tm_client: cometbft_rpc::Client, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub l0_client_id: u32, + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub l2_chain_id: ChainId, + + pub l1_ws_url: String, + pub l2_rpc_url: String, +} + +impl Plugin for Module { + type Call = ModuleCall; + type Callback = ModuleCallback; + + type Config = Config; + type Cmd = DefaultCmd; + + async fn new(config: Self::Config) -> Result { + let l1_tm_client = cometbft_rpc::Client::new(config.l1_ws_url).await?; + + let l1_chain_id = l1_tm_client.status().await?.node_info.network.to_string(); + + if l1_chain_id != config.l1_chain_id.as_str() { + return Err(format!( + "incorrect chain id: expected `{}`, but found `{}`", + config.l1_chain_id, l1_chain_id + ) + .into()); + } + + let l2_eth_provider = ProviderBuilder::new() + .on_builtin(&config.l2_rpc_url) + .await?; + + let l2_chain_id = ChainId::new(l2_eth_provider.get_chain_id().await?.to_string()); + + Ok(Self { + l0_client_id: config.l0_client_id, + l1_client_id: config.l1_client_id, + l1_chain_id: ChainId::new(l1_chain_id), + l2_chain_id, + l1_tm_client, + l2_eth_provider, + }) + } + + fn info(config: Self::Config) -> PluginInfo { + PluginInfo { + name: plugin_name(&config.l2_chain_id), + interest_filter: UpdateHook::filter( + &config.l2_chain_id, + &ClientType::new(ClientType::STATE_LENS_EVM), + ), + } + } + + async fn cmd(_config: Self::Config, cmd: Self::Cmd) { + match cmd {} + } +} + +fn plugin_name(chain_id: &ChainId) -> String { + pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME"); + + format!("{PLUGIN_NAME}/{}", chain_id) +} + +impl Module { + fn plugin_name(&self) -> String { + plugin_name(&self.l2_chain_id) + } +} + +#[async_trait] +impl PluginServer for Module { + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn run_pass( + &self, + _: &Extensions, + msgs: Vec>, + ) -> RpcResult> { + Ok(PassResult { + optimize_further: vec![], + ready: msgs + .into_iter() + .map(|mut op| { + UpdateHook::new( + &self.l2_chain_id, + &ClientType::new(ClientType::STATE_LENS_EVM), + |fetch| { + Call::Plugin(PluginMessage::new( + self.plugin_name(), + ModuleCall::from(FetchUpdate { + counterparty_chain_id: fetch.counterparty_chain_id.clone(), + update_from: fetch.update_from, + update_to: fetch.update_to, + }), + )) + }, + ) + .visit_op(&mut op); + + op + }) + .enumerate() + .map(|(i, op)| (vec![i], op)) + .collect(), + }) + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn call(&self, ext: &Extensions, msg: ModuleCall) -> RpcResult> { + match msg { + ModuleCall::FetchUpdate(FetchUpdate { + counterparty_chain_id, + update_from, + update_to, + }) => { + let voy_client = ext.try_get::()?; + let l1_latest_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), true) + .await?; + let l2_consensus_proof = voy_client + .query_ibc_proof( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + ConsensusStatePath { + client_id: self.l1_client_id, + height: update_to.height(), + }, + ) + .await; + let continuation = call(PluginMessage::new( + self.plugin_name(), + ModuleCall::from(FetchUpdateAfterL1Update { + counterparty_chain_id, + update_from, + update_to, + }), + )); + // If the L2 consensus proof exists on the L1, we don't have to update the L2 on the L1. + match l2_consensus_proof { + Ok(_) => Ok(continuation), + Err(_) => Ok(conc([ + // Update the L2 (eth) client on L1 (union) and then dispatch the continuation + promise( + [call(FetchUpdateHeaders { + client_type: ClientType::new(ClientType::ETHEREUM), + chain_id: self.l2_chain_id.clone(), + counterparty_chain_id: self.l1_chain_id.clone(), + update_from, + update_to, + })], + [], + AggregateMsgUpdateClientsFromOrderedHeaders { + ibc_spec_id: IbcUnion::ID, + chain_id: self.l1_chain_id.clone(), + client_id: RawClientId::new(self.l1_client_id.clone()), + }, + ), + seq([ + call(WaitForTrustedHeight { + chain_id: self.l1_chain_id.clone(), + ibc_spec_id: IbcUnion::ID, + client_id: RawClientId::new(self.l1_client_id), + height: update_to, + }), + continuation, + ]), + ])), + } + } + ModuleCall::FetchUpdateAfterL1Update(FetchUpdateAfterL1Update { + counterparty_chain_id, + update_to, + .. + }) => { + let voy_client = ext.try_get::()?; + let l1_latest_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), true) + .await?; + let l0_client_meta = voy_client + .client_meta::( + counterparty_chain_id.clone(), + QueryHeight::Latest, + self.l0_client_id, + ) + .await?; + let l2_consensus_state_proof = serde_json::from_value::( + voy_client + .query_ibc_proof( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + ConsensusStatePath { + client_id: self.l1_client_id, + height: update_to.height(), + }, + ) + .await + .expect("big trouble") + .proof, + ) + .expect("impossible"); + // Dispatch an update for the L1 on the destination, then dispatch the L2 update on the destination + Ok(conc([ + promise( + [call(FetchUpdateHeaders { + client_type: ClientType::new(ClientType::COMETBLS), + chain_id: self.l1_chain_id.clone(), + counterparty_chain_id: counterparty_chain_id.clone(), + update_from: l0_client_meta.height, + update_to: l1_latest_height, + })], + [], + AggregateMsgUpdateClientsFromOrderedHeaders { + ibc_spec_id: IbcUnion::ID, + chain_id: counterparty_chain_id.clone(), + client_id: RawClientId::new(self.l0_client_id.clone()), + }, + ), + seq([ + call(WaitForTrustedHeight { + chain_id: counterparty_chain_id, + ibc_spec_id: IbcSpecId::new(IbcSpecId::UNION), + client_id: RawClientId::new(self.l0_client_id), + height: l1_latest_height, + }), + data(OrderedHeaders { + headers: vec![( + DecodedHeaderMeta { height: update_to }, + into_value(Header { + l1_height: l1_latest_height, + l2_height: update_to, + l2_consensus_state_proof, + }), + )], + }), + ]), + ])) + } + } + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn callback( + &self, + _: &Extensions, + callback: ModuleCallback, + _data: VecDeque, + ) -> RpcResult> { + match callback {} + } +}