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.
322 lines
14 KiB
JavaScript
322 lines
14 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
/**
|
|
* AereOutboundVerifierV2 — anchor-bind fix, with the V1 forgery as a PAIRED CONTROL.
|
|
*
|
|
* WHAT THIS FILE PROVES, AND WHAT IT DOES NOT
|
|
* ───────────────────────────────────────────
|
|
* The forgery has two independent legs:
|
|
*
|
|
* LEG A (the zk layer) — "a valid SP1 proof of the forged statement exists".
|
|
* PROVEN LIVE, NOT HERE. Real Groth16 proofs of the forged header (7
|
|
* attacker-generated keys named as the validator set, self-sealed 5-of-7,
|
|
* attacker-built receiptsRoot) were generated on a dedicated prover and
|
|
* submitted by eth_call to the REAL, LIVE SP1VerifierGateway 0x9ca479C8…70628:
|
|
* * under the V1-design vkey 0x00123374…66f5 (256-byte, 8-word publicValues,
|
|
* no validatorSetRoot) -> ACCEPTED;
|
|
* * under the vkey the live V2 actually pins, 0x00a71f7d…08d0 -> ALSO ACCEPTED.
|
|
* Tampering either the proof or the publicValues -> REJECTED, so the gateway is
|
|
* genuinely verifying rather than rubber-stamping. The forged statement is
|
|
* cryptographically VALID; only the anchor bind refuses it.
|
|
*
|
|
* LEG B (the contract layer) — "given that proof, the contract's gates accept it".
|
|
* PROVEN HERE, with a mock verifier standing in for the (already-proven) zk leg.
|
|
*
|
|
* A and B together => V1's design would have delivered a forged bridge message.
|
|
* The mock verifier is NOT a shortcut around the cryptography: leg A establishes the
|
|
* real verifier's verdict on the real proof, and this file isolates the gate logic
|
|
* that runs after it. We deliberately did NOT deploy a live forgeable verifier.
|
|
*
|
|
* HONEST SCOPE OF THE BYTES BELOW: these publicValues are built in the SAME layout
|
|
* and shape as the real committed values, but re-derived for the LOCAL test chain id
|
|
* (V1/V2 both require destChainId == block.chainid, and this chain is not 2800), and
|
|
* with a placeholder blockHash/root. They exercise the GATE LOGIC, not the crypto.
|
|
* The genuinely real committed values (chainId 2800, forged blockHash
|
|
* 0xf210da56…b42b, attacker root 0x4f2c0cf7…23ee) were exercised live: the live V2
|
|
* reverts ValidatorSetMismatch(0x4f2c0cf7…23ee, 0x5fa93ba7…dd79) on them. See
|
|
* zk-light-client/OUTBOUND-VERIFIER-FIX-2026-07-16.md for every tx hash.
|
|
*/
|
|
|
|
// ── Real artifacts from the 2026-07-16/17 prover run (see the report) ──
|
|
const ANCHOR = "0x5fa93ba7730cdb599fd814c0497565f320f10d808b6ed485c3e02fb1a1e2dd79";
|
|
const OUTBOX = "0xd43FeacbbdDc5ff7cE4A72C726f3FBD204ef7936";
|
|
const MIN_SOURCE_BLOCK = 9312565;
|
|
const VKEY_V2 = "0x00a71f7d74d3a503326a93c88b0f6a6d3bfe4609e79959cc5cad56a0d5f208d0";
|
|
const VKEY_V1_CONTROL = "0x0012337429546d116941b8ac9532008a66d438b0de859002d26d6b205d4766f5"; // REAL vkey of the rebuilt V1-design guest (the mock ignores it; the live gateway accepted this vkey + the forged proof)
|
|
|
|
const AERE_CHAIN_ID = 2800n;
|
|
const EVENT_SIG = ethers.id("AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)");
|
|
|
|
const abi = ethers.AbiCoder.defaultAbiCoder();
|
|
|
|
function messageIdOf(destChainId, nonce, sender, handler, payload) {
|
|
return ethers.keccak256(
|
|
abi.encode(
|
|
["uint256", "uint256", "uint256", "address", "address", "bytes"],
|
|
[AERE_CHAIN_ID, destChainId, nonce, sender, handler, payload]
|
|
)
|
|
);
|
|
}
|
|
|
|
function commitmentOf(blockHash, outbox, messageId, destChainId, nonce, sender, handler, payload) {
|
|
const dataHash = ethers.keccak256(
|
|
abi.encode(["uint256", "address", "address", "bytes"], [nonce, sender, handler, payload])
|
|
);
|
|
return ethers.keccak256(
|
|
ethers.concat([
|
|
ethers.zeroPadValue(ethers.toBeHex(AERE_CHAIN_ID), 32),
|
|
blockHash,
|
|
ethers.zeroPadValue(outbox, 32),
|
|
EVENT_SIG,
|
|
messageId,
|
|
ethers.zeroPadValue(ethers.toBeHex(destChainId), 32),
|
|
dataHash,
|
|
])
|
|
);
|
|
}
|
|
|
|
// V2 guest: 9 committed words, chainId from a constant, validatorSetRoot present.
|
|
function encodeV2(chainId, blockNumber, blockHash, root, claimKind, commitment, vsr, finalized, falcon) {
|
|
return abi.encode(
|
|
["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bytes32", "bool", "bool"],
|
|
[chainId, blockNumber, blockHash, root, claimKind, commitment, vsr, finalized, falcon]
|
|
);
|
|
}
|
|
|
|
// V1 guest: 8 committed words, chainId ECHOED from prover input, NO validatorSetRoot.
|
|
function encodeV1(chainId, blockNumber, blockHash, root, claimKind, commitment, finalized, falcon) {
|
|
return abi.encode(
|
|
["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bool", "bool"],
|
|
[chainId, blockNumber, blockHash, root, claimKind, commitment, finalized, falcon]
|
|
);
|
|
}
|
|
|
|
describe("AereOutboundVerifierV2 — validator-set anchor bind (the V1 forgery fix)", () => {
|
|
let mock, recorder, v2, v1, deployer;
|
|
const FORGED_BLOCK_HASH = "0x" + "ab".repeat(32);
|
|
const FORGED_ROOT = "0x" + "cd".repeat(32);
|
|
// keccak over an attacker's self-nominated set — any value that is not the anchor.
|
|
const ATTACKER_VSR = ethers.keccak256("0x" + "de".repeat(140));
|
|
|
|
before(async () => {
|
|
[deployer] = await ethers.getSigners();
|
|
|
|
const Mock = await ethers.getContractFactory("MockSP1VerifierAlwaysAccepts");
|
|
mock = await Mock.deploy();
|
|
await mock.waitForDeployment();
|
|
|
|
const Rec = await ethers.getContractFactory("AereOutboundDeliveryRecorder");
|
|
recorder = await Rec.deploy();
|
|
await recorder.waitForDeployment();
|
|
|
|
const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
|
|
v2 = await V2.deploy(await mock.getAddress(), VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX);
|
|
await v2.waitForDeployment();
|
|
|
|
// The ORIGINAL V1 contract, wired to the same mock — the control.
|
|
const V1 = await ethers.getContractFactory("AereOutboundVerifier");
|
|
v1 = await V1.deploy(await mock.getAddress(), VKEY_V1_CONTROL, OUTBOX);
|
|
await v1.waitForDeployment();
|
|
});
|
|
|
|
it("CONTROL: the V1 design ACCEPTS the forgery and delivers a message nobody authorised", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const nonce = 999999n;
|
|
const sender = ethers.getAddress("0x00000000000000000000000000000000deadbeef");
|
|
const payload = ethers.hexlify(ethers.toUtf8Bytes("FORGED"));
|
|
|
|
const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
|
|
const commitment = commitmentOf(
|
|
FORGED_BLOCK_HASH,
|
|
OUTBOX,
|
|
messageId,
|
|
destChainId,
|
|
nonce,
|
|
sender,
|
|
handler,
|
|
payload
|
|
);
|
|
|
|
// The attacker simply declares chainId = 2800 — it was an echoed prover input.
|
|
const pv = encodeV1(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, true, false);
|
|
|
|
const before = await recorder.deliveryCount();
|
|
await v1.deliver(pv, "0x", destChainId, nonce, sender, handler, payload);
|
|
const after = await recorder.deliveryCount();
|
|
|
|
// THE BUG, REPRODUCED: a message the real Outbox never emitted was delivered.
|
|
expect(after - before).to.equal(1n);
|
|
expect(await v1.delivered(messageId)).to.equal(true);
|
|
const d = await recorder.deliveries(Number(after) - 1);
|
|
expect(d.sender).to.equal(sender);
|
|
expect(ethers.toUtf8String(await recorder.payloadAt(Number(after) - 1))).to.equal("FORGED");
|
|
});
|
|
|
|
it("FIX: V2 REJECTS the identical forgery — committed root != anchored root", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const nonce = 999999n;
|
|
const sender = ethers.getAddress("0x00000000000000000000000000000000deadbeef");
|
|
const payload = ethers.hexlify(ethers.toUtf8Bytes("FORGED"));
|
|
|
|
const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
|
|
const commitment = commitmentOf(
|
|
FORGED_BLOCK_HASH,
|
|
OUTBOX,
|
|
messageId,
|
|
destChainId,
|
|
nonce,
|
|
sender,
|
|
handler,
|
|
payload
|
|
);
|
|
|
|
// Same forgery. The V2 guest is FORCED to commit which set it used.
|
|
const pv = encodeV2(
|
|
2800,
|
|
9999999,
|
|
FORGED_BLOCK_HASH,
|
|
FORGED_ROOT,
|
|
1,
|
|
commitment,
|
|
ATTACKER_VSR, // <- the attacker's own set
|
|
true,
|
|
false
|
|
);
|
|
|
|
const before = await recorder.deliveryCount();
|
|
await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload))
|
|
.to.be.revertedWithCustomError(v2, "ValidatorSetMismatch")
|
|
.withArgs(ATTACKER_VSR, ANCHOR);
|
|
expect(await recorder.deliveryCount()).to.equal(before); // nothing delivered
|
|
expect(await v2.delivered(messageId)).to.equal(false);
|
|
});
|
|
|
|
it("FIX: V2 ACCEPTS a well-formed message that attests the ANCHORED set", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const nonce = 7n;
|
|
const sender = deployer.address;
|
|
const payload = ethers.hexlify(ethers.toUtf8Bytes("legit"));
|
|
|
|
const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
|
|
const commitment = commitmentOf(
|
|
FORGED_BLOCK_HASH,
|
|
OUTBOX,
|
|
messageId,
|
|
destChainId,
|
|
nonce,
|
|
sender,
|
|
handler,
|
|
payload
|
|
);
|
|
const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);
|
|
|
|
await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload)).to.emit(v2, "MessageDelivered");
|
|
expect(await v2.delivered(messageId)).to.equal(true);
|
|
});
|
|
|
|
it("replay of the same delivered message reverts", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const nonce = 7n;
|
|
const sender = deployer.address;
|
|
const payload = ethers.hexlify(ethers.toUtf8Bytes("legit"));
|
|
const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
|
|
const commitment = commitmentOf(
|
|
FORGED_BLOCK_HASH,
|
|
OUTBOX,
|
|
messageId,
|
|
destChainId,
|
|
nonce,
|
|
sender,
|
|
handler,
|
|
payload
|
|
);
|
|
const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);
|
|
|
|
await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload))
|
|
.to.be.revertedWithCustomError(v2, "AlreadyDelivered")
|
|
.withArgs(messageId);
|
|
});
|
|
|
|
it("rejects a wrong-length publicValues blob instead of silently abi.decoding it", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const short = encodeV1(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, true, false);
|
|
expect(ethers.dataLength(short)).to.equal(256); // the V1 8-word layout
|
|
await expect(v2.deliver(short, "0x", destChainId, 1n, deployer.address, handler, "0x"))
|
|
.to.be.revertedWithCustomError(v2, "BadPublicValuesLength")
|
|
.withArgs(256);
|
|
});
|
|
|
|
it("rejects a source block below the anchored set's validity window", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const pv = encodeV2(2800, 1000, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, ANCHOR, true, false);
|
|
await expect(v2.deliver(pv, "0x", destChainId, 1n, deployer.address, handler, "0x"))
|
|
.to.be.revertedWithCustomError(v2, "SourceBlockTooOld")
|
|
.withArgs(1000, MIN_SOURCE_BLOCK);
|
|
});
|
|
|
|
it("rejects an unbacked post-quantum claim (falconVerified = true)", async () => {
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, ANCHOR, true, true);
|
|
await expect(v2.deliver(pv, "0x", destChainId, 1n, deployer.address, handler, "0x")).to.be.revertedWithCustomError(
|
|
v2,
|
|
"UnexpectedFalconFlag"
|
|
);
|
|
});
|
|
|
|
it("constructor refuses a zero vkey (V1's fail-closed trap is not repeatable)", async () => {
|
|
const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
|
|
await expect(
|
|
V2.deploy(await mock.getAddress(), ethers.ZeroHash, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX)
|
|
).to.be.revertedWithCustomError(V2, "ZeroValue");
|
|
await expect(
|
|
V2.deploy(await mock.getAddress(), VKEY_V2, ethers.ZeroHash, MIN_SOURCE_BLOCK, OUTBOX)
|
|
).to.be.revertedWithCustomError(V2, "ZeroValue");
|
|
});
|
|
|
|
it("constructor refuses a code-less verifier", async () => {
|
|
const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
|
|
await expect(
|
|
V2.deploy(deployer.address, VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX)
|
|
).to.be.revertedWithCustomError(V2, "VerifierHasNoCode");
|
|
});
|
|
|
|
it("rejects a proof the SP1 verifier does not accept", async () => {
|
|
const Rejecting = await ethers.getContractFactory("MockSP1VerifierAlwaysReverts");
|
|
const rej = await Rejecting.deploy();
|
|
await rej.waitForDeployment();
|
|
const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
|
|
const v2r = await V2.deploy(await rej.getAddress(), VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX);
|
|
await v2r.waitForDeployment();
|
|
|
|
const handler = await recorder.getAddress();
|
|
const destChainId = (await ethers.provider.getNetwork()).chainId;
|
|
const nonce = 42n;
|
|
const sender = deployer.address;
|
|
const payload = "0x";
|
|
const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
|
|
const commitment = commitmentOf(
|
|
FORGED_BLOCK_HASH,
|
|
OUTBOX,
|
|
messageId,
|
|
destChainId,
|
|
nonce,
|
|
sender,
|
|
handler,
|
|
payload
|
|
);
|
|
const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);
|
|
await expect(v2r.deliver(pv, "0x", destChainId, nonce, sender, handler, payload)).to.be.revertedWithCustomError(
|
|
v2r,
|
|
"InvalidProof"
|
|
);
|
|
});
|
|
});
|