Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
184 lines
8.1 KiB
JavaScript
184 lines
8.1 KiB
JavaScript
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;
|
|
});
|
|
});
|