diff --git a/solidity/contracts/interfaces/IBlockHashOracle.sol b/solidity/contracts/interfaces/IBlockHashOracle.sol new file mode 100644 index 00000000000..c53383733d6 --- /dev/null +++ b/solidity/contracts/interfaces/IBlockHashOracle.sol @@ -0,0 +1,15 @@ +pragma solidity >=0.6.11; +// SPDX-License-Identifier: MIT OR Apache-2.0 +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@*/ +interface IBlockHashOracle { + function blockhash(uint256 height) external view returns (bytes32 hash); +} diff --git a/solidity/contracts/interfaces/IInterchainSecurityModule.sol b/solidity/contracts/interfaces/IInterchainSecurityModule.sol index 65bc2bcf91b..42557cc1fec 100644 --- a/solidity/contracts/interfaces/IInterchainSecurityModule.sol +++ b/solidity/contracts/interfaces/IInterchainSecurityModule.sol @@ -14,7 +14,8 @@ interface IInterchainSecurityModule { ARB_L2_TO_L1, WEIGHTED_MERKLE_ROOT_MULTISIG, WEIGHTED_MESSAGE_ID_MULTISIG, - OP_L2_TO_L1 + OP_L2_TO_L1, + BLOCKHASH } /** diff --git a/solidity/contracts/interfaces/isms/IBlockHashIsm.sol b/solidity/contracts/interfaces/isms/IBlockHashIsm.sol new file mode 100644 index 00000000000..802ad992695 --- /dev/null +++ b/solidity/contracts/interfaces/isms/IBlockHashIsm.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; +import {IBlockHashOracle} from "../IBlockHashOracle.sol"; + +interface IBlockHashIsm is IInterchainSecurityModule { + function blockHashOracles( + uint32 _origin + ) external view returns (IBlockHashOracle); + + function originMailboxAddresses( + uint32 _origin + ) external view returns (address); + + function setBlockHashOracle( + uint32 _origin, + address _blockHashOracle + ) external; + + function setOriginMailboxAddress( + uint32 _origin, + address _mailboxAddress + ) external; +} diff --git a/solidity/contracts/isms/blockhash/BlockHashIsm.sol b/solidity/contracts/isms/blockhash/BlockHashIsm.sol new file mode 100644 index 00000000000..555cb160e67 --- /dev/null +++ b/solidity/contracts/isms/blockhash/BlockHashIsm.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +// ============ Internal Imports ============ +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {IBlockHashIsm} from "../../interfaces/isms/IBlockHashIsm.sol"; +import {IBlockHashOracle} from "../../interfaces/IBlockHashOracle.sol"; +import {Message} from "../../libs/Message.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; +import {MerklePatriciaProof} from "../../libs/MerklePatriciaProof.sol"; +import {RLPReader} from "../../libs/RLPReader.sol"; +import {PackageVersioned} from "../../PackageVersioned.sol"; + +/** + * @title BlockHashIsm + * @notice + */ + +// ============ ASSUMPTIONS ============ +// Relayers are untrusted +// Blockhash oracles are trusted +// Metadata field is +// Message ID is keccak256 of the message +// Finding the associated logbloom of 'DispatchId(bytes32 indexed messageId )' is the cheapest way to match a blocks succesful inclusion of the message; +// without verifying the canonical address for the mailbox on the origin chain, it is very easy to spoof this check, as it would pass the existence check as long as the log matches the format of the mailbox dispatch event + +contract BlockHashIsm is IBlockHashIsm, Ownable, PackageVersioned { + using Message for bytes; + using TypeCasts for bytes32; + using TypeCasts for address; + using Address for address; + using Strings for uint32; + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + using MerklePatriciaProof for bytes; + // ============ Storage ============ + // The origin chain of the message -> the blockhash oracle for the origin chain + mapping(uint32 => IBlockHashOracle) public blockHashOracles; + mapping(uint32 => address) public originMailboxAddresses; + + // solhint-disable-next-line const-name-snakecase + uint8 public constant moduleType = + uint8(IInterchainSecurityModule.Types.BLOCKHASH); + + // ============ Constructor ============ + constructor() {} + + // ============ External Admin Functions ============ + + function setBlockHashOracle( + uint32 _origin, + address _blockHashOracle + ) external onlyOwner { + require( + _blockHashOracle != address(0), + "BlockHashIsm: Blockhash Oracle cannot be zero address" + ); + blockHashOracles[_origin] = IBlockHashOracle(_blockHashOracle); + } + + function setOriginMailboxAddress( + uint32 _origin, + address _mailbox + ) external onlyOwner { + require( + _mailbox != address(0), + "BlockHashIsm: Mailbox cannot be zero address" + ); + originMailboxAddresses[_origin] = _mailbox; + } + + // ============ Public Functions ============ + + /*** + VIA RETH:primitives Struct Header + pub struct Header { + [0]pub parent_hash: FixedBytes<32>, + [1]pub ommers_hash: FixedBytes<32>, + [2]pub beneficiary: Address, + [3]pub state_root: FixedBytes<32>, + [4]pub transactions_root: FixedBytes<32>, + [5]pub receipts_root: FixedBytes<32>, + [6]pub logs_bloom: Bloom, + [7]pub difficulty: Uint<256, 4>, + [8]pub number: u64, + [9]pub gas_limit: u64, + [10]pub gas_used: u64, + [11]pub timestamp: u64, + [12]pub extra_data: Bytes, + [13]pub mix_hash: FixedBytes<32>, + [14]pub nonce: FixedBytes<8>, + [15]pub base_fee_per_gas: Option, + [16]pub withdrawals_root: Option>, + [17]pub blob_gas_used: Option, + [18]pub excess_blob_gas: Option, + [19]pub parent_beacon_block_root: Option>, + [20]pub requests_hash: Option>, + } + */ + + /** + * @notice Verifies that a message was dispatched, by proving inclusion of a DispatchId log (from the mailbox) + * in a receipt, using a Merkle Patricia proof against the receipts trie of a block, + * whose hash is verified via a trusted block hash oracle. + * @param _metadata is abi encoded block header(rlp) + transaction receipt (rlp) + receipt trie key + receipt trie nodes + * bytes rlpBlockHeader [in full to verify the blockhash] + * bytes expectedReceiptRlp + * bytes receiptTrieKey, + * bytes32 encodedTrieNodes + * @param _message Formatted Hyperlane message (see Message.sol). + */ + struct verifyLocalVars { + bytes rlpBlockHeader; + bytes expectedReceiptRlp; + bytes receiptTrieKey; + bytes encodedTrieNodes; + uint32 originDomain; + uint256 blockNumber; + bytes32 blockHash; + bytes32 computedBlockHash; + bytes32 receiptsRoot; + bytes32 dispatchIDSignature; + bytes32 messageID; + uint256 i; + address emitter; + bytes32 topic0; + bytes32 topic1; + } + + function verify( + bytes calldata _metadata, + bytes calldata _message + ) public view returns (bool) { + verifyLocalVars memory vars; + ( + vars.rlpBlockHeader, + vars.expectedReceiptRlp, + vars.receiptTrieKey, + vars.encodedTrieNodes + ) = abi.decode(_metadata, (bytes, bytes, bytes, bytes)); + + { + vars.originDomain = Message.origin(_message); + RLPReader.RLPItem[] memory blockHeader = vars + .rlpBlockHeader + .toRlpItem() + .toList(); + vars.blockNumber = blockHeader[8].toUint(); + vars.blockHash = blockHashOracles[vars.originDomain].blockhash( + vars.blockNumber + ); + require(vars.blockHash != bytes32(0), "Blockhash Not Found"); + vars.computedBlockHash = keccak256(vars.rlpBlockHeader); + require( + vars.blockHash == vars.computedBlockHash, + "Blockhash Mismatch" + ); + vars.receiptsRoot = bytes32(blockHeader[5].toBytes()); + } + + require( + MerklePatriciaProof.verify( + vars.expectedReceiptRlp, + vars.receiptTrieKey, + vars.encodedTrieNodes, + vars.receiptsRoot + ), + "Invalid Receipt Proof" + ); + + RLPReader.RLPItem[] memory receipt = vars + .expectedReceiptRlp + .toRlpItem() + .toList(); + + /*** + From RETH:primitives Struct Receipt + pub struct Receipt { + pub tx_type: TxType, + pub success: bool, + pub cumulative_gas_used: u64, + pub logs: Vec, + } + */ + + require(receipt.length > 3, "Transaction Malformed"); + require(receipt[1].toUint() == 1, "Transaction Reverted"); + + RLPReader.RLPItem[] memory logs = receipt[3].toList(); + vars.dispatchIDSignature = keccak256("DispatchId(bytes32)"); + vars.messageID = _message.id(); + for (vars.i = 0; vars.i < logs.length; vars.i++) { + RLPReader.RLPItem[] memory log = logs[vars.i].toList(); + + vars.emitter = log[0].toAddress(); + if (vars.emitter != originMailboxAddresses[vars.originDomain]) { + continue; + } + + RLPReader.RLPItem[] memory topics = log[1].toList(); + if (topics.length < 2) { + continue; + } + vars.topic0 = bytes32(topics[0].toUint()); + vars.topic1 = bytes32(topics[1].toUint()); + if ( + vars.topic0 == vars.dispatchIDSignature && + vars.topic1 == vars.messageID + ) { + return true; + } + } + + revert("DispatchId Log Not Found"); + } +} diff --git a/solidity/contracts/libs/MerklePatriciaProof.sol b/solidity/contracts/libs/MerklePatriciaProof.sol new file mode 100644 index 00000000000..6e5a4465e02 --- /dev/null +++ b/solidity/contracts/libs/MerklePatriciaProof.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +// From Polygon fx-portal +pragma solidity ^0.8.0; + +import {RLPReader} from "./RLPReader.sol"; + +library MerklePatriciaProof { + /* + * @dev Verifies a merkle patricia proof. + * @param value The terminating value in the trie. + * @param encodedPath The path in the trie leading to value. + * @param rlpParentNodes The rlp encoded stack of nodes. + * @param root The root hash of the trie. + * @return The boolean validity of the proof. + */ + function verify( + bytes memory value, + bytes memory encodedPath, + bytes memory rlpParentNodes, + bytes32 root + ) internal pure returns (bool) { + RLPReader.RLPItem memory item = RLPReader.toRlpItem(rlpParentNodes); + RLPReader.RLPItem[] memory parentNodes = RLPReader.toList(item); + + bytes memory currentNode; + RLPReader.RLPItem[] memory currentNodeList; + + bytes32 nodeKey = root; + uint256 pathPtr = 0; + + bytes memory path = _getNibbleArray(encodedPath); + if (path.length == 0) { + return false; + } + + for (uint256 i = 0; i < parentNodes.length; i++) { + if (pathPtr > path.length) { + return false; + } + + currentNode = RLPReader.toRlpBytes(parentNodes[i]); + if (nodeKey != keccak256(currentNode)) { + return false; + } + currentNodeList = RLPReader.toList(parentNodes[i]); + + if (currentNodeList.length == 17) { + if (pathPtr == path.length) { + if ( + keccak256(RLPReader.toBytes(currentNodeList[16])) == + keccak256(value) + ) { + return true; + } else { + return false; + } + } + + uint8 nextPathNibble = uint8(path[pathPtr]); + if (nextPathNibble > 16) { + return false; + } + nodeKey = bytes32( + RLPReader.toUintStrict(currentNodeList[nextPathNibble]) + ); + pathPtr += 1; + } else if (currentNodeList.length == 2) { + uint256 traversed = _nibblesToTraverse( + RLPReader.toBytes(currentNodeList[0]), + path, + pathPtr + ); + if (pathPtr + traversed == path.length) { + //leaf node + if ( + keccak256(RLPReader.toBytes(currentNodeList[1])) == + keccak256(value) + ) { + return true; + } else { + return false; + } + } + + //extension node + if (traversed == 0) { + return false; + } + + pathPtr += traversed; + nodeKey = bytes32(RLPReader.toUintStrict(currentNodeList[1])); + } else { + return false; + } + } + } + + function _nibblesToTraverse( + bytes memory encodedPartialPath, + bytes memory path, + uint256 pathPtr + ) private pure returns (uint256) { + uint256 len = 0; + // encodedPartialPath has elements that are each two hex characters (1 byte), but partialPath + // and slicedPath have elements that are each one hex character (1 nibble) + bytes memory partialPath = _getNibbleArray(encodedPartialPath); + bytes memory slicedPath = new bytes(partialPath.length); + + // pathPtr counts nibbles in path + // partialPath.length is a number of nibbles + for (uint256 i = pathPtr; i < pathPtr + partialPath.length; i++) { + bytes1 pathNibble = path[i]; + slicedPath[i - pathPtr] = pathNibble; + } + + if (keccak256(partialPath) == keccak256(slicedPath)) { + len = partialPath.length; + } else { + len = 0; + } + return len; + } + + // bytes b must be hp encoded + function _getNibbleArray( + bytes memory b + ) internal pure returns (bytes memory) { + bytes memory nibbles = ""; + if (b.length > 0) { + uint8 offset; + uint8 hpNibble = uint8(_getNthNibbleOfBytes(0, b)); + if (hpNibble == 1 || hpNibble == 3) { + nibbles = new bytes(b.length * 2 - 1); + bytes1 oddNibble = _getNthNibbleOfBytes(1, b); + nibbles[0] = oddNibble; + offset = 1; + } else { + nibbles = new bytes(b.length * 2 - 2); + offset = 0; + } + + for (uint256 i = offset; i < nibbles.length; i++) { + nibbles[i] = _getNthNibbleOfBytes(i - offset + 2, b); + } + } + return nibbles; + } + + function _getNthNibbleOfBytes( + uint256 n, + bytes memory str + ) private pure returns (bytes1) { + return + bytes1( + n % 2 == 0 ? uint8(str[n / 2]) / 0x10 : uint8(str[n / 2]) % 0x10 + ); + } +} diff --git a/solidity/contracts/libs/RLPReader.sol b/solidity/contracts/libs/RLPReader.sol new file mode 100644 index 00000000000..d479657a994 --- /dev/null +++ b/solidity/contracts/libs/RLPReader.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + */ +pragma solidity >=0.5.10 <0.9.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint256 nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self)); + + uint256 ptr = self.nextPtr; + uint256 itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem( + bytes memory item + ) internal pure returns (RLPItem memory) { + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator( + RLPItem memory self + ) internal pure returns (Iterator memory) { + require(isList(self)); + + uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param the RLP item. + */ + function rlpLen(RLPItem memory item) internal pure returns (uint256) { + return item.len; + } + + /* + * @param the RLP item. + * @return (memPtr, len) pair: location of the item's payload in memory. + */ + function payloadLocation( + RLPItem memory item + ) internal pure returns (uint256, uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 memPtr = item.memPtr + offset; + uint256 len = item.len - offset; // data length + return (memPtr, len); + } + + /* + * @param the RLP item. + */ + function payloadLen(RLPItem memory item) internal pure returns (uint256) { + (, uint256 len) = payloadLocation(item); + return len; + } + + /* + * @param the RLP item containing the encoded list. + */ + function toList( + RLPItem memory item + ) internal pure returns (RLPItem[] memory) { + require(isList(item)); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /* + * @dev A cheaper version of keccak256(toRlpBytes(item)) that avoids copying memory. + * @return keccak256 hash of RLP encoded bytes. + */ + function rlpBytesKeccak256( + RLPItem memory item + ) internal pure returns (bytes32) { + uint256 ptr = item.memPtr; + uint256 len = item.len; + bytes32 result; + assembly { + result := keccak256(ptr, len) + } + return result; + } + + /* + * @dev A cheaper version of keccak256(toBytes(item)) that avoids copying memory. + * @return keccak256 hash of the item payload. + */ + function payloadKeccak256( + RLPItem memory item + ) internal pure returns (bytes32) { + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes32 result; + assembly { + result := keccak256(memPtr, len) + } + return result; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes( + RLPItem memory item + ) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(item.len > 0 && item.len <= 33); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + + uint256 result; + assembly { + result := mload(memPtr) + + // shift to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + // one byte prefix + require(item.len == 33); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(memPtr, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + if (item.len == 0) return 0; + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + itemLen = 1; + } else if (byte0 < STRING_LONG_START) { + itemLen = byte0 - STRING_SHORT_START + 1; + } else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + return 0; + } else if ( + byte0 < STRING_LONG_START || + (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START) + ) { + return 1; + } else if (byte0 < LIST_SHORT_START) { + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + } else { + return byte0 - (LIST_LONG_START - 1) + 1; + } + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy(uint256 src, uint256 dest, uint256 len) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256 ** (WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} diff --git a/solidity/test/isms/BlockHashIsm.t.sol b/solidity/test/isms/BlockHashIsm.t.sol new file mode 100644 index 00000000000..4e99505e9ed --- /dev/null +++ b/solidity/test/isms/BlockHashIsm.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "../../contracts/isms/blockhash/BlockHashIsm.sol"; + +contract MockBlockHashOracle is IBlockHashOracle { + mapping(uint256 => bytes32) public blockHashes; + + function setBlockhash(uint256 blockNumber, bytes32 hash) external { + blockHashes[blockNumber] = hash; + } + + function blockhash( + uint256 height + ) external view override returns (bytes32) { + return blockHashes[height]; + } +} + +contract BlockHashISMTest is Test { + BlockHashIsm ism; + MockBlockHashOracle oracle; + uint256 blockNumber = 22326018; + bytes canonicalBlockhash = + hex"da3962d24352ec1745f50162218f84169f22714fd331ce7e93a4c9c4b76c6126"; + bytes header = + hex"f90258a023f98b87f531bb9373a35c3a8be4922448551b50d2d7229c204302147cdb03b5a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479495222290dd7278aa3ddd389cc1e1d165cc4bafe5a08b70d097b6ddeb02fe51b273073a4a4bb71fd1c6c3a6c04f9054c278e50c6fe7a07a487ea9330afb706d6c610d2be495be2b747ab4960471964cb3274908d576afa09991568d50dd1a7529a96d979832d9843d7e53b02f0dd32ae8caa8e5e5ff3fd8b9010005ab4f44e5cadbd6f4b9e395aabd1bc153f7ea8e5fe327958f7f707ffc3ae948833d6fadb0acd684ca89fa10de5bff80271547749b367db677d73005902e7ee0d55695506c3bebce6c0365c84351b8bb8f824c13b0ecbaf21fdb5f42d97c06716ff901a4cf6300f2b3c6db251bbea8a5a927aff73fe52f59847b3cfc281a03002eb006fd76b5bbba2d79326dff230b2e4c071d63f919a0b9d27e1070ecf26aaa5f63292311b16d5a437731ed5bebada92fc2cf1f5db0c8a7d675ff35b6ab11f43843d2be5b94afc8bc5b6eb34abf1c7583fce58f852fc3955547396e2ad7fff2d7b4e4742679c0359475d4d1d95d1d980ddbb5593fbbaff37331b1c0b7ccd7a980840154ab02840225510083e10506846807d0a38f6265617665726275696c642e6f7267a0121a80be8e53d5341bf92f2468c50716ee76923405bca3980ab21fd192f60160880000000000000000848f39e656a08224781bb194c80c1584421480e3817b17dbb1e508a188340cb711c2bc3dfad6808404f20000a0606f2f2ddf2c3796fb76592d167ed6261c89f44ce51fa0a3019c6cbed9926ef5"; + bytes message = + hex"0300013633000000010000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92000000380000000000000000000000006e9804a08092d8ba4e69dacf422df12459f2599e0000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627a000000000000000000000000000000000000000000000018dc2fdc4bf0d6000000000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000000113"; + bytes transactionHash = + hex"35521245a08c4b3d7cbf5dce7761f63a5570a362fb261fd91c95b1f5b7c900a9"; + bytes receipt = hex"02f9083001836a1b72b9010000000000010000808000000000000800000000000000000000000000002000000000400000000000080020000000280000000000000200000000100000220800004000400000000000000008010100080200080000000000008000000000002000000000020000000000020000000800000000002004000000400010000000000000000000800000002100001000000000000002000000000000000040000000020000020000000200000000000000000240000040000000001000000020000000001002000400000000000000000000000000000008000000000000008020004010400004000000000000100100000000000400000000100200000000040400f90725f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627aa00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000a860e01cc4a889bb2917ec97104510a2e1ae0e53a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000a860e01cc4a889bb2917ec97104510a2e1ae0e53a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94a860e01cc4a889bb2917ec97104510a2e1ae0e53f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f8bc94a860e01cc4a889bb2917ec97104510a2e1ae0e53f863a0dcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92b840000000000000000000000000000000000000000000000018dc2fdc4bf0d60000000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f901be94c005dc82818d67af737725bd4bf75435d065d239f884a0769f711d20c679153d382254f59892613b58a97cc876b249134ac25c80f9c814a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a00000000000000000000000000000000000000000000000000000000000000038a00000000000000000000000006e9804a08092d8ba4e69dacf422df12459f2599eb90120000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000cd0300013633000000010000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92000000380000000000000000000000006e9804a08092d8ba4e69dacf422df12459f2599e0000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627a000000000000000000000000000000000000000000000018dc2fdc4bf0d6000000000000000000000000000000000000000000000000000000000002540be400000000000000000000000000000000000000000000000000000000000000011300000000000000000000000000000000000000f85a94c005dc82818d67af737725bd4bf75435d065d239f842a0788dbc1b7152732178210e7f4d9d010ef016f9eafbe66786bd7169f56e0c353aa0d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be60780f8799448e6c30b97748d1e2e03bf3e9fbe3890ca5f8ccae1a0253a3a04cab70d47c1504809242d9350cd81627b4f1d50753e159cf8cd76ed33b840d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be607000000000000000000000000000000000000000000000000000000000001362ff8bc949e6b1022be9bbf5afd152483dad9b88911bc8611f863a065695c3748edae85a24cc2c60b299b31f463050bc259150d2e5802ec8d11720aa0d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be607a00000000000000000000000000000000000000000000000000000000000000038b840000000000000000000000000000000000000000000000000000000000003ec5c0000000000000000000000000000000000000000000000000003951b1f32a1f3f89b949f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92f863a0d229aacb94204188fe8042965fa6b269c62dc5818b21238779ab64bdd17efeeca00000000000000000000000000000000000000000000000000000000000000038a00000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627aa0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000"; + bytes transactionIndex = hex"53"; //valid key since < 128 + bytes NodeTrie = hex"f90b8cb90134f90131a01de55f8224d584daa95efd45258417e4491419567c95ee785064634fdf3f26d6a0ed1fec15c89dc2c569784886a9fead15b92ac68c20c586d541a63ea8964d72d8a05f9d346fc79cb96c662c640fc91f67a656a5a0873a53ed8187be0831063b7b6ba08f078f153652d45e43d7042149fd050e1f9ab4390ca57d75c16446c1930acb51a0303a011bb17af3d1482af1a7acd08947318d453998072294bdc4aff97d259005a0cc36021e951ed032aad9cdbe4641fa5f66ea90415934fda9bb74623258a957f2a09ce32f716c0aa5705684c56c0c59e4efdacfd74e2873e7c07ce08087c0325d37a0df30358580406687ebd73296262ddf60977afbacd30871bb1b14111aa5b82543a0948b774066f23d465e8ddcb7e4ac945f41cc067049037dc8420ae43d5f5ed0ef8080808080808080b90214f90211a0001cb956dff139ecdcff91caf5854030bb8a1f1db986f34ec83d420690346feda05b2cdd9b6abcc93f16c5f5794ce3d203271b38630f28b503c0289a3297931e3aa0ce9a0aaf54b7c8daed94338d32d96d898300c89764472795943f4dc6bc1cec28a0660ed7f11d22336968ab56ef57dd5140d2d0772c2af7aa5670193a7e8155910ca0372c0158f3e8e4f52a748300b74cd322a01b23236a0f0a2f659ed8c0006ab0e4a0170fdace6f3e14fab391474224444f2919b1794c986192629d76f9af39f150b1a029cf988ba7222c74b0bcf797d4a27c20b248f3002e5f78fc9f62f1a3db79aaa7a09c01dabaae4ac817a5de72dc103152f130223d30ce934c7ede7f259fa06cdc4ba0e56dc47fe6458138581dbf095748d9610c41424c16f5f499580c32770f32e6a4a03c6216a2876277ca5be5d51339a6f75dab88c4899c3a5543dfe3f0f751d1ddada0a885b4ed28c14b8437f38f4507ed65b12c493577043c893a74cd2267f4a07526a0396766a5418922965b02d2b90cf17c0d99ccdbc3d8047944c2513d1f062a7d69a01449a180761e2a82cbb3bad7f8e98adab3dd85ca84811e7c1a8eed331d8c6bcfa0f076b3b74339ce3d92435cb9b5a6b8e301f5f3ca91792eeea539c04ec86ec7e1a002aa30ee6ade73afbad7f5527654255661fdea690c98902361ec2ab5ccc522d4a05e018aaa3cba4e8c6cb35267786ba933135bfd3be6a9d44b7d5c55251442626680b9083bf9083820b9083402f9083001836a1b72b9010000000000010000808000000000000800000000000000000000000000002000000000400000000000080020000000280000000000000200000000100000220800004000400000000000000008010100080200080000000000008000000000002000000000020000000000020000000800000000002004000000400010000000000000000000800000002100001000000000000002000000000000000040000000020000020000000200000000000000000240000040000000001000000020000000001002000400000000000000000000000000000008000000000000008020004010400004000000000000100100000000000400000000100200000000040400f90725f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627aa00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000a860e01cc4a889bb2917ec97104510a2e1ae0e53a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94e1f23869776c82f691d9cb34597ab1830fb0de58f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000a860e01cc4a889bb2917ec97104510a2e1ae0e53a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f89b94a860e01cc4a889bb2917ec97104510a2e1ae0e53f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f8bc94a860e01cc4a889bb2917ec97104510a2e1ae0e53f863a0dcbc1c05240f31ff3ad067ef1ee35ce4997762752e3a095284754544f4c709d7a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92b840000000000000000000000000000000000000000000000018dc2fdc4bf0d60000000000000000000000000000000000000000000000000018dc2fdc4bf0d60000f901be94c005dc82818d67af737725bd4bf75435d065d239f884a0769f711d20c679153d382254f59892613b58a97cc876b249134ac25c80f9c814a00000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92a00000000000000000000000000000000000000000000000000000000000000038a00000000000000000000000006e9804a08092d8ba4e69dacf422df12459f2599eb90120000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000cd0300013633000000010000000000000000000000009f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92000000380000000000000000000000006e9804a08092d8ba4e69dacf422df12459f2599e0000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627a000000000000000000000000000000000000000000000018dc2fdc4bf0d6000000000000000000000000000000000000000000000000000000000002540be400000000000000000000000000000000000000000000000000000000000000011300000000000000000000000000000000000000f85a94c005dc82818d67af737725bd4bf75435d065d239f842a0788dbc1b7152732178210e7f4d9d010ef016f9eafbe66786bd7169f56e0c353aa0d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be60780f8799448e6c30b97748d1e2e03bf3e9fbe3890ca5f8ccae1a0253a3a04cab70d47c1504809242d9350cd81627b4f1d50753e159cf8cd76ed33b840d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be607000000000000000000000000000000000000000000000000000000000001362ff8bc949e6b1022be9bbf5afd152483dad9b88911bc8611f863a065695c3748edae85a24cc2c60b299b31f463050bc259150d2e5802ec8d11720aa0d4be46f1c22a6ade864df2b2322abfe8285447088dc0529cf9cce04a858be607a00000000000000000000000000000000000000000000000000000000000000038b840000000000000000000000000000000000000000000000000000000000003ec5c0000000000000000000000000000000000000000000000000003951b1f32a1f3f89b949f6e6d150977dabc82d5d4eaabdb1f1ab0d25f92f863a0d229aacb94204188fe8042965fa6b269c62dc5818b21238779ab64bdd17efeeca00000000000000000000000000000000000000000000000000000000000000038a00000000000000000000000005e500c201ff3f2c6df76c600b994e32122bf627aa0000000000000000000000000000000000000000000000018dc2fdc4bf0d60000"; + address mailBox = 0xc005dc82818d67AF737725bD4bf75435d065D239; + uint32 originDomain = 1; + + function setUp() public { + oracle = new MockBlockHashOracle(); + ism = new BlockHashIsm(); + ism.setOriginMailboxAddress(originDomain, mailBox); + ism.setBlockHashOracle( + originDomain, + address(oracle) + ); + } + + function testVerify() public { + oracle.setBlockhash(blockNumber, bytes32(canonicalBlockhash)); + + bytes memory metadata = abi.encode( + header, + receipt, + transactionIndex, + NodeTrie + ); + + bool verified = ism.verify(metadata, message); + assertTrue(verified); + } +}