diff --git a/Cargo.lock b/Cargo.lock index 13c9932d33..3b41449d88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2586,9 +2586,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "serde", @@ -4863,6 +4863,17 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[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" @@ -12954,6 +12965,27 @@ dependencies = [ "voyager-message", ] +[[package]] +name = "voyager-client-bootstrap-module-state-lens-evm" +version = "0.1.0" +dependencies = [ + "alloy", + "beacon-api", + "beacon-api-types", + "cometbft-rpc", + "ethereum-light-client-types", + "evm-state-lens-light-client-types", + "ibc-union-spec", + "jsonrpsee", + "serde", + "serde_json", + "tokio", + "tracing", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-bootstrap-module-tendermint" version = "0.1.0" @@ -13029,6 +13061,31 @@ dependencies = [ "voyager-message", ] +[[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" @@ -13147,6 +13204,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" diff --git a/Cargo.toml b/Cargo.toml index f71a7e788c..a4412bdc48 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,11 +115,13 @@ members = [ "voyager/modules/client/ethereum", "voyager/modules/client/movement", "voyager/modules/client/tendermint", + "voyager/modules/client/state-lens/evm", "voyager/modules/client-bootstrap/cometbls", "voyager/modules/client-bootstrap/ethereum", "voyager/modules/client-bootstrap/movement", "voyager/modules/client-bootstrap/tendermint", + "voyager/modules/client-bootstrap/state-lens/evm", "voyager/modules/consensus/berachain", "voyager/modules/consensus/cometbls", @@ -131,6 +134,7 @@ members = [ "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", @@ -208,6 +212,8 @@ scroll-api = { path = "lib/scroll-api", default-features = fal scroll-codec = { path = "lib/scroll-codec", default-features = false } scroll-rpc = { path = "lib/scroll-rpc", 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/CometblsClient.sol b/evm/contracts/clients/CometblsClient.sol index 564b7bb53a..556ec541e7 100644 --- a/evm/contracts/clients/CometblsClient.sol +++ b/evm/contracts/clients/CometblsClient.sol @@ -5,14 +5,13 @@ import "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin-upgradeable/utils/PausableUpgradeable.sol"; -import "./ICS23MembershipVerifier.sol"; -import "./Verifier.sol"; - import "../core/02-client/ILightClient.sol"; import "../core/24-host/IBCStore.sol"; import "../core/24-host/IBCCommitment.sol"; import "../lib/Common.sol"; import "../lib/ICS23.sol"; +import "../lib/CometblsZKVerifier.sol"; +import "../lib/ICS23Verifier.sol"; struct SignedHeader { uint64 height; @@ -392,7 +391,7 @@ contract CometblsClient is } bytes32 contractAddress = clientStates[clientId].contractAddress; bytes32 appHash = consensusStates[clientId][height].appHash; - return ICS23MembershipVerifier.verifyMembership( + return ICS23Verifier.verifyMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), @@ -414,7 +413,7 @@ contract CometblsClient is } bytes32 contractAddress = clientStates[clientId].contractAddress; bytes32 appHash = consensusStates[clientId][height].appHash; - return ICS23MembershipVerifier.verifyNonMembership( + return ICS23Verifier.verifyNonMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), @@ -540,7 +539,7 @@ contract CometblsClient is commitmentHash ]; - return Verifier.verifyProof( + return CometblsZKVerifier.verifyProof( zkp.proof, zkp.proofCommitment, zkp.proofCommitmentPOK, publicInputs ); } diff --git a/evm/contracts/clients/CosmosInCosmosClient.sol b/evm/contracts/clients/CosmosInCosmosClient.sol index 83d5897673..94322428e7 100644 --- a/evm/contracts/clients/CosmosInCosmosClient.sol +++ b/evm/contracts/clients/CosmosInCosmosClient.sol @@ -11,8 +11,7 @@ import "../core/24-host/IBCStore.sol"; import "../core/24-host/IBCCommitment.sol"; import "../lib/ICS23.sol"; import "../lib/Common.sol"; - -import "./ICS23MembershipVerifier.sol"; +import "../lib/ICS23Verifier.sol"; struct TendermintConsensusState { uint64 timestamp; @@ -216,7 +215,7 @@ contract CosmosInCosmosClient is revert CosmosInCosmosLib.ErrClientFrozen(); } bytes32 appHash = consensusStates[clientId][height].appHash; - return ICS23MembershipVerifier.verifyMembership( + return ICS23Verifier.verifyMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), @@ -235,7 +234,7 @@ contract CosmosInCosmosClient is revert CosmosInCosmosLib.ErrClientFrozen(); } bytes32 appHash = consensusStates[clientId][height].appHash; - return ICS23MembershipVerifier.verifyNonMembership( + return ICS23Verifier.verifyNonMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), diff --git a/evm/contracts/clients/EvmInCosmosClient.sol b/evm/contracts/clients/EvmInCosmosClient.sol new file mode 100644 index 0000000000..8fd01b4250 --- /dev/null +++ b/evm/contracts/clients/EvmInCosmosClient.sol @@ -0,0 +1,301 @@ +pragma solidity ^0.8.27; + +import "@openzeppelin-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/utils/PausableUpgradeable.sol"; +import "solidity-bytes-utils/BytesLib.sol"; + +import "../core/02-client/ILightClient.sol"; +import "../core/24-host/IBCStore.sol"; +import "../core/24-host/IBCCommitment.sol"; +import "../lib/ICS23.sol"; +import "../lib/MPTVerifier.sol"; + +struct Header { + uint64 l1Height; + uint64 l2Height; + bytes l2InclusionProof; + bytes l2ConsensusState; +} + +struct ClientState { + string l2ChainId; + uint32 l1ClientId; + uint32 l2ClientId; + uint64 l2LatestHeight; + uint16 timestampOffset; + uint16 stateRootOffset; + uint16 storageRootOffset; +} + +struct ConsensusState { + uint64 timestamp; + bytes32 stateRoot; + bytes32 storageRoot; +} + +library EvmInCosmosLib { + uint256 public constant EVM_IBC_COMMITMENT_SLOT = 0; + + error ErrNotIBC(); + error ErrTrustedConsensusStateNotFound(); + error ErrClientFrozen(); + error ErrInvalidL1Proof(); + error ErrInvalidInitialConsensusState(); + error ErrUnsupported(); + + function encode( + ConsensusState memory consensusState + ) internal pure returns (bytes memory) { + return abi.encode(consensusState); + } + + function encode( + ClientState memory clientState + ) internal pure returns (bytes memory) { + return abi.encode(clientState); + } + + function commit( + ConsensusState memory consensusState + ) internal pure returns (bytes32) { + return keccak256(encode(consensusState)); + } + + function commit( + ClientState memory clientState + ) 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 + ILightClient, + Initializable, + UUPSUpgradeable, + OwnableUpgradeable, + PausableUpgradeable +{ + using EvmInCosmosLib for *; + + address private ibcHandler; + + mapping(uint32 => ClientState) private clientStates; + mapping(uint32 => mapping(uint64 => ConsensusState)) private consensusStates; + + constructor() { + _disableInitializers(); + } + + function initialize( + address _ibcHandler, + address admin + ) public initializer { + __Ownable_init(admin); + ibcHandler = _ibcHandler; + } + + function createClient( + uint32 clientId, + bytes calldata clientStateBytes, + bytes calldata consensusStateBytes + ) external override onlyIBC returns (ConsensusStateUpdate memory update) { + ClientState calldata clientState; + assembly { + clientState := clientStateBytes.offset + } + ConsensusState calldata consensusState; + assembly { + consensusState := consensusStateBytes.offset + } + if (clientState.l2LatestHeight == 0 || consensusState.timestamp == 0) { + revert EvmInCosmosLib.ErrInvalidInitialConsensusState(); + } + clientStates[clientId] = clientState; + consensusStates[clientId][clientState.l2LatestHeight] = consensusState; + return ConsensusStateUpdate({ + clientStateCommitment: clientState.commit(), + consensusStateCommitment: consensusState.commit(), + height: clientState.l2LatestHeight + }); + } + + /* + * We update the L₂ client through the L₁ client. + * Given an L₂ and L₁ heights (H₂, H₁), we prove that L₂[H₂] ∈ L₁[H₁]. + */ + function updateClient( + uint32 clientId, + bytes calldata clientMessageBytes + ) external override onlyIBC returns (ConsensusStateUpdate memory) { + Header calldata header; + assembly { + header := clientMessageBytes.offset + } + ClientState storage clientState = clientStates[clientId]; + ILightClient l1Client = + IBCStore(ibcHandler).getClient(clientState.l1ClientId); + // L₂[H₂] ∈ L₁[H₁] + if ( + !l1Client.verifyMembership( + clientState.l1ClientId, + header.l1Height, + header.l2InclusionProof, + abi.encodePacked( + IBCCommitment.consensusStateCommitmentKey( + clientState.l2ClientId, header.l2Height + ) + ), + abi.encodePacked(keccak256(header.l2ConsensusState)) + ) + ) { + revert EvmInCosmosLib.ErrInvalidL1Proof(); + } + + bytes calldata rawL2ConsensusState = header.l2ConsensusState; + 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.l2LatestHeight) { + clientState.l2LatestHeight = header.l2Height; + } + + // L₂[H₂] = S₂ + // We use ethereum native encoding to make it more efficient. + ConsensusState storage consensusState = + consensusStates[clientId][header.l2Height]; + consensusState.timestamp = l2Timestamp; + consensusState.stateRoot = l2StateRoot; + consensusState.storageRoot = l2StorageRoot; + + // commit(S₂) + return ConsensusStateUpdate({ + clientStateCommitment: clientState.commit(), + consensusStateCommitment: consensusState.commit(), + height: header.l2Height + }); + } + + function misbehaviour( + uint32 clientId, + bytes calldata clientMessageBytes + ) external override onlyIBC { + revert EvmInCosmosLib.ErrUnsupported(); + } + + function verifyMembership( + uint32 clientId, + uint64 height, + bytes calldata proof, + bytes calldata path, + bytes calldata value + ) external virtual returns (bool) { + if (isFrozenImpl(clientId)) { + revert EvmInCosmosLib.ErrClientFrozen(); + } + bytes32 storageRoot = consensusStates[clientId][height].storageRoot; + bytes32 slot = keccak256( + abi.encodePacked(path, EvmInCosmosLib.EVM_IBC_COMMITMENT_SLOT) + ); + (bool exists, bytes calldata provenValue) = MPTVerifier.verifyTrieValue( + proof, keccak256(abi.encodePacked(slot)), storageRoot + ); + return exists + && keccak256(RLP.encodeUint(uint256(bytes32(value)))) + == keccak256(provenValue); + } + + function verifyNonMembership( + uint32 clientId, + uint64 height, + bytes calldata proof, + bytes calldata path + ) external virtual returns (bool) { + if (isFrozenImpl(clientId)) { + revert EvmInCosmosLib.ErrClientFrozen(); + } + bytes32 storageRoot = consensusStates[clientId][height].storageRoot; + bytes32 slot = keccak256( + abi.encodePacked(path, EvmInCosmosLib.EVM_IBC_COMMITMENT_SLOT) + ); + (bool exists,) = MPTVerifier.verifyTrieValue( + proof, keccak256(abi.encodePacked(slot)), storageRoot + ); + return !exists; + } + + function getClientState( + uint32 clientId + ) external view returns (bytes memory) { + return clientStates[clientId].encode(); + } + + function getConsensusState( + uint32 clientId, + uint64 height + ) external view returns (bytes memory) { + return consensusStates[clientId][height].encode(); + } + + function getTimestampAtHeight( + uint32 clientId, + uint64 height + ) external view override returns (uint64) { + return consensusStates[clientId][height].timestamp; + } + + function getLatestHeight( + uint32 clientId + ) external view override returns (uint64) { + return clientStates[clientId].l2LatestHeight; + } + + function isFrozen( + uint32 clientId + ) external view virtual returns (bool) { + return isFrozenImpl(clientId); + } + + function isFrozenImpl( + uint32 clientId + ) internal view returns (bool) { + uint32 l1ClientId = clientStates[clientId].l1ClientId; + return IBCStore(ibcHandler).getClient(l1ClientId).isFrozen(l1ClientId); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + function _onlyIBC() internal view { + if (msg.sender != ibcHandler) { + revert EvmInCosmosLib.ErrNotIBC(); + } + } + + modifier onlyIBC() { + _onlyIBC(); + _; + } +} diff --git a/evm/contracts/clients/Verifier.sol b/evm/contracts/lib/CometblsZKVerifier.sol similarity index 99% rename from evm/contracts/clients/Verifier.sol rename to evm/contracts/lib/CometblsZKVerifier.sol index 067a6b7c09..f41fb9933a 100644 --- a/evm/contracts/clients/Verifier.sol +++ b/evm/contracts/lib/CometblsZKVerifier.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.27; /// @author Remco Bloemen /// @notice Supports verifying Groth16 proofs. Proofs can be in uncompressed /// (256 bytes) and compressed (128 bytes) format. -library Verifier { +library CometblsZKVerifier { // Addresses of precompiles uint256 constant PRECOMPILE_MODEXP = 0x05; uint256 constant PRECOMPILE_ADD = 0x06; diff --git a/evm/contracts/clients/ICS23MembershipVerifier.sol b/evm/contracts/lib/ICS23Verifier.sol similarity index 97% rename from evm/contracts/clients/ICS23MembershipVerifier.sol rename to evm/contracts/lib/ICS23Verifier.sol index 9c138d9d79..f91241b891 100644 --- a/evm/contracts/clients/ICS23MembershipVerifier.sol +++ b/evm/contracts/lib/ICS23Verifier.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import "../lib/ICS23.sol"; import "../lib/UnionICS23.sol"; -library ICS23MembershipVerifier { +library ICS23Verifier { function verifyMembership( bytes32 root, bytes calldata proof, diff --git a/evm/contracts/lib/MPTVerifier.sol b/evm/contracts/lib/MPTVerifier.sol new file mode 100644 index 0000000000..4d7199c391 --- /dev/null +++ b/evm/contracts/lib/MPTVerifier.sol @@ -0,0 +1,775 @@ +pragma solidity ^0.8.27; + +// custom bytes calldata pointer storing (length | offset) in one word, +// also allows calldata pointers to be stored in memory +type BytesCalldata is uint256; + +using BytesCalldataOps for BytesCalldata global; + +// can't introduce global using .. for non UDTs +// each consumer should add the following line: +using BytesCalldataOps for bytes; + +/** + * @author Theori, Inc + * @title BytesCalldataOps + * @notice Common operations for bytes calldata, implemented for both the builtin + * type and our BytesCalldata type. These operations are heavily optimized + * and omit safety checks, so this library should only be used when memory + * safety is not a security issue. + */ +library BytesCalldataOps { + function length( + BytesCalldata bc + ) internal pure returns (uint256 result) { + assembly { + result := shr(128, shl(128, bc)) + } + } + + function offset( + BytesCalldata bc + ) internal pure returns (uint256 result) { + assembly { + result := shr(128, bc) + } + } + + function convert( + BytesCalldata bc + ) internal pure returns (bytes calldata value) { + assembly { + value.offset := shr(128, bc) + value.length := shr(128, shl(128, bc)) + } + } + + function convert( + bytes calldata inp + ) internal pure returns (BytesCalldata bc) { + assembly { + bc := or(shl(128, inp.offset), inp.length) + } + } + + function slice( + BytesCalldata bc, + uint256 start, + uint256 len + ) internal pure returns (BytesCalldata result) { + assembly { + result := shl(128, add(shr(128, bc), start)) // add to the offset and clear the length + result := or(result, len) // set the new length + } + } + + function slice( + bytes calldata value, + uint256 start, + uint256 len + ) internal pure returns (bytes calldata result) { + assembly { + result.offset := add(value.offset, start) + result.length := len + } + } + + function prefix( + BytesCalldata bc, + uint256 len + ) internal pure returns (BytesCalldata result) { + assembly { + result := shl(128, shr(128, bc)) // clear out the length + result := or(result, len) // set it to the new length + } + } + + function prefix( + bytes calldata value, + uint256 len + ) internal pure returns (bytes calldata result) { + assembly { + result.offset := value.offset + result.length := len + } + } + + function suffix( + BytesCalldata bc, + uint256 start + ) internal pure returns (BytesCalldata result) { + assembly { + result := add(bc, shl(128, start)) // add to the offset + result := sub(result, start) // subtract from the length + } + } + + function suffix( + bytes calldata value, + uint256 start + ) internal pure returns (bytes calldata result) { + assembly { + result.offset := add(value.offset, start) + result.length := sub(value.length, start) + } + } + + function split( + BytesCalldata bc, + uint256 start + ) internal pure returns (BytesCalldata, BytesCalldata) { + return (prefix(bc, start), suffix(bc, start)); + } + + function split( + bytes calldata value, + uint256 start + ) internal pure returns (bytes calldata, bytes calldata) { + return (prefix(value, start), suffix(value, start)); + } +} + +/** + * @title RLP + * @author Theori, Inc. + * @notice Gas optimized RLP parsing code. Note that some parsing logic is + * duplicated because helper functions are oddly expensive. + */ +library RLP { + function parseUint( + bytes calldata buf + ) internal pure returns (uint256 result, uint256 size) { + assembly { + // check that we have at least one byte of input + if iszero(buf.length) { revert(0, 0) } + let first32 := calldataload(buf.offset) + let kind := shr(248, first32) + + // ensure it's a not a long string or list (> 0xB7) + // also ensure it's not a short string longer than 32 bytes (> 0xA0) + if gt(kind, 0xA0) { revert(0, 0) } + + switch lt(kind, 0x80) + case true { + // small single byte + result := kind + size := 1 + } + case false { + // short string + size := sub(kind, 0x80) + + // ensure it's not reading out of bounds + if lt(buf.length, size) { revert(0, 0) } + + switch eq(size, 32) + case true { + // if it's exactly 32 bytes, read it from calldata + result := calldataload(add(buf.offset, 1)) + } + case false { + // if it's < 32 bytes, we've already read it from calldata + result := shr(shl(3, sub(32, size)), shl(8, first32)) + } + size := add(size, 1) + } + } + } + + function nextSize( + bytes calldata buf + ) internal pure returns (uint256 size) { + assembly { + if iszero(buf.length) { revert(0, 0) } + let first32 := calldataload(buf.offset) + let kind := shr(248, first32) + + switch lt(kind, 0x80) + case true { + // small single byte + size := 1 + } + case false { + switch lt(kind, 0xB8) + case true { + // short string + size := add(1, sub(kind, 0x80)) + } + case false { + switch lt(kind, 0xC0) + case true { + // long string + let lengthSize := sub(kind, 0xB7) + + // ensure that we don't overflow + if gt(lengthSize, 31) { revert(0, 0) } + + // ensure that we don't read out of bounds + if lt(buf.length, lengthSize) { revert(0, 0) } + size := + shr(mul(8, sub(32, lengthSize)), shl(8, first32)) + size := add(size, add(1, lengthSize)) + } + case false { + switch lt(kind, 0xF8) + case true { + // short list + size := add(1, sub(kind, 0xC0)) + } + case false { + let lengthSize := sub(kind, 0xF7) + + // ensure that we don't overflow + if gt(lengthSize, 31) { revert(0, 0) } + // ensure that we don't read out of bounds + if lt(buf.length, lengthSize) { revert(0, 0) } + size := + shr(mul(8, sub(32, lengthSize)), shl(8, first32)) + size := add(size, add(1, lengthSize)) + } + } + } + } + } + } + + function skip( + bytes calldata buf + ) internal pure returns (bytes calldata) { + uint256 size = RLP.nextSize(buf); + assembly { + buf.offset := add(buf.offset, size) + buf.length := sub(buf.length, size) + } + return buf; + } + + function parseList( + bytes calldata buf + ) internal pure returns (uint256 listSize, uint256 offset) { + assembly { + // check that we have at least one byte of input + if iszero(buf.length) { revert(0, 0) } + let first32 := calldataload(buf.offset) + let kind := shr(248, first32) + + // ensure it's a list + if lt(kind, 0xC0) { revert(0, 0) } + + switch lt(kind, 0xF8) + case true { + // short list + listSize := sub(kind, 0xC0) + offset := 1 + } + case false { + // long list + let lengthSize := sub(kind, 0xF7) + + // ensure that we don't overflow + if gt(lengthSize, 31) { revert(0, 0) } + // ensure that we don't read out of bounds + if lt(buf.length, lengthSize) { revert(0, 0) } + listSize := shr(mul(8, sub(32, lengthSize)), shl(8, first32)) + offset := add(lengthSize, 1) + } + } + } + + function splitBytes( + bytes calldata buf + ) internal pure returns (bytes calldata result, bytes calldata rest) { + uint256 offset; + uint256 size; + assembly { + // check that we have at least one byte of input + if iszero(buf.length) { revert(0, 0) } + let first32 := calldataload(buf.offset) + let kind := shr(248, first32) + + // ensure it's a not list + if gt(kind, 0xBF) { revert(0, 0) } + + switch lt(kind, 0x80) + case true { + // small single byte + offset := 0 + size := 1 + } + case false { + switch lt(kind, 0xB8) + case true { + // short string + offset := 1 + size := sub(kind, 0x80) + } + case false { + // long string + let lengthSize := sub(kind, 0xB7) + + // ensure that we don't overflow + if gt(lengthSize, 31) { revert(0, 0) } + // ensure we don't read out of bounds + if lt(buf.length, lengthSize) { revert(0, 0) } + size := shr(mul(8, sub(32, lengthSize)), shl(8, first32)) + offset := add(lengthSize, 1) + } + } + + result.offset := add(buf.offset, offset) + result.length := size + + let end := add(offset, size) + rest.offset := add(buf.offset, end) + rest.length := sub(buf.length, end) + } + } + + function encodeUint( + uint256 value + ) internal pure returns (bytes memory) { + // allocate our result bytes + bytes memory result = new bytes(33); + + if (value == 0) { + // store length = 1, value = 0x80 + assembly { + mstore(add(result, 1), 0x180) + } + return result; + } + + if (value < 128) { + // store length = 1, value = value + assembly { + mstore(add(result, 1), or(0x100, value)) + } + return result; + } + + if ( + value + > 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ) { + // length 33, prefix 0xa0 followed by value + assembly { + mstore(add(result, 1), 0x21a0) + mstore(add(result, 33), value) + } + return result; + } + + if ( + value + > 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ) { + // length 32, prefix 0x9f followed by value + assembly { + mstore(add(result, 1), 0x209f) + mstore(add(result, 33), shl(8, value)) + } + return result; + } + + assembly { + let length := 1 + for { let min := 0x100 } lt(sub(min, 1), value) { + min := shl(8, min) + } { length := add(length, 1) } + + let bytesLength := add(length, 1) + + // bytes length field + let hi := shl(mul(bytesLength, 8), bytesLength) + + // rlp encoding of value + let lo := or(shl(mul(length, 8), add(length, 0x80)), value) + + mstore(add(result, bytesLength), or(hi, lo)) + } + return result; + } +} + +library MPTVerifier { + using BytesCalldataOps for bytes; + + struct Node { + BytesCalldata data; + bytes32 hash; + } + + // prefix constants + uint8 constant ODD_LENGTH = 1; + uint8 constant LEAF = 2; + uint8 constant MAX_PREFIX = 3; + + function parseHash( + bytes calldata buf + ) internal pure returns (bytes32 result, uint256 offset) { + uint256 value; + (value, offset) = RLP.parseUint(buf); + result = bytes32(value); + } + + /** + * @notice parses concatenated MPT nodes into processed Node structs + * @param input the concatenated MPT nodes + * @return result the parsed nodes array, containing a calldata slice and hash + * for each node + */ + function parseNodes( + bytes calldata input + ) internal pure returns (Node[] memory result) { + uint256 freePtr; + uint256 firstNode; + + // we'll use a dynamic amount of memory starting at the free pointer + // it is crucial that no other allocations happen during parsing + assembly { + freePtr := mload(0x40) + + // corrupt free pointer to cause out-of-gas if allocation occurs + mstore( + 0x40, + 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + ) + + firstNode := freePtr + } + + uint256 count; + while (input.length > 0) { + (uint256 listsize, uint256 offset) = RLP.parseList(input); + bytes calldata node = input.slice(offset, listsize); + BytesCalldata slice = node.convert(); + + uint256 len; + assembly { + len := add(listsize, offset) + + // compute node hash + calldatacopy(freePtr, input.offset, len) + let nodeHash := keccak256(freePtr, len) + + // store the Node struct (calldata slice and hash) + mstore(freePtr, slice) + mstore(add(freePtr, 0x20), nodeHash) + + // advance pointer + count := add(count, 1) + freePtr := add(freePtr, 0x40) + } + + input = input.suffix(len); + } + + assembly { + // allocate the result array and fill it with the node pointers + result := freePtr + mstore(result, count) + freePtr := add(freePtr, 0x20) + for { let i := 0 } lt(i, count) { i := add(i, 1) } { + mstore(freePtr, add(firstNode, mul(0x40, i))) + freePtr := add(freePtr, 0x20) + } + + // update the free pointer + mstore(0x40, freePtr) + } + } + + /** + * @notice parses a compressed MPT proof into arrays of Node structs + * @param nodes the set of nodes used in the compressed proofs + * @param compressed the compressed MPT proof + * @param count the number of proofs expected from the compressed proof + * @return result the array of proofs + */ + function parseCompressedProofs( + Node[] memory nodes, + bytes calldata compressed, + uint256 count + ) internal pure returns (Node[][] memory result) { + uint256 resultPtr; + uint256 freePtr; + + // we'll use a dynamic amount of memory starting at the free pointer + // it is crucial that no other allocations happen during parsing + assembly { + result := mload(0x40) + + // corrupt free pointer to cause out-of-gas if allocation occurs + mstore( + 0x40, + 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + ) + + mstore(result, count) + resultPtr := add(result, 0x20) + freePtr := add(resultPtr, mul(0x20, count)) + } + + (uint256 listSize, uint256 offset) = RLP.parseList(compressed); + compressed = compressed.slice(offset, listSize); + + // parse the indices and populate the proof list + for (; count > 0; count--) { + bytes calldata indices; + (listSize, offset) = RLP.parseList(compressed); + indices = compressed.slice(offset, listSize); + compressed = compressed.suffix(listSize + offset); + + // begin next proof array + uint256 arr; + assembly { + arr := freePtr + freePtr := add(freePtr, 0x20) + } + + // fill proof array + uint256 len; + for (len = 0; indices.length > 0; len++) { + uint256 idx; + (idx, offset) = RLP.parseUint(indices); + indices = indices.suffix(offset); + require( + idx < nodes.length, "invalid node index in compressed proof" + ); + assembly { + let node := mload(add(add(nodes, 0x20), mul(0x20, idx))) + mstore(freePtr, node) + freePtr := add(freePtr, 0x20) + } + } + + assembly { + // store the array length + mstore(arr, len) + + // store the array pointer in the result + mstore(resultPtr, arr) + resultPtr := add(resultPtr, 0x20) + } + } + + assembly { + // update the free pointer + mstore(0x40, freePtr) + } + } + + /** + * @notice Checks if the provided bytes match the key at a given offset + * @param key the MPT key to check against + * @param keyLen the length (in nibbles) of the key + * @param testBytes the subkey to check + */ + function subkeysEqual( + bytes32 key, + uint256 keyLen, + bytes calldata testBytes + ) private pure returns (bool result) { + // arithmetic cannot overflow because testBytes is from calldata + uint256 nibbleLength; + unchecked { + nibbleLength = 2 * testBytes.length; + require(nibbleLength <= keyLen); + } + + assembly { + let shiftAmount := sub(256, shl(2, nibbleLength)) + let testValue := shr(shiftAmount, calldataload(testBytes.offset)) + let subkey := shr(shiftAmount, key) + result := eq(testValue, subkey) + } + } + + /** + * @notice checks the MPT proof. Note: for certain optimizations, we assume + * that the rootHash belongs to a valid ethereum block. Correctness + * is only guaranteed in that case. + * Gas usage depends on both proof size and key nibble values. + * Gas usage for actual ethereum account proofs: ~ 30000 - 45000 + * @param nodes MPT proof nodes, parsed using parseNodes() + * @param key the MPT key, padded with trailing 0s if needed + * @param keyLen the byte length of the MPT key, must be <= 32 + * @param expectedHash the root hash of the MPT + */ + function verifyTrieValueWithNodes( + Node[] memory nodes, + bytes32 key, + uint256 keyLen, + bytes32 expectedHash + ) internal pure returns (bool exists, bytes calldata value) { + // handle completely empty trie case + if (nodes.length == 0) { + require(keccak256(hex"80") == expectedHash, "root hash incorrect"); + return (false, msg.data[:0]); + } + + // we will read the key nibble by nibble, so double the length + unchecked { + keyLen *= 2; + } + + // initialize return values to make solc happy; + // one will always be overwritten before returing + assembly { + value.offset := 0 + value.length := 0 + } + exists = true; + + // we'll use nodes as a pointer, advancing through each element + // end will point to the end of the array + uint256 end; + assembly { + end := add(nodes, add(0x20, mul(0x20, mload(nodes)))) + nodes := add(nodes, 0x20) + } + + while (true) { + bytes calldata node; + { + BytesCalldata slice; + bytes32 nodeHash; + + // load the element and advance the proof pointer + assembly { + // bounds checking + if iszero(lt(nodes, end)) { revert(0, 0) } + + let ptr := mload(nodes) + nodes := add(nodes, 0x20) + + slice := mload(ptr) + nodeHash := mload(add(ptr, 0x20)) + } + node = slice.convert(); + + require(nodeHash == expectedHash, "node hash incorrect"); + } + + // find the length of the first two elements + uint256 size = RLP.nextSize(node); + unchecked { + size += RLP.nextSize(node.suffix(size)); + } + + // we now know which type of node we're looking at: + // leaf + extension nodes have 2 list elements, branch nodes have 17 + if (size == node.length) { + // only two elements, leaf or extension node + bytes calldata encodedPath; + (encodedPath, node) = RLP.splitBytes(node); + + // keep track of whether the key nibbles match + bool keysMatch; + + // the first nibble of the encodedPath tells us the type of + // node and if it contains an even or odd number of nibbles + uint8 firstByte = uint8(encodedPath[0]); + uint8 prefix = firstByte >> 4; + require(prefix <= MAX_PREFIX); + if (prefix & ODD_LENGTH == 0) { + // second nibble is padding, must be 0 + require(firstByte & 0xf == 0); + keysMatch = true; + } else { + // second nibble is part of key + keysMatch = (firstByte & 0xf) == (uint8(bytes1(key)) >> 4); + unchecked { + key <<= 4; + keyLen--; + } + } + + // check the remainder of the encodedPath + encodedPath = encodedPath.suffix(1); + keysMatch = keysMatch && subkeysEqual(key, keyLen, encodedPath); + // cannot overflow because encodedPath is from calldata + unchecked { + key <<= 8 * encodedPath.length; + keyLen -= 2 * encodedPath.length; + } + + if (prefix & LEAF == 0) { + // extension can't prove nonexistence, subkeys must match + require(keysMatch); + + (expectedHash,) = parseHash(node); + } else { + // leaf node, must have used all of key + require(keyLen == 0); + + if (keysMatch) { + // if keys equal, we found the value + (value, node) = RLP.splitBytes(node); + break; + } else { + // if keys aren't equal, key doesn't exist + exists = false; + break; + } + } + } else { + // branch node, this is the hotspot for gas usage + + // there should be 17 elements (16 branch hashes + a value) + // we won't explicitly check this in order to save gas, since + // it's implied by inclusion in a valid ethereum block + + // also note, we never need the value element because we assume + // uniquely-prefixed keys, so branch nodes never hold values + + // fetch the branch for the next nibble of the key + uint256 keyNibble = uint256(key >> 252); + + // skip past the branches we don't need + // we already skipped past 2 elements; start there if we can + uint256 i = 0; + if (keyNibble >= 2) { + i = 2; + node = node.suffix(size); + } + while (i < keyNibble) { + node = RLP.skip(node); + unchecked { + i++; + } + } + + (expectedHash,) = parseHash(node); + // if we've reached an empty branch, key doesn't exist + if (expectedHash == 0) { + exists = false; + break; + } + unchecked { + key <<= 4; + keyLen -= 1; + } + } + } + } + + /** + * @notice checks the MPT proof. Note: for certain optimizations, we assume + * that the rootHash belongs to a valid ethereum block. Correctness + * is only guaranteed in that case. + * Gas usage depends on both proof size and key nibble values. + * Gas usage for actual ethereum account proofs: ~ 30000 - 45000 + * @param proof the encoded MPT proof noodes concatenated + * @param key the MPT key, padded with trailing 0s if needed + * @param rootHash the root hash of the MPT + */ + function verifyTrieValue( + bytes calldata proof, + bytes32 key, + bytes32 rootHash + ) internal pure returns (bool exists, bytes calldata value) { + Node[] memory nodes = parseNodes(proof); + return verifyTrieValueWithNodes(nodes, key, 32, rootHash); + } +} diff --git a/evm/evm.nix b/evm/evm.nix index 0af13da397..a7cef45ec5 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -333,7 +333,7 @@ _: { } ); - eth-deploy-multicall = + eth-deploy-single = { rpc-url, kind, @@ -342,7 +342,7 @@ _: { }: mkCi false ( pkgs.writeShellApplicationWithArgs { - name = "eth-deploy-multicall"; + name = "eth-deploy-single-${kind}"; runtimeInputs = [ self'.packages.forge ]; arguments = [ { @@ -355,6 +355,11 @@ _: { required = true; help = "The contract owner private key."; } + { + arg = "sender_pk"; + required = true; + help = "The sender address that created the contract through the deployer."; + } ]; text = '' ${ensureAtRepositoryRoot} @@ -364,12 +369,13 @@ _: { cp --no-preserve=mode -r ${evmSources}/* . DEPLOYER="$argc_deployer_pk" \ + SENDER="$argc_sender_pk" \ PRIVATE_KEY="$argc_private_key" \ FOUNDRY_PROFILE="script" \ forge script scripts/Deploy.s.sol:Deploy${kind} \ -vvvv \ --rpc-url "${rpc-url}" \ - --broadcast ${extra-args} + --broadcast popd rm -rf "$OUT" @@ -530,39 +536,37 @@ _: { } ); - # Stack too deep :) + # Stack too deep :), again # - solidity-coverage = - pkgs.runCommand "solidity-coverage" - { - buildInputs = [ - self'.packages.forge - pkgs.lcov - ]; - } - '' - cp --no-preserve=mode -r ${evmSources}/* . - FOUNDRY_PROFILE="test" forge coverage --ir-minimum --report lcov - lcov --remove ./lcov.info -o ./lcov.info.pruned \ - 'contracts/Multicall.sol' \ - 'contracts/clients/Verifier.sol' \ - 'contracts/apps/ucs/00-pingpong/*' \ - 'contracts/lib/*' \ - 'contracts/core/OwnableIBCHandler.sol' \ - 'contracts/core/24-host/IBCCommitment.sol' \ - 'contracts/core/25-handler/IBCHandler.sol' \ - 'contracts/clients/ICS23MembershipVerifier.sol' \ - 'tests/*' - genhtml lcov.info.pruned -o $out --branch-coverage - mv lcov.info.pruned $out/lcov.info - ''; - show-solidity-coverage = pkgs.writeShellApplication { - name = "show-solidity-coverage"; - runtimeInputs = [ ]; - text = '' - xdg-open ${self'.packages.solidity-coverage}/index.html - ''; - }; + # solidity-coverage = + # pkgs.runCommand "solidity-coverage" + # { + # buildInputs = [ + # self'.packages.forge + # pkgs.lcov + # ]; + # } + # '' + # cp --no-preserve=mode -r ${evmSources}/* . + # FOUNDRY_PROFILE="test" forge coverage --ir-minimum --report lcov + # lcov --remove ./lcov.info -o ./lcov.info.pruned \ + # 'contracts/Multicall.sol' \ + # 'contracts/apps/ucs/00-pingpong/*' \ + # 'contracts/lib/*' \ + # 'contracts/core/OwnableIBCHandler.sol' \ + # 'contracts/core/24-host/IBCCommitment.sol' \ + # 'contracts/core/25-handler/IBCHandler.sol' \ + # 'tests/*' + # genhtml lcov.info.pruned -o $out --branch-coverage + # mv lcov.info.pruned $out/lcov.info + # ''; + # show-solidity-coverage = pkgs.writeShellApplication { + # name = "show-solidity-coverage"; + # runtimeInputs = [ ]; + # text = '' + # xdg-open ${self'.packages.solidity-coverage}/index.html + # ''; + # }; hubble-abis = let @@ -683,10 +687,16 @@ _: { value = eth-deploy args; }) networks ) + // builtins.listToAttrs ( + builtins.map (args: { + name = "eth-deploy-${args.network}-evm-lens"; + value = eth-deploy-single ({ kind = "EvmLens"; } // args); + }) networks + ) // builtins.listToAttrs ( builtins.map (args: { name = "eth-deploy-${args.network}-multicall"; - value = eth-deploy-multicall ({ kind = "Multicall"; } // args); + value = eth-deploy-single ({ kind = "Multicall"; } // args); }) networks ) // builtins.listToAttrs ( @@ -713,6 +723,18 @@ _: { ); }) networks ) + // builtins.listToAttrs ( + builtins.map (args: { + name = "eth-upgrade-${args.network}-evm-lens-client"; + value = eth-upgrade ( + { + dry = false; + protocol = "EvmInCosmosClient"; + } + // args + ); + }) networks + ) // builtins.listToAttrs ( builtins.map (args: { name = "eth-dryupgrade-${args.network}-ibc"; diff --git a/evm/scripts/Deploy.s.sol b/evm/scripts/Deploy.s.sol index 08de9776ad..ba58cb2117 100644 --- a/evm/scripts/Deploy.s.sol +++ b/evm/scripts/Deploy.s.sol @@ -16,6 +16,7 @@ import "../contracts/core/OwnableIBCHandler.sol"; import "../contracts/clients/CometblsClient.sol"; import {CosmosInCosmosClient} from "../contracts/clients/CosmosInCosmosClient.sol"; +import {EvmInCosmosClient} from "../contracts/clients/EvmInCosmosClient.sol"; import "../contracts/apps/ucs/00-pingpong/PingPong.sol"; import "../contracts/apps/ucs/01-relay/Relay.sol"; import "../contracts/apps/ucs/02-nft/NFT.sol"; @@ -41,6 +42,7 @@ library IBC { library LightClients { string constant NAMESPACE = "lightclients"; string constant COMETBLS = "cometbls"; + string constant STATE_LENS_EVM = "state-lens/evm"; function make( string memory lightClient @@ -104,6 +106,23 @@ abstract contract UnionScript is UnionBase { ); } + function deployEvmLens( + IBCHandler handler, + address owner + ) internal returns (EvmInCosmosClient) { + return EvmInCosmosClient( + deploy( + LightClients.make(LightClients.STATE_LENS_EVM), + abi.encode( + address(new EvmInCosmosClient()), + abi.encodeCall( + EvmInCosmosClient.initialize, (address(handler), owner) + ) + ) + ) + ); + } + function deployCometbls( IBCHandler handler, address owner @@ -176,6 +195,7 @@ abstract contract UnionScript is UnionBase { returns ( IBCHandler, CometblsClient, + EvmInCosmosClient, PingPong, UCS01Relay, UCS02NFT, @@ -183,12 +203,21 @@ abstract contract UnionScript is UnionBase { ) { IBCHandler handler = deployIBCHandler(owner); - CometblsClient client = deployCometbls(handler, owner); + CometblsClient cometblsClient = deployCometbls(handler, owner); + EvmInCosmosClient evmLensClient = deployEvmLens(handler, owner); PingPong pingpong = deployUCS00(handler, owner, 100000000000000); UCS01Relay relay = deployUCS01(handler, owner); UCS02NFT nft = deployUCS02(handler, owner); Multicall multicall = deployMulticall(); - return (handler, client, pingpong, relay, nft, multicall); + return ( + handler, + cometblsClient, + evmLensClient, + pingpong, + relay, + nft, + multicall + ); } } @@ -224,6 +253,48 @@ contract DeployMulticall is UnionScript { } } +contract DeployEvmLens is UnionScript { + using LibString for *; + + address immutable deployer; + address immutable sender; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + } + + function getDeployer() internal view override returns (Deployer) { + return Deployer(deployer); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address owner = vm.addr(privateKey); + + address handler = getDeployed(IBC.BASED); + + vm.startBroadcast(privateKey); + + EvmInCosmosClient evmLensClient = + deployEvmLens(IBCHandler(handler), owner); + + vm.stopBroadcast(); + + console.log("EvmInCosmosClient: ", address(evmLensClient)); + } +} + contract DeployIBC is UnionScript { Deployer immutable deployer; @@ -241,20 +312,23 @@ contract DeployIBC is UnionScript { ( IBCHandler handler, - CometblsClient client, + CometblsClient cometblsClient, + EvmInCosmosClient evmLensClient, PingPong pingpong, UCS01Relay relay, UCS02NFT nft, Multicall multicall ) = deployIBC(vm.addr(privateKey)); - handler.registerClient(LightClients.COMETBLS, client); + handler.registerClient(LightClients.COMETBLS, cometblsClient); + handler.registerClient(LightClients.STATE_LENS_EVM, evmLensClient); vm.stopBroadcast(); console.log("Deployer: ", address(deployer)); console.log("Sender: ", vm.addr(privateKey)); console.log("IBCHandler: ", address(handler)); - console.log("CometblsClient: ", address(client)); + console.log("CometblsClient: ", address(cometblsClient)); + console.log("EvmInCosmosClient: ", address(evmLensClient)); console.log("UCS00: ", address(pingpong)); console.log("UCS01: ", address(relay)); console.log("UCS02: ", address(nft)); @@ -278,20 +352,23 @@ contract DeployDeployerAndIBC is UnionScript { ( IBCHandler handler, - CometblsClient client, + CometblsClient cometblsClient, + EvmInCosmosClient evmLensClient, PingPong pingpong, UCS01Relay relay, UCS02NFT nft, Multicall multicall ) = deployIBC(vm.addr(privateKey)); - handler.registerClient(LightClients.COMETBLS, client); + handler.registerClient(LightClients.COMETBLS, cometblsClient); + handler.registerClient(LightClients.STATE_LENS_EVM, evmLensClient); vm.stopBroadcast(); console.log("Deployer: ", address(deployer)); console.log("Sender: ", vm.addr(privateKey)); console.log("IBCHandler: ", address(handler)); - console.log("CometblsClient: ", address(client)); + console.log("CometblsClient: ", address(cometblsClient)); + console.log("EvmInCosmosClient: ", address(evmLensClient)); console.log("UCS00: ", address(pingpong)); console.log("UCS01: ", address(relay)); console.log("UCS02: ", address(nft)); @@ -332,6 +409,8 @@ contract GetDeployed is Script { address handler = getDeployed(IBC.BASED); address cometblsClient = getDeployed(LightClients.make(LightClients.COMETBLS)); + address evmLensClient = + getDeployed(LightClients.make(LightClients.STATE_LENS_EVM)); address ucs00 = getDeployed(Protocols.make(Protocols.UCS00)); address ucs01 = getDeployed(Protocols.make(Protocols.UCS01)); address ucs02 = getDeployed(Protocols.make(Protocols.UCS02)); @@ -349,6 +428,11 @@ contract GetDeployed is Script { ) ) ); + console.log( + string( + abi.encodePacked("EvmLensClient: ", evmLensClient.toHexString()) + ) + ); console.log(string(abi.encodePacked("UCS00: ", ucs00.toHexString()))); console.log(string(abi.encodePacked("UCS01: ", ucs01.toHexString()))); console.log(string(abi.encodePacked("UCS02: ", ucs02.toHexString()))); @@ -386,6 +470,22 @@ contract GetDeployed is Script { ); impls.serialize(cometblsClient.toHexString(), proxyComet); + string memory proxyEvmLens = "proxyEvmLens"; + proxyEvmLens.serialize( + "contract", + string( + "libs/@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy" + ) + ); + proxyEvmLens = proxyEvmLens.serialize( + "args", + abi.encode( + implOf(evmLensClient), + abi.encodeCall(EvmInCosmosClient.initialize, (handler, sender)) + ) + ); + impls.serialize(evmLensClient.toHexString(), proxyEvmLens); + string memory proxyUCS00 = "proxyUCS00"; proxyUCS00.serialize( "contract", @@ -464,6 +564,14 @@ contract GetDeployed is Script { implComet = implComet.serialize("args", bytes(hex"")); impls.serialize(implOf(cometblsClient).toHexString(), implComet); + string memory implEvmLens = "implEvmLens"; + implEvmLens.serialize( + "contract", + string("contracts/clients/EvmInCosmosClient.sol:EvmInCosmosClient") + ); + implEvmLens = implEvmLens.serialize("args", bytes(hex"")); + impls.serialize(implOf(evmLensClient).toHexString(), implEvmLens); + string memory implUCS00 = "implUCS00"; implUCS00.serialize( "contract", @@ -703,3 +811,42 @@ contract UpgradeCometblsClient is Script { vm.stopBroadcast(); } } + +contract UpgradeEvmInCosmosClient is Script { + using LibString for *; + + address immutable deployer; + address immutable sender; + uint256 immutable privateKey; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + privateKey = vm.envUint("PRIVATE_KEY"); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + address evmLensClient = + getDeployed(LightClients.make(LightClients.STATE_LENS_EVM)); + console.log( + string( + abi.encodePacked("EvmLensClient: ", evmLensClient.toHexString()) + ) + ); + vm.startBroadcast(privateKey); + address newImplementation = address(new EvmInCosmosClient()); + CometblsClient(evmLensClient).upgradeToAndCall( + newImplementation, new bytes(0) + ); + vm.stopBroadcast(); + } +} 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/evm/tests/src/Verifier.t.sol b/evm/tests/src/Verifier.t.sol index 5b8d2aa273..bd5bc4d3c4 100644 --- a/evm/tests/src/Verifier.t.sol +++ b/evm/tests/src/Verifier.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; import "forge-std/Test.sol"; -import "../../contracts/clients/Verifier.sol"; +import "../../contracts/lib/CometblsZKVerifier.sol"; import { CometblsClient, SignedHeader diff --git a/evm/tests/src/lib/MPTVerifier.t.sol b/evm/tests/src/lib/MPTVerifier.t.sol new file mode 100644 index 0000000000..5aaa57552d --- /dev/null +++ b/evm/tests/src/lib/MPTVerifier.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "../../../contracts/lib/MPTVerifier.sol"; + +contract MPTVerifierTests is Test { + function test_verify_ok2() public { + vm.pauseGasMetering(); + bytes[] memory proof = new bytes[](3); + proof[0] = + hex"f90211a0290ff9c2465abdc3e521b0e22d434ca9965d9294f984c4af27b62defa7aa0404a0681afeef44df0f0f3ff44a1fc6b6b1c1b5b3ddf1df4b8334184ac69e06d663cea0e7d87d908639d88cccb5e82139e6969ef7a60ef15f2c1b92a42721c00a684534a0cb0d69ffacac2472aba8113fabe43ae0fb1ec1adc0ba524b4d77a4ae1b9f1834a0d8c1a0faa0ee7b3d651997d9bed61cd1a38fdd1d5811d0f6f35135d505772271a055d3a97b39c767db94b3a1ec2cd527ecee17b2c48b05e478c846f74e8c4b0770a07f84fae77d495ad51e1754ca932a17967af94e0ab56da206569bf581b86ff1a3a0444fdf31592bedd27a9525245ce36aa23bb53767574d2ddf7db0c8ed649b7d08a02724a8992048374ba00a4381a1a0c44a10a3863647977f61e668de5532ae10eda0ceff06700cbb9dc8b2a95604aa18ec3863877f64b0101fdeba6fff45aa220e98a02a3280086775de99c51785b0281459bce312b7231bcd59a030dc3c277bb29854a03506c7687acc02c53b15bb4c15cea1cb1065b7a24a1a931200bd117d88dee84ea055759451409a66a368f9ee9bce914924080954448b54ee7e849fabdd7d5d4124a0fdfe38b6023fb6e7a4728a07872f191a0b173ca2b2f2c9dac4e46b1478594903a03bdaa97bd901df14cad52a8369a832f766f1da1877c72c26dff87e4f8eedf73fa02d69cf9241410b8bd737972cbf4675a8bfdce0f1ce923a7a5c8209579ba0b55180"; + proof[1] = + hex"f901518080a0fe0849c9829308dfeebb656b80c84fd25cddad6195e55da1759fb534872d0565a0492b4ed07e3463e8a3f2280e664efe6dcd914e8b6f96dd457b2fb1514fa4dcf3a0136bc83176214e5c162c0d0ee80ec5c99bb74b612de9d31651547629a6d3bff680a0b03da2ced67fe1e95e2d166e4faeac40f4deb62242e768646e20302e989ab6eb80a06507648f5ee64cf12436e03298b41203bf3bb7344ef267853802bf97fd9b48cba093f776423813a1ca4b75baa37b23e49a02898a68961e357d9e8064663d5bb20380a0450922b2b63e417a5397d6f4346828778a5d58bdf292948e1c542a1ed0319ee480a0d76251a116716185de4f499c9934deb89fb9bfecbd85d4e6d8fa268958fd4eb8a0cc6759e97e0d6b2947385d706a65b69f15512add39d2163b3cbcfd427307df87a036cee042c4ab7e473bfb673d9a8f99eb6d86ad267803005fdeb57eb636121ad180"; + proof[2] = + hex"f843a020d71926b1d4cc00b9747141c15cf96296e56262d843136a42daf00aca967037a1a0faadeddd9e83b87f941ff7ac6c1ff3a55a976f082f579d64ca49253295321ca6"; + bytes memory proofChain = hex""; + for (uint256 i = 0; i < 3; i++) { + proofChain = abi.encodePacked(proofChain, proof[i]); + } + this.checkExistence( + 0x64bec87c43e402ed2648ae3e10c9ba5d980ac30ae0dfcc4c90a47856380ce76a, + proofChain, + keccak256( + abi.encodePacked( + hex"91da3fd0782e51c6b3986e9e672fd566868e71f3dbc2d6c2cd6fbb3e361af2a7", + uint256(0) + ) + ), + RLP.encodeUint( + 0xfaadeddd9e83b87f941ff7ac6c1ff3a55a976f082f579d64ca49253295321ca6 + ) + ); + } + + /* Proof extracted from ethereum mainnet + * {"method":"eth_getProof","params":["0xd1d2eb1b1e90b638588728b4130137d262c87cae",["0x0"], "0x14655B2"],"id":1,"jsonrpc":"2.0"} + */ + function test_verify_ok() public { + vm.pauseGasMetering(); + bytes[] memory proof = new bytes[](7); + proof[0] = + hex"f90211a0b51ceda38c7c0d96cee1d651d8c9001299aae0a56dd4778366faccf8c89802f0a011e1adf2007c6afdc9300271c03ad104cf9ed625a3cca7050416449175f7ef21a0e4187606d7baba63b37fd6978f264374e8d7289da084c4a56170ce1e438ff0f0a061869b1b76c51cc75983fc4792b3fc9c1c5e366a76149979920143afd2899770a0ae2ffd634be69d00ca955e55ad4bb4c1065d40938f82f56d678a87180087d2aba0dcfab65101c9968d7891a91ffc1d6c8bcda2773458d802feca923a7d938f7695a0c62fdc1d9731b77b5310a9a9e1bc9edb79976637f6f29c13ce49459ef7cdb7d5a0fce12c4968e940f0f7dbe888d359b81425bde60f261761608465fd74fa390828a04f77e522f007df2b5c6090006e531d113647900ef01ce8ddad6b6b908e786ce9a04beb43119c19f9f2b94738830b8ca07ce2cb40a2fc60e51567810deda9719527a05085bfa24339e17ba1305a8d7c93468ab8414fde3b1b0ce77ea3f196e16217eaa0071e1a46d2a544b7cc24d3153619887ab88606501aea6f30f03e084dab9da01aa0a27d98ca7583cd6f303c41747e5109978c3399cb632283a9a6d5300366bfc97ca0c38268688069ddd9ec101532ea6f0253025f9df93c6d5e916968221232f8da00a0654ec1fadfb6c2d7849b96c26a1373e111cc6fd30c408ee833e0e2a89c4828f7a04a1eba1371dffabf57cd6f2a1774d2d464968546390a9f4dd78a76444cfce53580"; + proof[1] = + hex"f90211a0e06a0657d0607ed2e2c32e879f439169a7fc4af77b35d9932dbeb2dfebe695c7a0a36e146bac35dbfac21f392c4030f374d6a749fbb09a17f61763374b758850f7a06984b50415a207367532fa5a6191f819b7ac6ef29164bc545b55d49397e2651fa022cc8e966d7c342d94abbe77dbcfb0a52b123f8117d78aa50463b8355acaadaaa0fcd4ae819a2addc899ddc0dda500f51bf61e2f20b3835c9ebd1011d62f28c934a0db9ed7b2486bb67a7971ae8c29683266ad9add781a1825de4e36c890e0f3cdf6a03b12d6c5b2b7211fbe8be70283bfdb3a382f85ec3db7f4e40733037b0d74dd7fa0b47f4e1076af0e9fe906587dd314896bcf4e496660b1f48a7fffd23f82b2ab5da098b00edd42f648defdd793c58f1d7f62ffa20f0b2b49073901496b9735e70e39a03adf726421bddaf8624147ca2ec8abc017e40ad77eada3157da77078ea9adf22a0e9ae74e8967516c78db4ac8aa7de5d80f02cd78b63213c3ba3357410df0a2e04a0cf56383af5dbdf2f6a1faae0681f81d00235ed137d5ac60e9ec0ff6ec8c37617a03826b3b060923bb30be9247355a3a7f570798bd1bdffeaf8c9100a1148f2071ca0a26f8f831f9939d92f85544dd55113f789a2042c5bf6adbf2f1fa261ad0d1266a0d07681f008065226a1ca369667ca08e1b6f76e92e0943d8075a25cb44c000814a01bcb25ed256b6742464e7229c04009279be5064d99ff5beb73361c5e79e6a55480"; + proof[2] = + hex"f90211a09dd4cacf70185dbb577b5c33f042e6f9c549c52331097b52258f214247891619a022dbeaa0beca42f9149bed094e43e8917ae33fb12f4f0ef73918174726a51145a0dc8b421a2dba882b708f4f919a5d150d5ac23098758b51e6582d89e32749d25ba0791a4ba7cd6cbbbe430be28fb6274d96493387c57701f7e93032db5fd149f45ca04d7fb99c8c354be9261724a4fa71c5be06132c600179725e2e5c86c60251da64a042356ecfc50f7b34d72e0dd7317f259fbbbe3f34db86baa21dcf35020f033ae5a01baf1a9894e8a1d9421e89c5b35f9cb1a97d85de2eeaca8cb2887e8acb41339ea0e81d4a406243c4093b54586b4ff46013065260f9e5ef4c00f582a813fc403ae9a0299b27a27116a228dfa8c54475ff83b5f6cb0b220193d3e725d7384c2540f697a0bc67389552138a30be44b93506db12b3c744a49d0cd305284ade2147f1a6d24ba0b15bbf046f439753f42359ecd4c0a9b9bbfc7839db828a683d64d0f1e0eb2609a0b145b111161cfe54b14a06c0d9166303c3531ce1b28fabda37bb65716fc0f0b5a081da4b16a391ec0c44c1008368daeb742f023e953cdb4c22cb125cf67c16c3b8a002ce122b0ece08a93c64c24462663e5671c832091aea0a549b8d97cfe17f5c6aa03a507cd55ea9be74639d78ade7135a6816f677c85e29d9e575ee44b3b470bbe2a05049a431f1ce759fb5fe0053c1ceaa64fc3c3b69a4e936502946080a452e0bbc80"; + proof[3] = + hex"f90211a03600c8c217ed73076745dc695be6dd82dae247c6f8a1ab5a54c138a05430847ea0f1c001ada8d78660bd4e76f7e826d851c5cc8286f86d625be87b34b4f751be29a09a0cca8adf25996197bac969a6b51ff5c4277ef98d63a104c1d36a78c71f1e34a0ff0f8508adb6649051f05bee1918e4e4369ad69ddf75ee8bf85617cd5092ffdda01af98a2f12fb227c56da4fde86d2728154774385f9d4a3439c31b4c58e7fe69fa04303c4a3a0718a2d0802aba9c2f79d60b3da50ae58a66ca96d19d12ee1dad10fa066c67418b17b99c11716aa93dd2f37948f335f5028467deb592c5a760f406513a0b879342a5e6e0f0fcd9782bede64a229619485c1f00f7358fb4fb5a09277bc8ea0d3af6303bae36da3f7c9a77027b3cdbb752efe0ece4df34f12bc95f8b2a2c982a0685aea1196bd14056d1fe4d1ab6376239a2765865bec5e6a58a8ed3cf3687beba0f60c3da6c4bbc68bc02ae62ed14a53c6b456fb39f9a6a29192a3280eba80ca83a07eb4d7ee61fac4cd7d6b60ebd40e39ab67d149f92092fb4544886fcb2c129d98a089165c6c4ec484255323338862b5b4f84c1a425a0544a6197a44de209545163aa0969c546e88a081325e4e94790d16a42b737fcd4449f3ab5cf0357a369e50b1cfa0d4ef7c6ba9e64d60179b5a7cb84b678c9374b232d60edd14b42bcc856950726ca0d0a3de051337fee25685748612adde9bebc1acff896d2142eef25cd85e69b5c380"; + proof[4] = + hex"f8b180808080a0595f19b886413b654f6ec0fc17933985ec962dc71f526bbb58111ce8a6169f5d80808080a00803d929cf7dd0abafcc85912206b2f29b3c3d39b1042def65b0891c3891ef0d80a0a1793298225c34075209c538c84c18c39760df1b851cccb42b039a83e612469ca0b63459eca6ea6705c6b4067115a73202bba37f32db40d5df642138f46f98195980a08c14912091a7a7a1b45434498d91c48461dc50aff8895b0402f7c950b2c4f2178080"; + proof[5] = + hex"f8518080808080a03073e62a78a3f9f4d405308b3da3311763019d81f315ea983561f9cebd783b18808080808080a099d0f432e9ce1cb35f07c66e4936a06134dc48b4d43d4c058fe17dc0ecb281ff80808080"; + proof[6] = + hex"e09e20d9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56301"; + bytes memory proofChain = hex""; + for (uint256 i = 0; i < 7; i++) { + proofChain = abi.encodePacked(proofChain, proof[i]); + } + this.checkExistence( + 0x195170ca4e76873504de92ee3651ba91e339555d9d008c5995e51c2c3ada74eb, + proofChain, + hex"00", + hex"01" + ); + } + + function checkExistence( + bytes32 storageRoot, + bytes calldata proof, + bytes32 slot, + bytes calldata expectedValue + ) public { + vm.resumeGasMetering(); + (bool exists, bytes calldata value) = MPTVerifier.verifyTrieValue( + proof, keccak256(abi.encodePacked(slot)), storageRoot + ); + assertEq(exists, true); + assertEq(value, expectedValue); + } + + function test_verify_absence_ok() public { + vm.pauseGasMetering(); + bytes[] memory proof = new bytes[](6); + proof[0] = + hex"f90211a0b51ceda38c7c0d96cee1d651d8c9001299aae0a56dd4778366faccf8c89802f0a011e1adf2007c6afdc9300271c03ad104cf9ed625a3cca7050416449175f7ef21a0e4187606d7baba63b37fd6978f264374e8d7289da084c4a56170ce1e438ff0f0a061869b1b76c51cc75983fc4792b3fc9c1c5e366a76149979920143afd2899770a0ae2ffd634be69d00ca955e55ad4bb4c1065d40938f82f56d678a87180087d2aba0dcfab65101c9968d7891a91ffc1d6c8bcda2773458d802feca923a7d938f7695a0c62fdc1d9731b77b5310a9a9e1bc9edb79976637f6f29c13ce49459ef7cdb7d5a0fce12c4968e940f0f7dbe888d359b81425bde60f261761608465fd74fa390828a04f77e522f007df2b5c6090006e531d113647900ef01ce8ddad6b6b908e786ce9a04beb43119c19f9f2b94738830b8ca07ce2cb40a2fc60e51567810deda9719527a05085bfa24339e17ba1305a8d7c93468ab8414fde3b1b0ce77ea3f196e16217eaa0071e1a46d2a544b7cc24d3153619887ab88606501aea6f30f03e084dab9da01aa0a27d98ca7583cd6f303c41747e5109978c3399cb632283a9a6d5300366bfc97ca0c38268688069ddd9ec101532ea6f0253025f9df93c6d5e916968221232f8da00a0654ec1fadfb6c2d7849b96c26a1373e111cc6fd30c408ee833e0e2a89c4828f7a04a1eba1371dffabf57cd6f2a1774d2d464968546390a9f4dd78a76444cfce53580"; + proof[1] = + hex"f90211a0d2ad4e2cda383901cb101634058b0c28584f168817c8447237daeb3c9faf6a57a02aded885b60f7182faa84f45d253edfab7a4038ee77c0175a8a20c9ecdd5f7c1a0438af8d7678818cc1e6f8bd83fc257651d70271cb471aa44b8ae9ca1aaa39786a0461f00dce5db7e9aa3ea7809371894a533df231c19a7c560bc7dd8ed2da011bba0c7a35d543c85562f030230c6da9e7b14009763aede970ced5a7aa386efcc62aaa09ed4100cc66c040897a2f0ea8aad36b9e40a0a2d4c60ecd7e6f2fa1d0e9fe707a007dadece03c60c19d74e6b837b77016ec48bb2c03ee427f87a12fa2302414a70a099ea06c8a1ac2f55e4d92ee67d611246e4a5decbfacfb401280d36fdd7ed72f0a04314144712573463dd20917ec938c54c19d033345f4eefaa8176d5cc0947cd1ba0b186d1d4416d902040cd48bf76d66b9c07178a295eedd0b27dcdb9a1c62eaf3ea0725af34d18fa40a2bceb40044cc53887f6142ce85a117594880a961d7338ff5da0a5e885ec57fe93a8ea19d0a7d59978504369d1d70cc6725be2c3ce8269e8e43da03a9da344b108b810acdbb908a5126884db431ab6f92c1ae4ec8588878c9755e0a060725a3a4909300cb466cead03f96631829328cad205178b209075f93e3fba2aa088932d16240dba2d1e610ba21f8ae39eec93c7ea2b7f24cfaf5382b9473a6df7a07c8ef77d3c8bf3eba9410f21bd181cd5e3ab748853591bcb4590b369b51f8b6c80"; + proof[2] = + hex"f90211a0c3f832d8e98835a14ea9a03c00a01cbf29276bfb67651eac13718367a4ad0e76a0db6816cae273414a9a5df1737a56881974aed8c1d9b04cf1f507046a61cc7b1ba04063f66644dda617fe33e8d5ea79a099060be0b795b1bcae23f3385368176e0fa08dbda17ff3ac3ac7119738929d5b3e7b3693a838fdc619dfaa1c3edf56d2d471a0cf3b6bca6c7f17ba1850a73298fd39ee664da3477fec632db21b4dcd606e000aa026cee290ba68be291699ccfdf9a36b9c71095823fdd7a2728c3c7c4de7a5a5a7a078edcc7bd5823abc0d061a6161f08b8570a0ea1e18c37bd45daa2c8f92e66c79a05aa01ed340d6660dfbba7d83ed8f3f7c67051eacb094539f4c6e66f5380ac811a0607e85089c3c93110100f72589316c5ee05a294170b9c7c237ba3e6d7b43fe24a08f62d04a9f3f53bbc0eedbd6977ab1b310f95dfac18c474e18276d10e841eaf8a016faef66b158b706e9ef82cbce0af7633a07a692b4efb0c190a7b512bdd60d53a0cf3b7eec574b244719ec6e6839d77ae6729b601f08ed95005604a8db1ab00a09a0cd3c3095df97c9704ca69e39f5447e025dddde3a88dd627d6b53498e752b1222a0633ba144a3457f62fee4d360651e9bf74b47e586a0ea728f007c5384782d40bba05fa650d9a8e984e22122a9e0f784c05eaaf715a6c47b4e25ef11f5c76eaa4a7da0ee4d4150dc5bcb72e56b064379612bf9f98f737669ddb2dee0fc37e106b6f07480"; + proof[3] = + hex"f90211a05c977fc66d9a243988617d8342a5b16e5c699dad0d0fb41b9e2338eeb50d3243a0a384c741370fc093dcfd685ef64e17cd7f60620e3c5667944875c711e866f04fa0f1b3c8885165550d9c6210b68c06f403deec85c80ce416622704f9c4f2fb0d67a046f967f97da2767a0441a3b6e781d50a2d4c0bcc8486712f3fb2fbedaa33b656a0bb65d399adb76fdcf6b65a191886fb1371bcf24508f75a7cce35bd89a35117a5a0464f1500e7d54aa4947a16d0f6853db12cca3fb0d3e75b05b44d64ea3af6fcfca02fd9eca689c827f6583dadca743f4fb215b2098fd1fceb0ce2641d25191058d3a03d3507f62fc570fb59a2a6af53a0902ab6c09f7ae8553f81ff408d03fef60258a0cf027ae79de4be99b553c4107000c7dbbe0f86a8feceb0e84cf54e47e00676c1a0759bc87c6eebad2fa9ce8d3ab17926bae7140dc63e92ac9eff66b3e7c0bd0688a02b3a89578a034492fd59e93d0db0112ca9a3291ddcc631b7786edb10859d8015a0ca725677841d29a9acc1a1b61232d2d8332649b33ddd2160294901e20dac5e96a08f494c65b6346ca006ffbc13fce5089beb3f3e60b85091e598baef14abccead1a0dec06af2f069a106a419338744178e5b240302c17db4dc61a88c78ac6763f1eba0713ef37453d2d3c1fbe0a897c9f0757a3d1fdb53a20504aee155da26df70c117a0c4c286a1d37c2bb0a776d0895391e5dd89b1d902ac1526dedead2859faf8137d80"; + proof[4] = + hex"f89180a0b0ed89cc0fc540cb9cf910b35e5a017a0f171ef740c046716e23c35af3e668b380a05fff94dc1b65c002a713803772dc0ea531377d4e35f723163b8ee85a9747dc47a0f818819b52ccd3f4617889ab8480540cf7f2d591dade8b04a843d5ab3bb3781980808080808080a0e826c12966bfaf8019ae0e008dd2a972b92255bb7bab9a9283d6803f1de1a19d80808080"; + proof[5] = hex""; + bytes memory proofChain = hex""; + for (uint256 i = 0; i < 6; i++) { + proofChain = abi.encodePacked(proofChain, proof[i]); + } + this.checkNonexistence( + 0x195170ca4e76873504de92ee3651ba91e339555d9d008c5995e51c2c3ada74eb, + proofChain, + bytes32(uint256(1)) + ); + } + + function checkNonexistence( + bytes32 storageRoot, + bytes calldata proof, + bytes32 slot + ) public { + vm.resumeGasMetering(); + (bool exists, bytes calldata _value) = MPTVerifier.verifyTrieValue( + proof, keccak256(abi.encodePacked(slot)), storageRoot + ); + assertEq(exists, false); + } +} diff --git a/lib/chain-utils/src/cosmos_sdk.rs b/lib/chain-utils/src/cosmos_sdk.rs index 89a2bc2a6d..7700291389 100644 --- a/lib/chain-utils/src/cosmos_sdk.rs +++ b/lib/chain-utils/src/cosmos_sdk.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +#[allow(unused_imports, reason = "it is used???")] use bip32::secp256k1::ecdsa::signature::SignatureEncoding; use prost::{Message, Name}; use serde::{Deserialize, Serialize}; diff --git a/lib/state-lens/evm-light-client-types/Cargo.toml b/lib/state-lens/evm-light-client-types/Cargo.toml new file mode 100644 index 0000000000..5a7de0de49 --- /dev/null +++ b/lib/state-lens/evm-light-client-types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +edition = "2021" +name = "evm-state-lens-light-client-types" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["sol-types"], optional = true } +protos = { workspace = true, optional = true, features = ["proto_full", "serde"] } +serde = { workspace = true, optional = true, features = ["derive"] } +thiserror = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi", "proto"] } + +[features] +default = [] +ethabi = ["unionlabs/ethabi", "dep:alloy", "dep:protos"] +serde = ["dep:serde"] diff --git a/lib/state-lens/evm-light-client-types/src/client_state.rs b/lib/state-lens/evm-light-client-types/src/client_state.rs new file mode 100644 index 0000000000..325dfdc6a5 --- /dev/null +++ b/lib/state-lens/evm-light-client-types/src/client_state.rs @@ -0,0 +1,87 @@ +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ClientState { + /// l2 chain id + pub l2_chain_id: String, + /// l1 client id used to check the l2 inclusion proof against + pub l1_client_id: u32, + /// l2 client id + pub l2_client_id: u32, + /// l2 latest height + pub l2_latest_height: u64, + /// the offset at which we extract the u64 timestamp from the l2 consensus state + /// timestamp = consensus_state[timestamp_offset:timestamp_offset+8] + pub timestamp_offset: u16, + /// the offset at which we extract the bytes32 state root from the l2 consensus state + /// state_root = consensus_state[state_root_offset:state_root_offset+32] + pub state_root_offset: u16, + /// the offset at which we extract the bytes32 storage root (of the ibc contract on the l2) from the l2 consensus state + /// storage_root = consensus_state[storage_root_offset:storage_root_offset+32] + pub storage_root_offset: u16, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use core::str; + use std::string::FromUtf8Error; + + use alloy::sol_types::SolValue; + use unionlabs::{ + encoding::{Decode, Encode, EthAbi}, + TryFromEthAbiBytesErrorAlloy, + }; + + use crate::ClientState; + + alloy::sol! { + struct SolClientState { + string l2ChainId; + uint32 l1ClientId; + uint32 l2ClientId; + uint64 l2LatestHeight; + uint16 timestampOffset; + uint16 stateRootOffset; + uint16 storageRootOffset; + } + } + + impl Encode for ClientState { + fn encode(self) -> Vec { + SolClientState { + l2ChainId: self.l2_chain_id, + l1ClientId: self.l1_client_id, + l2ClientId: self.l2_client_id, + l2LatestHeight: self.l2_latest_height, + timestampOffset: self.timestamp_offset, + stateRootOffset: self.state_root_offset, + storageRootOffset: self.storage_root_offset, + } + .abi_encode_params() + } + } + + impl Decode for ClientState { + type Error = TryFromEthAbiBytesErrorAlloy; + + fn decode(bytes: &[u8]) -> Result { + let client_state = SolClientState::abi_decode(bytes, true)?; + + Ok(Self { + l2_chain_id: String::from_utf8(client_state.l2ChainId.into_bytes()) + .map_err(|err| TryFromEthAbiBytesErrorAlloy::Convert(Error::ChainId(err)))?, + l1_client_id: client_state.l1ClientId, + l2_client_id: client_state.l2ClientId, + l2_latest_height: client_state.l2LatestHeight, + timestamp_offset: client_state.timestampOffset, + state_root_offset: client_state.stateRootOffset, + storage_root_offset: client_state.storageRootOffset, + }) + } + } + + #[derive(Debug, Clone, PartialEq, thiserror::Error)] + pub enum Error { + #[error("invalid chain_id")] + ChainId(#[from] FromUtf8Error), + } +} diff --git a/lib/state-lens/evm-light-client-types/src/consensus_state.rs b/lib/state-lens/evm-light-client-types/src/consensus_state.rs new file mode 100644 index 0000000000..b55b86da8f --- /dev/null +++ b/lib/state-lens/evm-light-client-types/src/consensus_state.rs @@ -0,0 +1,50 @@ +use unionlabs::hash::H256; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ConsensusState { + /// Timestamp of the execution layer. + pub timestamp: u64, + /// State root of the execution layer. + pub state_root: H256, + /// Storage root of the ibc contract extracted from the state root. + pub storage_root: H256, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use alloy::sol_types::SolValue; + use unionlabs::impl_ethabi_via_try_from_into; + + use super::*; + + impl_ethabi_via_try_from_into!(ConsensusState => SolConsensusState); + + alloy::sol! { + struct SolConsensusState { + uint64 timestamp; + bytes32 stateRoot; + bytes32 storageRoot; + } + } + + impl From for SolConsensusState { + fn from(value: ConsensusState) -> Self { + Self { + timestamp: value.timestamp, + stateRoot: value.state_root.get().into(), + storageRoot: value.storage_root.get().into(), + } + } + } + + impl From for ConsensusState { + fn from(value: SolConsensusState) -> Self { + Self { + timestamp: value.timestamp, + state_root: H256::new(value.stateRoot.0), + storage_root: H256::new(value.storageRoot.0), + } + } + } +} diff --git a/lib/state-lens/evm-light-client-types/src/header.rs b/lib/state-lens/evm-light-client-types/src/header.rs new file mode 100644 index 0000000000..751d06ec9a --- /dev/null +++ b/lib/state-lens/evm-light-client-types/src/header.rs @@ -0,0 +1,125 @@ +use unionlabs::{ + bytes::Bytes, + ibc::core::{client::height::Height, commitment::merkle_proof::MerkleProof}, +}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Header { + pub l1_height: Height, + pub l2_height: Height, + pub l2_consensus_state_proof: MerkleProof, + pub l2_consensus_state: Bytes, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use alloy::sol_types::SolValue; + use unionlabs::{ + encoding::{Encode, EthAbi}, + union::ics23, + }; + + use crate::Header; + + impl Encode for Header { + fn encode(self) -> Vec { + Into::::into(self).abi_encode_params() + } + } + + alloy::sol! { + struct SolHeader { + uint64 l1Height; + uint64 l2Height; + bytes l2InclusionProof; + bytes l2ConsensusState; + } + } + + #[derive(Debug, Clone, PartialEq, thiserror::Error)] + pub enum Error {} + + impl From
for SolHeader { + fn from(value: Header) -> Self { + Self { + l1Height: value.l1_height.height(), + l2Height: value.l2_height.height(), + l2InclusionProof: encode_merkle_proof_for_evm(value.l2_consensus_state_proof) + .into(), + l2ConsensusState: value.l2_consensus_state.into(), + } + } + } + + // FIXME: deduplicate with voyager/module/client/cometbls, in unionlabs? + fn encode_merkle_proof_for_evm( + proof: unionlabs::ibc::core::commitment::merkle_proof::MerkleProof, + ) -> Vec { + alloy::sol! { + struct ExistenceProof { + bytes key; + bytes value; + bytes leafPrefix; + InnerOp[] path; + } + + struct NonExistenceProof { + bytes key; + ExistenceProof left; + ExistenceProof right; + } + + struct InnerOp { + bytes prefix; + bytes suffix; + } + + struct ProofSpec { + uint256 childSize; + uint256 minPrefixLength; + uint256 maxPrefixLength; + } + } + + let merkle_proof = ics23::merkle_proof::MerkleProof::try_from( + protos::ibc::core::commitment::v1::MerkleProof::from(proof), + ) + .unwrap(); + + let convert_inner_op = |i: unionlabs::union::ics23::inner_op::InnerOp| InnerOp { + prefix: i.prefix.into(), + suffix: i.suffix.into(), + }; + + let convert_existence_proof = + |e: unionlabs::union::ics23::existence_proof::ExistenceProof| ExistenceProof { + key: e.key.into(), + value: e.value.into(), + leafPrefix: e.leaf_prefix.into(), + path: e.path.into_iter().map(convert_inner_op).collect(), + }; + + let exist_default = || ics23::existence_proof::ExistenceProof { + key: vec![].into(), + value: vec![].into(), + leaf_prefix: vec![].into(), + path: vec![], + }; + + match merkle_proof { + ics23::merkle_proof::MerkleProof::Membership(a, b) => { + (convert_existence_proof(a), convert_existence_proof(b)).abi_encode_params() + } + ics23::merkle_proof::MerkleProof::NonMembership(a, b) => ( + NonExistenceProof { + key: a.key.into(), + left: convert_existence_proof(a.left.unwrap_or_else(exist_default)), + right: convert_existence_proof(a.right.unwrap_or_else(exist_default)), + }, + convert_existence_proof(b), + ) + .abi_encode_params(), + } + } +} diff --git a/lib/state-lens/evm-light-client-types/src/lib.rs b/lib/state-lens/evm-light-client-types/src/lib.rs new file mode 100644 index 0000000000..3d184a96ce --- /dev/null +++ b/lib/state-lens/evm-light-client-types/src/lib.rs @@ -0,0 +1,5 @@ +pub mod client_state; +pub mod consensus_state; +pub mod header; + +pub use crate::{client_state::ClientState, consensus_state::ConsensusState, header::Header}; diff --git a/lib/voyager-core/src/lib.rs b/lib/voyager-core/src/lib.rs index 158aac2702..a73698251e 100644 --- a/lib/voyager-core/src/lib.rs +++ b/lib/voyager-core/src/lib.rs @@ -113,6 +113,14 @@ impl ClientType { /// [Movement]: https://github.com/movementlabsxyz/movement pub const MOVEMENT: &'static str = "movement"; + // TODO: Update this doc comment and rename the client type to be more accurate + /// 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 + /// [CometBLS]: https://github.com/unionlabs/cometbls + 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/call.rs b/lib/voyager-message/src/call.rs index 7c77ce29c5..6b8c4d6439 100644 --- a/lib/voyager-message/src/call.rs +++ b/lib/voyager-message/src/call.rs @@ -132,6 +132,7 @@ pub struct WaitForTrustedHeight { pub ibc_spec_id: IbcSpecId, pub client_id: RawClientId, pub height: Height, + pub finalized: bool, } impl CallT for Call { @@ -258,6 +259,7 @@ impl CallT for Call { ibc_spec_id, client_id, height, + finalized, }) => { let trusted_client_state_meta = ctx .rpc_server @@ -265,7 +267,11 @@ impl CallT for Call { .client_meta( &chain_id, &ibc_spec_id, - QueryHeight::Latest, + if finalized { + QueryHeight::Finalized + } else { + QueryHeight::Latest + }, client_id.clone(), ) .await @@ -288,6 +294,7 @@ impl CallT for Call { ibc_spec_id, client_id, height, + finalized, }), ])) } diff --git a/lib/voyager-message/src/lib.rs b/lib/voyager-message/src/lib.rs index 6dab91a431..10d5b0458b 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::{ @@ -31,7 +32,7 @@ use tracing::{ use unionlabs::{bytes::Bytes, ibc::core::client::height::Height, traits::Member, ErrorReporter}; use voyager_core::{ ChainId, ClientInfo, ClientStateMeta, ClientType, IbcInterface, IbcSpec, IbcSpecId, - IbcStorePathKey, QueryHeight, + IbcStorePathKey, QueryHeight, Timestamp, }; use voyager_vm::{ItemId, QueueError, QueueMessage}; @@ -564,6 +565,48 @@ 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, + client_type: ClientType, + height: QueryHeight, + ) -> RpcResult { + let client_state = self + .0 + .self_client_state(chain_id, client_type, height) + .await + .map_err(json_rpc_error_to_error_object)?; + Ok(client_state) + } + + pub async fn self_consensus_state( + &self, + chain_id: ChainId, + client_type: ClientType, + height: QueryHeight, + ) -> RpcResult { + let consensus_state = self + .0 + .self_consensus_state(chain_id, client_type, 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/lib/voyager-message/src/rpc/server.rs b/lib/voyager-message/src/rpc/server.rs index a37d5c6d15..1a6cc3a4e8 100644 --- a/lib/voyager-message/src/rpc/server.rs +++ b/lib/voyager-message/src/rpc/server.rs @@ -254,7 +254,7 @@ impl Server { .unwrap(), ) .await - .map_err(fatal_error)?; + .map_err(json_rpc_error_to_error_object)?; trace!(%client_state); diff --git a/voyager/modules/client-bootstrap/state-lens/evm/Cargo.toml b/voyager/modules/client-bootstrap/state-lens/evm/Cargo.toml new file mode 100644 index 0000000000..c0f4ad169b --- /dev/null +++ b/voyager/modules/client-bootstrap/state-lens/evm/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2021" +name = "voyager-client-bootstrap-module-state-lens-evm" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +beacon-api = { workspace = true } +beacon-api-types = { workspace = true, features = ["serde"] } +cometbft-rpc = { workspace = true } +ethereum-light-client-types = { workspace = true, features = ["serde"] } +evm-state-lens-light-client-types = { workspace = true, features = ["serde"] } +ibc-union-spec.workspace = true +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-message = { workspace = true } +voyager-vm = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/voyager/modules/client-bootstrap/state-lens/evm/src/main.rs b/voyager/modules/client-bootstrap/state-lens/evm/src/main.rs new file mode 100644 index 0000000000..a0832116dc --- /dev/null +++ b/voyager/modules/client-bootstrap/state-lens/evm/src/main.rs @@ -0,0 +1,96 @@ +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, ClientType, QueryHeight}, + into_value, + module::{ClientBootstrapModuleInfo, ClientBootstrapModuleServer}, + ClientBootstrapModule, ExtensionsExt, VoyagerClient, +}; +use voyager_vm::BoxDynError; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module { + 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, +} + +#[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, +} + +impl ClientBootstrapModule for Module { + type Config = Config; + + async fn new( + config: Self::Config, + info: ClientBootstrapModuleInfo, + ) -> Result { + Ok(Self { + l2_chain_id: info.chain_id, + 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 ClientBootstrapModuleServer for Module { + #[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(), + ClientType::new(ClientType::ETHEREUM), + 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/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..4725801ab7 --- /dev/null +++ b/voyager/modules/client/state-lens/evm/src/main.rs @@ -0,0 +1,223 @@ +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::{DecodeAs, EncodeAs, EthAbi}, + ibc::core::client::height::Height, + ErrorReporter, +}; +use voyager_message::{ + core::{ + ChainId, ClientStateMeta, ClientType, ConsensusStateMeta, ConsensusType, IbcInterface, + Timestamp, + }, + into_value, + 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)?; + info.ensure_consensus_type(ConsensusType::ETHEREUM)?; + info.ensure_ibc_interface(IbcInterface::IBC_SOLIDITY)?; + 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()), + counterparty_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: Timestamp::from_nanos(cs.timestamp), + }) + } + + #[instrument] + async fn decode_client_state(&self, _: &Extensions, client_state: Bytes) -> RpcResult { + Ok(into_value(Module::decode_client_state(&client_state)?)) + } + + #[instrument] + async fn decode_consensus_state( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + Ok(into_value(Module::decode_consensus_state( + &consensus_state, + )?)) + } + + #[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 { + let proof = serde_json::from_value::(proof).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize proof: {}", ErrorReporter(err)), + None::<()>, + ) + })?; + // TODO: extract to unionlabs? this is MPT proofs encoding for EVM + // the solidity MPT verifier expects the proof RLP nodes to be serialized in sequence + Ok(proof.proof.concat().into()) + } +} diff --git a/voyager/modules/consensus/berachain/src/main.rs b/voyager/modules/consensus/berachain/src/main.rs index e9996bef74..d2e73cf146 100644 --- a/voyager/modules/consensus/berachain/src/main.rs +++ b/voyager/modules/consensus/berachain/src/main.rs @@ -5,13 +5,11 @@ use alloy::{ 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, @@ -21,7 +19,6 @@ use unionlabs::{ }; use voyager_message::{ core::{ChainId, ConsensusType, Timestamp}, - into_value, module::{ConsensusModuleInfo, ConsensusModuleServer}, ConsensusModule, ExtensionsExt, VoyagerClient, }; diff --git a/voyager/modules/consensus/cometbls/src/main.rs b/voyager/modules/consensus/cometbls/src/main.rs index 6c9adb1e4d..f41f53b402 100644 --- a/voyager/modules/consensus/cometbls/src/main.rs +++ b/voyager/modules/consensus/cometbls/src/main.rs @@ -127,7 +127,6 @@ impl ConsensusModuleServer for Module { } /// 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, @@ -165,10 +164,8 @@ impl ConsensusModuleServer for Module { } } - Ok( - Timestamp::from_nanos(commit_response.signed_header.header.time.as_unix_nanos()) - .try_into() - .expect("should be fine"), - ) + Ok(Timestamp::from_nanos( + commit_response.signed_header.header.time.as_unix_nanos(), + )) } } diff --git a/voyager/modules/consensus/ethereum/src/main.rs b/voyager/modules/consensus/ethereum/src/main.rs index 4aa9aaa081..e63ef9720c 100644 --- a/voyager/modules/consensus/ethereum/src/main.rs +++ b/voyager/modules/consensus/ethereum/src/main.rs @@ -156,7 +156,6 @@ impl ConsensusModuleServer for Module { } /// Query the latest finalized timestamp of this chain. - // TODO: Use a better timestamp type here #[instrument(skip_all, fields(chain_id = %self.chain_id, finalized))] async fn query_latest_timestamp( &self, diff --git a/voyager/modules/consensus/tendermint/src/main.rs b/voyager/modules/consensus/tendermint/src/main.rs index 67e54c215c..66edea08a2 100644 --- a/voyager/modules/consensus/tendermint/src/main.rs +++ b/voyager/modules/consensus/tendermint/src/main.rs @@ -157,10 +157,8 @@ impl ConsensusModuleServer for Module { } } - Ok( - Timestamp::from_nanos(commit_response.signed_header.header.time.as_unix_nanos()) - .try_into() - .expect("should be fine"), - ) + Ok(Timestamp::from_nanos( + commit_response.signed_header.header.time.as_unix_nanos(), + )) } } diff --git a/voyager/modules/proof/ethereum/Cargo.toml b/voyager/modules/proof/ethereum/Cargo.toml index 362c7021e7..70c08a87ca 100644 --- a/voyager/modules/proof/ethereum/Cargo.toml +++ b/voyager/modules/proof/ethereum/Cargo.toml @@ -4,7 +4,7 @@ name = "voyager-proof-module-ethereum" version = "0.1.0" [dependencies] -alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws"] } +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } ethereum-light-client-types = { workspace = true } ibc-union-spec.workspace = true jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } diff --git a/voyager/modules/state/movement/src/main.rs b/voyager/modules/state/movement/src/main.rs index c85ce706e4..1bfb8f3c7c 100644 --- a/voyager/modules/state/movement/src/main.rs +++ b/voyager/modules/state/movement/src/main.rs @@ -130,7 +130,6 @@ impl Module { } /// Query the latest finalized timestamp of this chain. - // TODO: Use a better timestamp type here #[instrument(skip_all, fields(chain_id = %self.chain_id))] pub async fn query_latest_timestamp(&self, e: &Extensions) -> RpcResult { let latest_height = self.query_latest_height(e).await?; @@ -145,7 +144,7 @@ impl Module { debug!(%timestamp, %latest_height, "latest timestamp"); - Ok(Timestamp::from_nanos(timestamp).try_into().unwrap()) + Ok(Timestamp::from_nanos(timestamp)) } Err(err) => Err(ErrorObject::owned( -1, diff --git a/voyager/plugins/client-update/berachain/src/main.rs b/voyager/plugins/client-update/berachain/src/main.rs index 9d2c40cd32..050f0e3948 100644 --- a/voyager/plugins/client-update/berachain/src/main.rs +++ b/voyager/plugins/client-update/berachain/src/main.rs @@ -265,6 +265,7 @@ impl PluginServer for Module { // `counterparty_chain_id` client_id: RawClientId::new(self.l1_client_id), height: update_to, + finalized: true, })]), data(OrderedHeaders { headers: vec![( 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..7ec196b15c --- /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 = ["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..04c9d85ace --- /dev/null +++ b/voyager/plugins/client-update/state-lens/evm/src/main.rs @@ -0,0 +1,341 @@ +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::{debug, instrument}; +use unionlabs::ibc::core::commitment::merkle_proof::MerkleProof; +use voyager_message::{ + call::{Call, FetchUpdateHeaders, WaitForTrustedHeight}, + callback::AggregateMsgUpdateClientsFromOrderedHeaders, + core::{ChainId, ClientType, IbcSpec, 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_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"); + let l2_merkle_proof = unionlabs::union::ics23::merkle_proof::MerkleProof::try_from( + protos::ibc::core::commitment::v1::MerkleProof::from(l2_consensus_state_proof), + ) + .expect("impossible"); + 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_merkle_proof { + unionlabs::union::ics23::merkle_proof::MerkleProof::Membership(_, _) => { + Ok(continuation) + } + _ => 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), + }, + ), + 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, + finalized: true, + }), + continuation, + ]), + ])), + } + } + ModuleCall::FetchUpdateAfterL1Update(FetchUpdateAfterL1Update { + counterparty_chain_id, + .. + }) => { + let voy_client = ext.try_get::()?; + let l1_latest_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), false) + .await?; + debug!("l1 latest height {}", l1_latest_height); + let l0_client_meta = voy_client + .client_meta::( + counterparty_chain_id.clone(), + QueryHeight::Latest, + self.l0_client_id, + ) + .await?; + let l1_client_meta = voy_client + .client_meta::( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + self.l1_client_id, + ) + .await?; + // The client has been updated to at least update_to + let update_to = l1_client_meta.counterparty_height; + debug!("l0 client meta {:#?}", l0_client_meta); + let l2_consensus_state_path = ConsensusStatePath { + client_id: self.l1_client_id, + height: update_to.height(), + }; + let l2_consensus_state = voy_client + .query_ibc_state( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + l2_consensus_state_path.clone(), + ) + .await? + .state; + debug!("l2 consensus state {:#?}", l2_consensus_state); + let l2_consensus_state_proof = serde_json::from_value::( + voy_client + .query_ibc_proof( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + l2_consensus_state_path, + ) + .await + .expect("big trouble") + .proof, + ) + .expect("impossible"); + debug!("l2 consensus state proof {:#?}", l2_consensus_state_proof); + // 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.counterparty_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), + }, + ), + seq([ + call(WaitForTrustedHeight { + chain_id: counterparty_chain_id, + ibc_spec_id: IbcUnion::ID, + client_id: RawClientId::new(self.l0_client_id), + height: l1_latest_height, + finalized: false, + }), + data(OrderedHeaders { + headers: vec![( + DecodedHeaderMeta { height: update_to }, + into_value(Header { + l1_height: l1_latest_height, + l2_height: update_to, + l2_consensus_state_proof, + l2_consensus_state, + }), + )], + }), + ]), + ])) + } + } + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn callback( + &self, + _: &Extensions, + callback: ModuleCallback, + _data: VecDeque, + ) -> RpcResult> { + match callback {} + } +} diff --git a/voyager/plugins/event-source/cosmos-sdk/Cargo.toml b/voyager/plugins/event-source/cosmos-sdk/Cargo.toml index 6e108172a6..1738353dd3 100644 --- a/voyager/plugins/event-source/cosmos-sdk/Cargo.toml +++ b/voyager/plugins/event-source/cosmos-sdk/Cargo.toml @@ -11,7 +11,7 @@ dashmap = { workspace = true } enumorph = { workspace = true } ibc-classic-spec.workspace = true ibc-solidity = { workspace = true, features = ["serde"] } -ibc-union-spec.workspace = true +ibc-union-spec = { workspace = true, features = ["tracing"] } jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } macros = { workspace = true } prost = { workspace = true } diff --git a/voyager/plugins/periodic-client-update/src/main.rs b/voyager/plugins/periodic-client-update/src/main.rs index 245e8e4755..30dbf2df81 100644 --- a/voyager/plugins/periodic-client-update/src/main.rs +++ b/voyager/plugins/periodic-client-update/src/main.rs @@ -158,6 +158,7 @@ impl Module { client_meta.counterparty_height.revision(), client_meta.counterparty_height.height() + max_age, ), + finalized: false, }), call(PluginMessage::new( self.plugin_name(), diff --git a/voyager/plugins/transaction-batch/Cargo.toml b/voyager/plugins/transaction-batch/Cargo.toml index 9b9761becb..cc22935039 100644 --- a/voyager/plugins/transaction-batch/Cargo.toml +++ b/voyager/plugins/transaction-batch/Cargo.toml @@ -4,7 +4,7 @@ name = "voyager-plugin-transaction-batch" version = "0.1.0" [dependencies] -alloy = { workspace = true, features = ["sol-types"] } +alloy = { workspace = true, features = ["sol-types", "rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } either = { workspace = true } enumorph = { workspace = true } futures = { workspace = true } diff --git a/voyager/plugins/transaction-batch/src/callback.rs b/voyager/plugins/transaction-batch/src/callback.rs index e51a214905..15d6c2995b 100644 --- a/voyager/plugins/transaction-batch/src/callback.rs +++ b/voyager/plugins/transaction-batch/src/callback.rs @@ -4,7 +4,7 @@ use enumorph::Enumorph; use ibc_classic_spec::IbcClassic; use ibc_union_spec::IbcUnion; use itertools::Itertools; -use jsonrpsee::{core::RpcResult, types::ErrorObject}; +use jsonrpsee::core::RpcResult; use macros::model; use tracing::{debug, instrument, warn}; use unionlabs::ibc::core::client::height::Height; @@ -12,7 +12,7 @@ use voyager_message::{ call::{SubmitTx, WaitForTrustedHeight}, core::{ChainId, ClientStateMeta, QueryHeight}, data::{Data, IbcDatagram, OrderedClientUpdates}, - PluginMessage, RawClientId, VoyagerClient, VoyagerMessage, FATAL_JSONRPC_ERROR_CODE, + PluginMessage, RawClientId, VoyagerClient, VoyagerMessage, }; use voyager_vm::{call, conc, noop, promise, seq, Op}; @@ -49,7 +49,7 @@ where module_server: &Module, datas: VecDeque, ) -> RpcResult> { - let updates @ OrderedClientUpdates { .. } = datas + let updates: Option = datas .into_iter() .exactly_one() .map_err(|found| serde_json::to_string(&found.collect::>()).unwrap()) @@ -57,17 +57,7 @@ where d.try_into() .map_err(|found| serde_json::to_string(&found).unwrap()) }) - .map_err(|found| { - ErrorObject::owned( - FATAL_JSONRPC_ERROR_CODE, - format!( - "OrderedHeaders not present in data queue for \ - AggregateMsgUpdateClientsFromOrderedHeaders, \ - found {found}", - ), - None::<()>, - ) - })?; + .ok(); let client_meta = voyager_client .client_meta::( @@ -78,17 +68,22 @@ where .await?; let new_trusted_height = updates - .updates - .last() - .expect("must have at least one update") - .0 - .height; + .as_ref() + .map(|updates| { + updates + .updates + .last() + .expect("must have at least one update") + .0 + .height + }) + .unwrap_or(client_meta.counterparty_height); make_msgs( module_server, self.client_id, self.batches, - Some(updates), + updates, client_meta, new_trusted_height, ) @@ -238,6 +233,7 @@ impl MakeBatchTransaction { client_id: RawClientId::new(self.client_id.clone()), ibc_spec_id: V::ID, height: required_consensus_height, + finalized: false, }), call(SubmitTx { chain_id, diff --git a/voyager/plugins/transaction/cosmos-sdk/src/main.rs b/voyager/plugins/transaction/cosmos-sdk/src/main.rs index e942d1662f..c92a6fc78f 100644 --- a/voyager/plugins/transaction/cosmos-sdk/src/main.rs +++ b/voyager/plugins/transaction/cosmos-sdk/src/main.rs @@ -361,6 +361,7 @@ impl Module { .encode_as::(), ) .expect("signing failed") + .to_bytes() .to_vec(); let tx_raw_bytes = TxRaw { @@ -534,6 +535,7 @@ impl Module { .encode_as::(), ) .expect("signing failed") + .to_bytes() .to_vec(); let result = client @@ -651,7 +653,7 @@ impl PluginServer for Module { ModuleCall::SubmitTransaction(msgs) => { let mut out = vec![]; - for msgs in msgs.chunks(5) { + for msgs in msgs.chunks(1) { let res = self .do_send_transaction(msgs.to_vec()) .await diff --git a/voyager/plugins/transaction/ethereum/Cargo.toml b/voyager/plugins/transaction/ethereum/Cargo.toml index d3895e8319..a4438a1688 100644 --- a/voyager/plugins/transaction/ethereum/Cargo.toml +++ b/voyager/plugins/transaction/ethereum/Cargo.toml @@ -4,7 +4,7 @@ name = "voyager-transaction-plugin-ethereum" version = "0.1.0" [dependencies] -alloy = { workspace = true, features = ["contract", "network", "providers", "signers", "signer-local"] } +alloy = { workspace = true, features = ["contract", "network", "providers", "signers", "signer-local", "rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } bip32 = { workspace = true } chain-utils = { workspace = true } enumorph = { workspace = true } diff --git a/voyager/plugins/transaction/ethereum/src/main.rs b/voyager/plugins/transaction/ethereum/src/main.rs index 151bdadaeb..7d8725d9db 100644 --- a/voyager/plugins/transaction/ethereum/src/main.rs +++ b/voyager/plugins/transaction/ethereum/src/main.rs @@ -27,7 +27,7 @@ use unionlabs::{ ErrorReporter, }; use voyager_message::{ - core::{ChainId, IbcSpec}, + core::ChainId, data::Data, hook::SubmitTxHook, module::{PluginInfo, PluginServer}, diff --git a/voyager/src/main.rs b/voyager/src/main.rs index e8bf5fdc8c..4a33d36a26 100644 --- a/voyager/src/main.rs +++ b/voyager/src/main.rs @@ -716,7 +716,7 @@ fn print_json(t: &T) { // TODO: Extract all logic here to a plugin pub mod utils { - use anyhow::{anyhow, bail}; + use anyhow::bail; use ibc_classic_spec::IbcClassic; use ibc_union_spec::IbcUnion; use serde_json::Value; @@ -767,25 +767,25 @@ pub mod utils { .await?; trace!(%self_consensus_state); - let consensus_type = ctx - .rpc_server - .modules()? - .chain_consensus_type(&counterparty_chain_id)?; - - let client_consensus_type = ctx - .rpc_server - .modules()? - .client_consensus_type(&client_type)?; - - if client_consensus_type != consensus_type { - return Err(anyhow!( - "attempted to create a {client_type} client on \ - {chain_id} tracking {counterparty_chain_id}, but \ - the consensus of that chain ({consensus_type}) is \ - not verifiable by a client of type {client_type} \ - (which instead verifies {client_consensus_type})." - )); - } + // let consensus_type = ctx + // .rpc_server + // .modules()? + // .chain_consensus_type(&counterparty_chain_id)?; + + // let client_consensus_type = ctx + // .rpc_server + // .modules()? + // .client_consensus_type(&client_type)?; + + // if client_consensus_type != consensus_type { + // return Err(anyhow!( + // "attempted to create a {client_type} client on \ + // {chain_id} tracking {counterparty_chain_id}, but \ + // the consensus of that chain ({consensus_type}) is \ + // not verifiable by a client of type {client_type} \ + // (which instead verifies {client_consensus_type})." + // )); + // } let client_module = ctx.rpc_server