aere-contracts/test/AereOutboundVerifier.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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);
});
});