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.
419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AereOutboundVerifier — trust-minimised OUTBOUND interop (AERE -> dest chain).
|
|
//
|
|
// The zk-interop SP1 guest commits ABI-encoded public values:
|
|
// (uint64 chainId, uint64 blockNumber, bytes32 blockHash, bytes32 root,
|
|
// uint8 claimKind, bytes32 commitment, bool finalized, bool falconVerified)
|
|
// where `commitment` = interop_core::log_commitment(0) over the Outbox log:
|
|
// keccak256( cid(32) ‖ blockHash(32) ‖ addr(32) ‖ topic0 ‖ topic1 ‖ topic2
|
|
// ‖ keccak256(data) )
|
|
//
|
|
// Hardhat has no SP1 Groth16 verifier, so we install a MockSP1Verifier that,
|
|
// like a real verifier, accepts ONLY a registered (vkey, publicValues, proof)
|
|
// triple. The commitment is rebuilt here byte-for-byte the way the Rust guest
|
|
// builds it, so what the verifier checks on-chain is exactly what the guest
|
|
// proves off-chain. (The real guest logic is separately validated against live
|
|
// chain-2800 data in zk-interop/interop-core/tests/real_inclusion.rs.)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const AERE_CHAIN_ID = 2800n;
|
|
const abi = ethers.AbiCoder.defaultAbiCoder();
|
|
|
|
// keccak256 of the event signature string (topic0).
|
|
const EVENT_SIG = ethers.keccak256(
|
|
ethers.toUtf8Bytes(
|
|
"AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)"
|
|
)
|
|
);
|
|
|
|
// 32-byte left-padded word from a uint / address, matching the Rust padding.
|
|
function word(v) {
|
|
return ethers.zeroPadValue(ethers.toBeHex(BigInt(v)), 32);
|
|
}
|
|
function addrWord(a) {
|
|
return ethers.zeroPadValue(ethers.getAddress(a), 32);
|
|
}
|
|
|
|
// Rebuild the guest commitment for log index 0 of the Outbox event.
|
|
function buildCommitment({ blockHash, outbox, messageId, destChainId, dataHash }) {
|
|
return ethers.keccak256(
|
|
ethers.solidityPacked(
|
|
["bytes32", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
|
|
[
|
|
word(AERE_CHAIN_ID), // cid
|
|
blockHash,
|
|
addrWord(outbox),
|
|
EVENT_SIG, // topic0
|
|
messageId, // topic1
|
|
word(destChainId), // topic2
|
|
dataHash,
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
function messageIdOf(destChainId, nonce, sender, targetHandler, payload) {
|
|
return ethers.keccak256(
|
|
abi.encode(
|
|
["uint256", "uint256", "uint256", "address", "address", "bytes"],
|
|
[AERE_CHAIN_ID, destChainId, nonce, sender, targetHandler, payload]
|
|
)
|
|
);
|
|
}
|
|
function dataHashOf(nonce, sender, targetHandler, payload) {
|
|
return ethers.keccak256(
|
|
abi.encode(
|
|
["uint256", "address", "address", "bytes"],
|
|
[nonce, sender, targetHandler, payload]
|
|
)
|
|
);
|
|
}
|
|
|
|
// The existing MockSp1Verifier keys acceptance on a single marker that binds
|
|
// vkey + publicValues + proof, exactly like the real gateway's (vkey, pv, proof).
|
|
function proofMarker(vkey, pv, proof) {
|
|
return ethers.keccak256(
|
|
abi.encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof])
|
|
);
|
|
}
|
|
|
|
// Encode the guest's committed public values.
|
|
function encodePublicValues(pv) {
|
|
return abi.encode(
|
|
["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bool", "bool"],
|
|
[
|
|
pv.chainId,
|
|
pv.blockNumber,
|
|
pv.blockHash,
|
|
pv.root,
|
|
pv.claimKind,
|
|
pv.commitment,
|
|
pv.finalized,
|
|
pv.falconVerified,
|
|
]
|
|
);
|
|
}
|
|
|
|
describe("AereOutboundVerifier", function () {
|
|
const GOOD_VKEY =
|
|
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
|
const WRONG_VKEY =
|
|
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
const PROOF = "0xa1b2c3d4"; // opaque Groth16 proof bytes (mock).
|
|
|
|
let deployer, sender, other;
|
|
let sp1, handler, outbox, verifier;
|
|
let destChainId, blockHash, root;
|
|
let msg; // { nonce, sender, targetHandler, payload }
|
|
let messageId, dataHash, commitment, publicValues;
|
|
|
|
beforeEach(async function () {
|
|
[deployer, sender, other] = await ethers.getSigners();
|
|
|
|
// Destination chain id = the chain the verifier runs on (hardhat = 31337).
|
|
destChainId = BigInt((await ethers.provider.getNetwork()).chainId);
|
|
|
|
sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
|
|
handler = await (
|
|
await ethers.getContractFactory("MockAereMessageHandler")
|
|
).deploy();
|
|
outbox = await (
|
|
await ethers.getContractFactory("AereOutboundOutbox")
|
|
).deploy();
|
|
|
|
verifier = await (
|
|
await ethers.getContractFactory("AereOutboundVerifier")
|
|
).deploy(await sp1.getAddress(), GOOD_VKEY, await outbox.getAddress());
|
|
|
|
// The message fields.
|
|
msg = {
|
|
nonce: 0n,
|
|
sender: sender.address,
|
|
targetHandler: await handler.getAddress(),
|
|
payload: "0xcafe1234",
|
|
};
|
|
|
|
// A QBFT-final block that carries the Outbox log (arbitrary in the mock).
|
|
blockHash =
|
|
"0x6c9dbe1c0000000000000000000000000000000000000000000000000015e900";
|
|
root =
|
|
"0x5ceadcd400000000000000000000000000000000000000000000000000a03500";
|
|
|
|
messageId = messageIdOf(
|
|
destChainId,
|
|
msg.nonce,
|
|
msg.sender,
|
|
msg.targetHandler,
|
|
msg.payload
|
|
);
|
|
dataHash = dataHashOf(
|
|
msg.nonce,
|
|
msg.sender,
|
|
msg.targetHandler,
|
|
msg.payload
|
|
);
|
|
commitment = buildCommitment({
|
|
blockHash,
|
|
outbox: await outbox.getAddress(),
|
|
messageId,
|
|
destChainId,
|
|
dataHash,
|
|
});
|
|
|
|
publicValues = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
|
|
// Register the known-good proof triple with the mock verifier.
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, publicValues, PROOF), true);
|
|
});
|
|
|
|
function callDeliver(v = verifier, pv = publicValues, proof = PROOF, m = msg) {
|
|
return v.deliver(
|
|
pv,
|
|
proof,
|
|
destChainId,
|
|
m.nonce,
|
|
m.sender,
|
|
m.targetHandler,
|
|
m.payload
|
|
);
|
|
}
|
|
|
|
it("Outbox.send emits the canonical event and matching messageId", async function () {
|
|
const tx = await outbox
|
|
.connect(sender)
|
|
.send(destChainId, msg.targetHandler, msg.payload);
|
|
await expect(tx)
|
|
.to.emit(outbox, "AereCrossChainMessage")
|
|
.withArgs(
|
|
messageId,
|
|
destChainId,
|
|
0n,
|
|
sender.address,
|
|
msg.targetHandler,
|
|
msg.payload
|
|
);
|
|
expect(await outbox.nonce()).to.equal(1n);
|
|
});
|
|
|
|
it("delivers a valid proof with an unused nonce (exactly once)", async function () {
|
|
await expect(callDeliver())
|
|
.to.emit(verifier, "MessageDelivered")
|
|
.withArgs(messageId, sender.address, msg.targetHandler, 9261789n);
|
|
|
|
expect(await verifier.delivered(messageId)).to.equal(true);
|
|
expect(await handler.callCount()).to.equal(1n);
|
|
expect(await handler.lastSrcChainId()).to.equal(AERE_CHAIN_ID);
|
|
expect(await handler.lastSender()).to.equal(sender.address);
|
|
expect(await handler.lastPayload()).to.equal(msg.payload);
|
|
});
|
|
|
|
it("rejects a replay of the same message", async function () {
|
|
await callDeliver();
|
|
await expect(callDeliver())
|
|
.to.be.revertedWithCustomError(verifier, "AlreadyDelivered")
|
|
.withArgs(messageId);
|
|
expect(await handler.callCount()).to.equal(1n); // no second delivery
|
|
});
|
|
|
|
it("rejects a proof verified against the wrong pinned vkey", async function () {
|
|
const wrongVerifier = await (
|
|
await ethers.getContractFactory("AereOutboundVerifier")
|
|
).deploy(await sp1.getAddress(), WRONG_VKEY, await outbox.getAddress());
|
|
// The mock only accepted (GOOD_VKEY, pv, proof); WRONG_VKEY is unregistered.
|
|
await expect(callDeliver(wrongVerifier)).to.be.revertedWithCustomError(
|
|
sp1,
|
|
"MockInvalidProof"
|
|
);
|
|
});
|
|
|
|
it("rejects tampered public inputs (SP1 layer catches it)", async function () {
|
|
// Flip blockNumber — not part of the commitment, so only the SP1 proof
|
|
// binding can catch it. The mock has no registered triple for this pv.
|
|
const tampered = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9999999n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
await expect(callDeliver(verifier, tampered)).to.be.revertedWithCustomError(
|
|
sp1,
|
|
"MockInvalidProof"
|
|
);
|
|
});
|
|
|
|
it("rejects delivered fields that do not match the proven commitment", async function () {
|
|
// Correct proof/publicValues, but deliver a DIFFERENT payload: the
|
|
// re-derived commitment no longer matches the proven one.
|
|
const tamperedMsg = { ...msg, payload: "0xdeadbeef" };
|
|
await expect(
|
|
callDeliver(verifier, publicValues, PROOF, tamperedMsg)
|
|
).to.be.revertedWithCustomError(verifier, "CommitmentMismatch");
|
|
});
|
|
|
|
it("rejects a proof for the wrong source chain", async function () {
|
|
// chainId != 2800. Rebuild a self-consistent, registered proof so the SP1
|
|
// layer passes and the chain-id gate is what rejects it.
|
|
const badChainId = 9999n;
|
|
const pv = encodePublicValues({
|
|
chainId: badChainId,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(callDeliver(verifier, pv))
|
|
.to.be.revertedWithCustomError(verifier, "WrongSourceChain")
|
|
.withArgs(badChainId);
|
|
});
|
|
|
|
it("rejects a non-final source block", async function () {
|
|
const pv = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment,
|
|
finalized: false,
|
|
falconVerified: false,
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
|
|
verifier,
|
|
"NotFinal"
|
|
);
|
|
});
|
|
|
|
it("rejects a non-receipt claim kind", async function () {
|
|
const pv = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 0, // storage claim, not a message log
|
|
commitment,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(callDeliver(verifier, pv))
|
|
.to.be.revertedWithCustomError(verifier, "NotReceiptClaim")
|
|
.withArgs(0);
|
|
});
|
|
|
|
it("rejects an unbacked post-quantum (falconVerified) claim", async function () {
|
|
const pv = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment,
|
|
finalized: true,
|
|
falconVerified: true, // mainnet consensus is ECDSA; must be false
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
|
|
verifier,
|
|
"UnexpectedFalconFlag"
|
|
);
|
|
});
|
|
|
|
it("rejects a message addressed to the wrong destination chain", async function () {
|
|
const badDest = destChainId + 1n;
|
|
const mid = messageIdOf(
|
|
badDest,
|
|
msg.nonce,
|
|
msg.sender,
|
|
msg.targetHandler,
|
|
msg.payload
|
|
);
|
|
const dh = dataHashOf(msg.nonce, msg.sender, msg.targetHandler, msg.payload);
|
|
const cmt = buildCommitment({
|
|
blockHash,
|
|
outbox: await outbox.getAddress(),
|
|
messageId: mid,
|
|
destChainId: badDest,
|
|
dataHash: dh,
|
|
});
|
|
const pv = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment: cmt,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(
|
|
verifier.deliver(
|
|
pv,
|
|
PROOF,
|
|
badDest,
|
|
msg.nonce,
|
|
msg.sender,
|
|
msg.targetHandler,
|
|
msg.payload
|
|
)
|
|
)
|
|
.to.be.revertedWithCustomError(verifier, "WrongDestinationChain")
|
|
.withArgs(badDest);
|
|
});
|
|
|
|
it("rejects a log claimed from an untrusted (wrong) outbox address", async function () {
|
|
// Build a commitment as if a DIFFERENT contract emitted the log. The
|
|
// verifier is pinned to the real outbox, so re-derivation mismatches.
|
|
const cmt = buildCommitment({
|
|
blockHash,
|
|
outbox: other.address, // not the pinned outbox
|
|
messageId,
|
|
destChainId,
|
|
dataHash,
|
|
});
|
|
const pv = encodePublicValues({
|
|
chainId: AERE_CHAIN_ID,
|
|
blockNumber: 9261789n,
|
|
blockHash,
|
|
root,
|
|
claimKind: 1,
|
|
commitment: cmt,
|
|
finalized: true,
|
|
falconVerified: false,
|
|
});
|
|
await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
|
|
await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
|
|
verifier,
|
|
"CommitmentMismatch"
|
|
);
|
|
});
|
|
|
|
it("propagates a handler revert (message not marked delivered)", async function () {
|
|
await handler.setShouldRevert(true);
|
|
await expect(callDeliver()).to.be.revertedWith("handler: forced revert");
|
|
// State-changing revert rolls back the delivered flag too.
|
|
expect(await verifier.delivered(messageId)).to.equal(false);
|
|
});
|
|
});
|