// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "./lib/MerklePatriciaProof.sol"; import "./lib/RLPReader.sol"; interface IAereEthLightClient { function verifyReceiptsRootAgainstFinalized( bytes32 receiptsRoot, bytes32 bodyRoot, bytes32[] calldata branch, uint256 depth, uint256 subtreeIndex ) external view returns (bool); function finalizedSlot() external view returns (uint64); } /** * @title AereEthMessageInbox — deliver an Ethereum event to AERE, trust-minimized. * * @notice A thin inbox over {AereEthLightClient}. It accepts (proof, message) and * delivers a message ONLY if: * 1. the light client attests `receiptsRoot` is committed under the current * FINALIZED Ethereum beacon header (execution-payload SSZ branch), and * 2. the referenced transaction receipt is included in that `receiptsRoot` * via a Merkle-Patricia proof (key = rlp(txIndex)), and * 3. the chosen log inside that receipt was emitted by the registered source * contract on Ethereum. * Each (receiptsRoot, txIndex, logIndex) is delivered at most once. * * The message payload is the log itself: (emitter, topics, data). No bridge * operator can inject a message; forging one requires forging Ethereum * sync-committee finality OR a keccak/MPT collision. * * @dev SECURITY. Inherits the light client's model: >= 2/3 of Ethereum's 512-member * sync committee (classical BLS12-381, quantum-vulnerable). This is Ethereum's * trust model, not AERE's. See AereEthLightClient for the full statement. * * The execution-payload generalized index (depth, subtreeIndex) locating * receipts_root under the beacon body root is FORK-DEPENDENT and supplied by * the caller; the inbox pins the expected values it was configured with so a * caller cannot point the branch at the wrong field. */ contract AereEthMessageInbox { using RLPReader for bytes; IAereEthLightClient public immutable lightClient; /// @notice Registered Ethereum source contract whose logs this inbox accepts. address public immutable sourceContract; /// @notice Pinned execution-payload branch shape (receipts_root under body_root). uint256 public immutable execBranchDepth; uint256 public immutable execBranchIndex; mapping(bytes32 => bool) public delivered; event MessageDelivered( bytes32 indexed receiptsRoot, uint256 indexed txIndex, uint256 indexed logIndex, address emitter, bytes32[] topics, bytes data ); constructor( address lightClient_, address sourceContract_, uint256 execBranchDepth_, uint256 execBranchIndex_ ) { require(lightClient_ != address(0), "Inbox:zero lc"); lightClient = IAereEthLightClient(lightClient_); sourceContract = sourceContract_; execBranchDepth = execBranchDepth_; execBranchIndex = execBranchIndex_; } struct ReceiptProof { bytes32 receiptsRoot; bytes32 finalizedBodyRoot; // the beacon body root the light client finalized bytes32[] execBranch; // receipts_root -> finalized body_root uint256 txIndex; // transaction index in the block bytes[] mptNodes; // receipts-trie nodes along rlp(txIndex) uint256 logIndex; // which log within the receipt to deliver } /// @notice Verify and deliver the log referenced by `p`. Reverts on any failure. /// @return emitter the log's emitting address (== sourceContract) /// @return topics the log topics /// @return data the log data function deliver(ReceiptProof calldata p) external returns (address emitter, bytes32[] memory topics, bytes memory data) { // 1. finality: receiptsRoot committed under the finalized beacon body. require( lightClient.verifyReceiptsRootAgainstFinalized( p.receiptsRoot, p.finalizedBodyRoot, p.execBranch, execBranchDepth, execBranchIndex ), "Inbox:not finalized" ); // 2. inclusion: the receipt is in receiptsRoot at key rlp(txIndex). bytes memory key = _rlpUint(p.txIndex); (bool included, bytes memory receiptValue) = MerklePatriciaProof.verify(p.receiptsRoot, key, p.mptNodes); require(included, "Inbox:receipt not included"); // 3. extract the chosen log and check the emitter. (emitter, topics, data) = _parseLog(receiptValue, p.logIndex); require(emitter == sourceContract, "Inbox:wrong source"); // 4. replay protection. bytes32 id = keccak256(abi.encodePacked(p.receiptsRoot, p.txIndex, p.logIndex)); require(!delivered[id], "Inbox:already delivered"); delivered[id] = true; emit MessageDelivered(p.receiptsRoot, p.txIndex, p.logIndex, emitter, topics, data); } // ------------------------------------------------------------------------- // receipt / log parsing // ------------------------------------------------------------------------- /// @notice Parse the `logIndex`-th log out of an EIP-2718 receipt value. /// Handles both legacy (rlp list) and typed (type byte || rlp list) receipts. function parseLog(bytes memory receiptValue, uint256 logIndex) external pure returns (address emitter, bytes32[] memory topics, bytes memory data) { return _parseLog(receiptValue, logIndex); } function _parseLog(bytes memory receiptValue, uint256 logIndex) internal pure returns (address emitter, bytes32[] memory topics, bytes memory data) { // Strip the EIP-2718 type prefix if present (first byte < 0xc0 => typed). bytes memory rlpList = receiptValue; if (receiptValue.length != 0 && uint8(receiptValue[0]) < 0xc0) { rlpList = new bytes(receiptValue.length - 1); for (uint256 i = 0; i < rlpList.length; i++) rlpList[i] = receiptValue[i + 1]; } // receipt = [status, cumulativeGasUsed, logsBloom, logs] bytes[] memory fields = rlpList.splitList(); require(fields.length == 4, "Inbox:bad receipt"); bytes[] memory logs = fields[3].splitList(); require(logIndex < logs.length, "Inbox:log index oob"); // log = [address, topics[], data] bytes[] memory log = logs[logIndex].splitList(); require(log.length == 3, "Inbox:bad log"); emitter = _toAddress(log[0].readBytes()); bytes[] memory rawTopics = log[1].splitList(); topics = new bytes32[](rawTopics.length); for (uint256 i = 0; i < rawTopics.length; i++) { topics[i] = _toBytes32(rawTopics[i].readBytes()); } data = log[2].readBytes(); } // ------------------------------------------------------------------------- // helpers // ------------------------------------------------------------------------- /// @dev RLP-encode a non-negative integer (transaction index key). function _rlpUint(uint256 v) internal pure returns (bytes memory) { if (v == 0) return hex"80"; // minimal big-endian bytes uint256 len = 0; uint256 t = v; while (t != 0) { len++; t >>= 8; } bytes memory be = new bytes(len); t = v; for (uint256 i = 0; i < len; i++) { be[len - 1 - i] = bytes1(uint8(t & 0xff)); t >>= 8; } if (len == 1 && uint8(be[0]) < 0x80) { return be; // single small byte encodes as itself } return bytes.concat(bytes1(uint8(0x80 + len)), be); } function _toAddress(bytes memory b) internal pure returns (address a) { require(b.length == 20, "Inbox:addr len"); assembly { a := shr(96, mload(add(b, 32))) } } function _toBytes32(bytes memory b) internal pure returns (bytes32 r) { require(b.length == 32, "Inbox:b32 len"); assembly { r := mload(add(b, 32)) } } }