const { expect } = require("chai"); const { ethers } = require("hardhat"); const H = require("./helpers"); const T = require("./trie"); // ----------------------------------------------------------------------------- // AereEthMessageInbox — finality (via light client) + receipts-trie MPT inclusion + // replay protection. The receipts trie is built by an INDEPENDENT JS MPT builder; // the on-chain MerklePatriciaProof verifier walks it. Finality is isolated by // bootstrapping the light client directly with the finalized body root that commits // the receiptsRoot (the sync-committee path is covered in the light-client suite). // ----------------------------------------------------------------------------- const GVR = "0x" + "22".repeat(32); // Synthetic execution-payload branch shape: receipts_root committed under the beacon // body root at (depth, subtreeIndex). In production these are the fork-specific // generalized index for ExecutionPayload.receipts_root under BeaconBlockBody. const EXEC_DEPTH = 9; const EXEC_INDEX = 291; const EXEC_GINDEX = (1 << EXEC_DEPTH) + EXEC_INDEX; // 803 const SOURCE = ethers.getAddress("0x00000000000000000000000000000000000000A1"); const OTHER = ethers.getAddress("0x00000000000000000000000000000000000000B2"); const TOPIC0 = "0x" + "aa".repeat(32); const TOPIC1 = "0x" + "bb".repeat(32); // Build receipts, the receipts trie, and the finalized body tree that commits its root. function buildScenario(receipts) { const entries = receipts.map((r, i) => ({ key: T.txIndexKey(i), value: r.value })); const { root: receiptsRoot, nodes } = T.buildTrie(entries); // finalized beacon body tree: receiptsRoot at EXEC_GINDEX. const assign = {}; assign[EXEC_GINDEX] = receiptsRoot; const bodyTree = H.buildTree(EXEC_DEPTH, assign); return { receiptsRoot, nodes, entries, bodyTree }; } async function deployStack(bodyRoot, source = SOURCE) { const bootstrapFinalized = { slot: 5000, proposerIndex: 3, parentRoot: "0x" + "00".repeat(32), stateRoot: "0x" + "01".repeat(32), bodyRoot: H.hex(bodyRoot), // commits the receiptsRoot at EXEC_GINDEX }; const LC = await ethers.getContractFactory("AereEthLightClient"); // any nonzero committee root; the inbox path doesn't process sync updates. const lc = await LC.deploy(H.hex(H.DST), GVR, "0x" + "07".repeat(32), bootstrapFinalized); await lc.waitForDeployment(); const Inbox = await ethers.getContractFactory("AereEthMessageInbox"); const inbox = await Inbox.deploy(await lc.getAddress(), source, EXEC_DEPTH, EXEC_INDEX); await inbox.waitForDeployment(); return { lc, inbox }; } function receiptWith(type, logs) { return { value: T.encodeReceipt(type, { status: 1, cumulativeGas: 21000, logs }) }; } function proofFor(scn, txIndex, logIndex) { const key = T.txIndexKey(txIndex); const mptNodes = T.getProof(scn.receiptsRoot, scn.nodes, key).map((b) => "0x" + b.toString("hex")); return { receiptsRoot: H.hex(scn.receiptsRoot), finalizedBodyRoot: H.hex(scn.bodyTree.root), execBranch: scn.bodyTree.branch(EXEC_GINDEX).map(H.hex), txIndex, mptNodes, logIndex, }; } describe("AereEthMessageInbox (finality + MPT receipt inclusion)", function () { it("delivers a real message: finalized receiptsRoot + MPT inclusion + correct source", async function () { const msgData = "0x" + "1234abcd".repeat(4); const receipts = [ receiptWith(2, [{ address: SOURCE, topics: [TOPIC0, TOPIC1], data: msgData }]), ]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); const p = proofFor(scn, 0, 0); await expect(inbox.deliver(p)) .to.emit(inbox, "MessageDelivered") .withArgs(H.hex(scn.receiptsRoot), 0, 0, SOURCE, [TOPIC0, TOPIC1], msgData); const id = ethers.keccak256( ethers.solidityPacked(["bytes32", "uint256", "uint256"], [H.hex(scn.receiptsRoot), 0, 0]) ); expect(await inbox.delivered(id)).to.equal(true); }); it("selects the right receipt out of a multi-receipt trie (branch/extension nodes)", async function () { const receipts = []; for (let i = 0; i < 6; i++) { const addr = i === 3 ? SOURCE : OTHER; receipts.push(receiptWith(2, [{ address: addr, topics: [TOPIC0], data: "0x" + "0".repeat(8) }])); } const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); // receipt at txIndex 3 was emitted by SOURCE. const p = proofFor(scn, 3, 0); await expect(inbox.deliver(p)).to.emit(inbox, "MessageDelivered"); }); it("supports legacy (type-0) receipts too", async function () { const receipts = [receiptWith(0, [{ address: SOURCE, topics: [TOPIC0], data: "0xdead" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); await expect(inbox.deliver(proofFor(scn, 0, 0))).to.emit(inbox, "MessageDelivered"); }); it("parses the correct log from a multi-log receipt", async function () { const receipts = [ receiptWith(2, [ { address: OTHER, topics: [TOPIC0], data: "0x00" }, { address: SOURCE, topics: [TOPIC0, TOPIC1], data: "0xbeef" }, ]), ]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); // logIndex 1 is the SOURCE log. await expect(inbox.deliver(proofFor(scn, 0, 1))) .to.emit(inbox, "MessageDelivered") .withArgs(H.hex(scn.receiptsRoot), 0, 1, SOURCE, [TOPIC0, TOPIC1], "0xbeef"); // logIndex 0 is emitted by OTHER -> wrong source. const { inbox: inbox2 } = await deployStack(scn.bodyTree.root); await expect(inbox2.deliver(proofFor(scn, 0, 0))).to.be.revertedWith("Inbox:wrong source"); }); it("rejects replay (same receipt+log delivered twice)", async function () { const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); await inbox.deliver(proofFor(scn, 0, 0)); await expect(inbox.deliver(proofFor(scn, 0, 0))).to.be.revertedWith("Inbox:already delivered"); }); it("rejects a receiptsRoot not committed under the finalized body (bad exec branch)", async function () { const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); const p = proofFor(scn, 0, 0); const b = p.execBranch.slice(); b[0] = "0x" + "cc".repeat(32); // corrupt the execution-payload branch p.execBranch = b; await expect(inbox.deliver(p)).to.be.revertedWith("Inbox:not finalized"); }); it("rejects a body root the light client never finalized", async function () { const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); const p = proofFor(scn, 0, 0); p.finalizedBodyRoot = "0x" + "77".repeat(32); // never finalized await expect(inbox.deliver(p)).to.be.revertedWith("Inbox:not finalized"); }); it("rejects a forged receipt (tampered receipt value breaks MPT inclusion)", async function () { const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); const p = proofFor(scn, 0, 0); // corrupt the sole proof node so keccak(node) != receiptsRoot. const nodes = p.mptNodes.slice(); const bad = Buffer.from(nodes[0].slice(2), "hex"); bad[bad.length - 1] ^= 0x01; nodes[0] = "0x" + bad.toString("hex"); p.mptNodes = nodes; await expect(inbox.deliver(p)).to.be.reverted; // hash mismatch -> missing node }); it("rejects the wrong txIndex key (proves a different, absent slot)", async function () { const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])]; const scn = buildScenario(receipts); const { inbox } = await deployStack(scn.bodyTree.root); const p = proofFor(scn, 0, 0); p.txIndex = 7; // key rlp(7) is not in the single-entry trie await expect(inbox.deliver(p)).to.be.reverted; }); });