From 66c74bf8770649c4be0d18a469b4650cd74f3452 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Wed, 11 Dec 2024 14:07:47 +0100 Subject: [PATCH] feat(evm): introduce `EvmInCosmosClient` state lens --- evm/contracts/clients/EvmInCosmosClient.sol | 311 ++++++++ evm/contracts/lib/MPTVerifier.sol | 776 ++++++++++++++++++++ evm/evm.nix | 74 +- evm/scripts/Deploy.s.sol | 1 + evm/tests/src/lib/MPTVerifier.t.sol | 90 +++ 5 files changed, 1220 insertions(+), 32 deletions(-) create mode 100644 evm/contracts/clients/EvmInCosmosClient.sol create mode 100644 evm/contracts/lib/MPTVerifier.sol create mode 100644 evm/tests/src/lib/MPTVerifier.t.sol diff --git a/evm/contracts/clients/EvmInCosmosClient.sol b/evm/contracts/clients/EvmInCosmosClient.sol new file mode 100644 index 0000000000..73252ade28 --- /dev/null +++ b/evm/contracts/clients/EvmInCosmosClient.sol @@ -0,0 +1,311 @@ +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/Common.sol"; +import "../lib/MPTVerifier.sol"; + +struct Header { + uint64 l1Height; + uint64 l2Height; + bytes l2InclusionProof; + bytes l2ConsensusState; +} + +struct ClientState { + uint32 l1ClientId; + uint32 l2ChainId; + uint32 l2ClientId; + uint64 latestHeight; + 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 ErrInvalidMisbehaviour(); + + 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)); + } +} + +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; + mapping(uint32 => mapping(uint64 => ProcessedMoment)) private + processedMoments; + + 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.latestHeight == 0 || consensusState.timestamp == 0) { + revert EvmInCosmosLib.ErrInvalidInitialConsensusState(); + } + clientStates[clientId] = clientState; + consensusStates[clientId][clientState.latestHeight] = consensusState; + // Normalize to nanosecond because ibc-go recvPacket expects nanos... + processedMoments[clientId][clientState.latestHeight] = ProcessedMoment({ + timestamp: block.timestamp * 1e9, + height: block.number + }); + return ConsensusStateUpdate({ + clientStateCommitment: clientState.commit(), + consensusStateCommitment: consensusState.commit(), + height: clientState.latestHeight + }); + } + + /* + * 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 memory 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(abi.encode(header.l2ConsensusState))) + ) + ) { + revert EvmInCosmosLib.ErrInvalidL1Proof(); + } + + bytes calldata rawL2ConsensusState = header.l2ConsensusState; + uint64 timestampOffset = clientState.timestampOffset; + uint64 stateRootOffset = clientState.stateRootOffset; + uint64 storageRootOffset = clientState.storageRootOffset; + uint64 l2Timestamp; + bytes32 l2StateRoot; + bytes32 l2StorageRoot; + assembly { + l2Timestamp := + calldataload(add(rawL2ConsensusState.offset, timestampOffset)) + l2StateRoot := + calldataload(add(rawL2ConsensusState.offset, stateRootOffset)) + l2StorageRoot := + calldataload(add(rawL2ConsensusState.offset, storageRootOffset)) + } + + if (header.l2Height > clientState.latestHeight) { + clientState.latestHeight = 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; + + // P[H₂] = now() + ProcessedMoment storage processed = + processedMoments[clientId][header.l2Height]; + processed.timestamp = block.timestamp * 1e9; + processed.height = block.number; + + // commit(S₂) + return ConsensusStateUpdate({ + clientStateCommitment: clientState.commit(), + consensusStateCommitment: consensusState.commit(), + height: header.l2Height + }); + } + + function misbehaviour( + uint32 clientId, + bytes calldata clientMessageBytes + ) external override onlyIBC { + revert EvmInCosmosLib.ErrInvalidMisbehaviour(); + } + + 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( + 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(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( + keccak256(abi.encodePacked(path)), + EvmInCosmosLib.EVM_IBC_COMMITMENT_SLOT + ) + ); + (bool exists, bytes calldata provenValue) = 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].latestHeight; + } + + 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/lib/MPTVerifier.sol b/evm/contracts/lib/MPTVerifier.sol new file mode 100644 index 0000000000..333154ec4f --- /dev/null +++ b/evm/contracts/lib/MPTVerifier.sol @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: UNLICENSED +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 1b370c25ff..5e9949bfe0 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -11,6 +11,12 @@ _: { ... }: let + solidity-merkle-trees = pkgs.fetchFromGitHub { + owner = "polytope-labs"; + repo = "solidity-merkle-trees"; + rev = "93b47e7847c05e4f0e6f9f5220cb5330133e166f"; + hash = "sha256-2AX5AeaTSF7Z9JbSqqu0gvpYa8+HIMsdjdCmrMjLE2o="; + }; solidity-stringutils = pkgs.fetchFromGitHub { owner = "Arachnid"; repo = "solidity-stringutils"; @@ -83,6 +89,10 @@ _: { name = "@openzeppelin-foundry-upgradeable"; path = "${openzeppelin-foundry-upgrades}/src"; } + { + name = "@solidity-merkle-trees"; + path = "${solidity-merkle-trees}/src"; + } ]; evmLibs = pkgs.stdenv.mkDerivation { name = "evm-libs-src"; @@ -452,39 +462,39 @@ _: { } ); - # 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/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 + # ''; + # }; hubble-abis = let diff --git a/evm/scripts/Deploy.s.sol b/evm/scripts/Deploy.s.sol index ba1b35eb24..207639c836 100644 --- a/evm/scripts/Deploy.s.sol +++ b/evm/scripts/Deploy.s.sol @@ -12,6 +12,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"; diff --git a/evm/tests/src/lib/MPTVerifier.t.sol b/evm/tests/src/lib/MPTVerifier.t.sol new file mode 100644 index 0000000000..f185959db8 --- /dev/null +++ b/evm/tests/src/lib/MPTVerifier.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "../../../contracts/lib/MPTVerifier.sol"; + +contract MPTVerifierTests is Test { + /* 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); + } +}