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.
254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
// AereStateRootAnchor tests.
|
|
//
|
|
// What is REAL here:
|
|
// - The AERE genesis block header (chain 2800) is real data; encodeShanghaiHeader
|
|
// reconstructs its canonical hash 0xd86d57a8... exactly. The anchor's keccak
|
|
// match + state-root decode is tested against it (genesis is outside the live
|
|
// 256-block BLOCKHASH window, so the mock injects its real canonical hash).
|
|
// - The real BLOCKHASH opcode path is tested end-to-end on the local chain:
|
|
// a freshly mined block is reconstructed, keccak-verified against blockhash,
|
|
// then anchored through the PRODUCTION contract (no mock).
|
|
// - proveAgainstAnchor is tested end-to-end against a faithful stand-in for the
|
|
// live AereStorageProofVerifier's verify/submitProofWithExpectedRoot ABI.
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
const { anyUint } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
|
|
const { encodeHeader } = require("./header-rlp");
|
|
const genesis = require("./fixtures/aere-genesis-header.json");
|
|
|
|
const ZERO32 = "0x" + "00".repeat(32);
|
|
|
|
// abi.encode(chainId, blockNumber, stateRoot, account, slot, value) -> 192 bytes
|
|
function publicValues({ chainId = 2800n, blockNumber, stateRoot, account, slot, value }) {
|
|
return ethers.AbiCoder.defaultAbiCoder().encode(
|
|
["uint256", "uint256", "bytes32", "address", "bytes32", "bytes32"],
|
|
[chainId, blockNumber, stateRoot, account, slot, value]
|
|
);
|
|
}
|
|
|
|
describe("AereStateRootAnchor", function () {
|
|
let verifier, mockAnchor, prodAnchor, user;
|
|
|
|
beforeEach(async function () {
|
|
[user] = await ethers.getSigners();
|
|
|
|
const V = await ethers.getContractFactory("MockStorageProofVerifier");
|
|
verifier = await V.deploy();
|
|
await verifier.waitForDeployment();
|
|
const vAddr = await verifier.getAddress();
|
|
|
|
const M = await ethers.getContractFactory("MockBlockhashAnchor");
|
|
mockAnchor = await M.deploy(vAddr);
|
|
await mockAnchor.waitForDeployment();
|
|
|
|
const P = await ethers.getContractFactory("AereStateRootAnchor");
|
|
prodAnchor = await P.deploy(vAddr);
|
|
await prodAnchor.waitForDeployment();
|
|
});
|
|
|
|
describe("constructor", function () {
|
|
it("stores the verifier and rejects the zero address", async function () {
|
|
expect(await prodAnchor.VERIFIER()).to.equal(await verifier.getAddress());
|
|
expect(await prodAnchor.BLOCKHASH_WINDOW()).to.equal(256n);
|
|
|
|
const P = await ethers.getContractFactory("AereStateRootAnchor");
|
|
await expect(P.deploy(ethers.ZeroAddress)).to.be.revertedWithCustomError(
|
|
prodAnchor,
|
|
"ZeroVerifier"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("encoder / fixture sanity (REAL AERE genesis)", function () {
|
|
it("reconstructs the real canonical AERE genesis block hash", function () {
|
|
const header = encodeHeader(genesis);
|
|
expect(ethers.keccak256(header)).to.equal(genesis.hash);
|
|
});
|
|
|
|
it("previewStateRoot decodes the real genesis state root", async function () {
|
|
const header = encodeHeader(genesis);
|
|
expect(await prodAnchor.previewStateRoot(header)).to.equal(genesis.stateRoot);
|
|
});
|
|
});
|
|
|
|
describe("anchorRecentHeader (against REAL AERE genesis via injected canonical hash)", function () {
|
|
it("anchors the real genesis header and records the real state root", async function () {
|
|
const header = encodeHeader(genesis);
|
|
await mockAnchor.setBlockHash(0, genesis.hash);
|
|
|
|
await expect(mockAnchor.anchorRecentHeader(0, header))
|
|
.to.emit(mockAnchor, "HeaderAnchored")
|
|
.withArgs(0, genesis.hash, genesis.stateRoot, anyUint);
|
|
|
|
expect(await mockAnchor.isAnchored(0)).to.equal(true);
|
|
expect(await mockAnchor.anchoredStateRoot(0)).to.equal(genesis.stateRoot);
|
|
|
|
const rec = await mockAnchor.getAnchoredRoot(0);
|
|
expect(rec.exists).to.equal(true);
|
|
expect(rec.stateRoot).to.equal(genesis.stateRoot);
|
|
expect(rec.blockHash).to.equal(genesis.hash);
|
|
expect(rec.blockNum).to.equal(0n);
|
|
expect(rec.verifiedAt).to.be.gt(0n);
|
|
});
|
|
|
|
it("reverts HeaderHashMismatch when the header is tampered", async function () {
|
|
const header = encodeHeader(genesis);
|
|
await mockAnchor.setBlockHash(0, genesis.hash);
|
|
|
|
// Flip one byte inside the state-root region of the header.
|
|
const bytesArr = ethers.getBytes(header);
|
|
bytesArr[60] ^= 0x01;
|
|
const tampered = ethers.hexlify(bytesArr);
|
|
|
|
await expect(mockAnchor.anchorRecentHeader(0, tampered)).to.be.revertedWithCustomError(
|
|
mockAnchor,
|
|
"HeaderHashMismatch"
|
|
);
|
|
});
|
|
|
|
it("reverts MalformedHeader when a hash-matching input is not a valid header", async function () {
|
|
// A short RLP list whose keccak we inject: passes the hash check, then
|
|
// fails structural decode.
|
|
const bogus = "0xc0"; // empty RLP list
|
|
await mockAnchor.setBlockHash(7, ethers.keccak256(bogus));
|
|
await expect(mockAnchor.anchorRecentHeader(7, bogus)).to.be.revertedWithCustomError(
|
|
mockAnchor,
|
|
"MalformedHeader"
|
|
);
|
|
});
|
|
|
|
it("reverts OutOfWindow when no canonical hash is available", async function () {
|
|
const header = encodeHeader(genesis);
|
|
// No injected hash for block 0 -> _blockHashOf returns 0.
|
|
await expect(mockAnchor.anchorRecentHeader(0, header)).to.be.revertedWithCustomError(
|
|
mockAnchor,
|
|
"OutOfWindow"
|
|
);
|
|
});
|
|
|
|
it("decodes an arbitrary (real) state root value carried in a valid header", async function () {
|
|
// Same real genesis layout but carrying AERE recent-block state root
|
|
// 0x9592f7...; proves the decode reads whatever root the header commits.
|
|
const recentRoot =
|
|
"0x9592f7acd3ec0a4694b5fa23cd9b8b814d49a19c5c9fcf6c47a523611e5b0245";
|
|
const header = encodeHeader({ ...genesis, stateRoot: recentRoot });
|
|
await mockAnchor.setBlockHash(123, ethers.keccak256(header));
|
|
await mockAnchor.anchorRecentHeader(123, header);
|
|
expect(await mockAnchor.anchoredStateRoot(123)).to.equal(recentRoot);
|
|
});
|
|
});
|
|
|
|
describe("real BLOCKHASH opcode path (PRODUCTION contract, no mock)", function () {
|
|
it("anchors a freshly mined local block via the real blockhash() opcode", async function () {
|
|
// Produce a few normal blocks.
|
|
for (let i = 0; i < 3; i++) await ethers.provider.send("evm_mine", []);
|
|
const head = await ethers.provider.getBlockNumber();
|
|
const target = head - 1; // inside the 256 window, not the current block
|
|
|
|
const raw = await ethers.provider.send("eth_getBlockByNumber", [
|
|
"0x" + target.toString(16),
|
|
false,
|
|
]);
|
|
const header = encodeHeader(raw);
|
|
|
|
// Hard gate: our reconstruction must equal the chain's block hash AND the
|
|
// value the contract sees from the real BLOCKHASH opcode.
|
|
expect(ethers.keccak256(header)).to.equal(raw.hash);
|
|
expect(await prodAnchor.canonicalBlockHash(target)).to.equal(raw.hash);
|
|
expect(await prodAnchor.isWithinWindow(target)).to.equal(true);
|
|
|
|
await prodAnchor.anchorRecentHeader(target, header);
|
|
expect(await prodAnchor.anchoredStateRoot(target)).to.equal(raw.stateRoot);
|
|
});
|
|
|
|
it("reverts OutOfWindow for the current/future block (blockhash == 0)", async function () {
|
|
const head = await ethers.provider.getBlockNumber();
|
|
const header = encodeHeader(genesis); // content irrelevant; window check is first
|
|
// A future block number: BLOCKHASH returns 0 for the current and any
|
|
// future block, so the window check reverts before the hash check.
|
|
await expect(
|
|
prodAnchor.anchorRecentHeader(head + 100, header)
|
|
).to.be.revertedWithCustomError(prodAnchor, "OutOfWindow");
|
|
});
|
|
|
|
it("reverts OutOfWindow for a block older than 256 (past the BLOCKHASH window)", async function () {
|
|
await ethers.provider.send("hardhat_mine", ["0x160"]); // mine 352 blocks
|
|
const header = encodeHeader(genesis);
|
|
// Block 1 is now far outside the last-256 window.
|
|
await expect(prodAnchor.anchorRecentHeader(1, header)).to.be.revertedWithCustomError(
|
|
prodAnchor,
|
|
"OutOfWindow"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("proveAgainstAnchor (end-to-end with faithful verifier stand-in)", function () {
|
|
const account = "0x00000000000000000000000000000000000000A1";
|
|
const slot = "0x" + "00".repeat(31) + "05";
|
|
const value = "0x" + "00".repeat(31) + "2a";
|
|
|
|
beforeEach(async function () {
|
|
// Anchor the real genesis root at block 0 on the mock anchor.
|
|
const header = encodeHeader(genesis);
|
|
await mockAnchor.setBlockHash(0, genesis.hash);
|
|
await mockAnchor.anchorRecentHeader(0, header);
|
|
});
|
|
|
|
it("records a proof whose committed root equals the anchored root", async function () {
|
|
const pv = publicValues({
|
|
blockNumber: 0n,
|
|
stateRoot: genesis.stateRoot,
|
|
account,
|
|
slot,
|
|
value,
|
|
});
|
|
await expect(mockAnchor.proveAgainstAnchor(pv, "0x"))
|
|
.to.emit(mockAnchor, "ProvenAgainstAnchor")
|
|
.withArgs(0, genesis.stateRoot, account, slot, value);
|
|
|
|
expect(await verifier.recorded(ethers.keccak256(pv))).to.equal(true);
|
|
});
|
|
|
|
it("reverts RootNotCanonical when the proof's root is not the anchored root", async function () {
|
|
const pv = publicValues({
|
|
blockNumber: 0n,
|
|
stateRoot: ZERO32, // not the anchored genesis root
|
|
account,
|
|
slot,
|
|
value,
|
|
});
|
|
await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
|
|
mockAnchor,
|
|
"RootNotCanonical"
|
|
);
|
|
});
|
|
|
|
it("reverts NotAnchored when the proof's block is not anchored", async function () {
|
|
const pv = publicValues({
|
|
blockNumber: 999n,
|
|
stateRoot: genesis.stateRoot,
|
|
account,
|
|
slot,
|
|
value,
|
|
});
|
|
await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
|
|
mockAnchor,
|
|
"NotAnchored"
|
|
);
|
|
});
|
|
|
|
it("reverts when the underlying SP1 proof is invalid", async function () {
|
|
await verifier.setValidate(false);
|
|
const pv = publicValues({
|
|
blockNumber: 0n,
|
|
stateRoot: genesis.stateRoot,
|
|
account,
|
|
slot,
|
|
value,
|
|
});
|
|
await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.reverted;
|
|
});
|
|
});
|
|
});
|