const { expect } = require("chai"); const { ethers, network } = require("hardhat"); // --------------------------------------------------------------------------- // AereAgentActionReceipt — PQC-anchored, publicly verifiable AI provenance. // // An agent commits the HASH of an output it produced, signed by a short-lived // secp256k1 SESSION key issued under a quantum-durable Falcon ROOT via AereAgentDID // (Falcon PoP verified on-chain by the live native precompile — here the mock at // 0x0AE1..0x0AE4, exactly as in AereAgentDID.test.js). The receipt is append-only, // re-verifiable by anyone in one read, and cross-wires to the agent's AereAgentBond // stake + AereAIReputation score at the canonical key (operator = DID controller, // agentId = bytes32(rootKeyId)). // --------------------------------------------------------------------------- const PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), 2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), 4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), }; const META = { 1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true }, 2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true }, 3: { pkLen: 1312, sigLen: 2420, falcon: false }, 4: { pkLen: 32, sigLen: 7856, falcon: false }, }; function makeRng(seedText) { let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); function bytes(n) { const out = new Uint8Array(n); let i = 0; while (i < n) { seed = ethers.keccak256(seed); const chunk = ethers.getBytes(seed); for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j]; } return out; } function int(n) { const b = bytes(4); return (((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0) % n; } return { bytes, int }; } function makePubKey(scheme, rng) { const m = META[scheme]; const raw = rng.bytes(m.pkLen); if (m.falcon) raw[0] = m.pkHeader; return ethers.hexlify(raw); } function commitmentFor(pubKey, message) { return ethers.keccak256(ethers.concat([pubKey, message])); } function envelope(scheme, commitment, rng) { const m = META[scheme]; if (m.falcon) { const nonce = rng.bytes(40); const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); return ethers.hexlify(ethers.concat([nonce, esig])); } else { const sig = new Uint8Array(m.sigLen); sig.set(ethers.getBytes(commitment), 0); return ethers.hexlify(sig); } } function genuine(scheme, pubKey, message, rng) { return envelope(scheme, commitmentFor(pubKey, message), rng); } // secp256k1 signature (r||s||v, low-s, v in {27,28}) by `wallet` over the 32-byte digest. function ecdsaSign(wallet, digest) { return wallet.signingKey.sign(digest).serialized; } const ONE_AERE = ethers.parseUnits("1", 18); const TEN_AERE = ethers.parseUnits("10", 18); describe("AereAgentActionReceipt (PQC-anchored AI provenance)", function () { let owner, attestor, relayer, other; let reg, did, bond, rep, aere, sink, receipt; let mockCode; before(async function () { [owner, attestor, relayer, other] = await ethers.getSigners(); mockCode = {}; const F = await ethers.getContractFactory("MockPQCPrecompile"); for (const scheme of [1, 2, 3, 4]) { const mock = await F.deploy(scheme); await mock.waitForDeployment(); mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress()); } }); async function installMocks() { for (const scheme of [1, 2, 3, 4]) { await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]); } } beforeEach(async function () { await installMocks(); // PQC key registry + Falcon-rooted DID (real Falcon-PoP session issuance). const R = await ethers.getContractFactory("AerePQCKeyRegistry"); reg = await R.deploy(); await reg.waitForDeployment(); // Economic layer: AERE token + sink + slashable bond + composable reputation. const T = await ethers.getContractFactory("MockERC20Lending"); aere = await T.deploy("AERE", "AERE", 18); await aere.waitForDeployment(); const S = await ethers.getContractFactory("MockSinkSimple"); sink = await S.deploy(); await sink.waitForDeployment(); const B = await ethers.getContractFactory("AereAgentBond"); bond = await B.deploy(await aere.getAddress(), await sink.getAddress(), [owner.address]); // owner = slashing oracle await bond.waitForDeployment(); const Rep = await ethers.getContractFactory("AereAIReputation"); rep = await Rep.deploy(await bond.getAddress(), [attestor.address]); await rep.waitForDeployment(); const D = await ethers.getContractFactory("AereAgentDID"); did = await D.deploy(await reg.getAddress(), await bond.getAddress(), await rep.getAddress()); await did.waitForDeployment(); const AR = await ethers.getContractFactory("AereAgentActionReceipt"); receipt = await AR.deploy(await did.getAddress(), await bond.getAddress(), await rep.getAddress()); await receipt.waitForDeployment(); // Fund owner (operator) so it can post a bond under the canonical key. await aere.mint(owner.address, ethers.parseUnits("100", 18)); await aere.connect(owner).approve(await bond.getAddress(), ethers.MaxUint256); }); // Register a Falcon root key under `signer`, return { rootKeyId, rootPk, scheme }. async function registerFalconRoot(scheme, signer = owner, rng = makeRng("root-" + scheme)) { const pk = makePubKey(scheme, rng); const nonce = await reg.identityNonce(signer.address); const ch = await reg.popChallenge(signer.address, scheme, pk, nonce); const tx = await reg.connect(signer).registerKey(scheme, pk, genuine(scheme, pk, ch, rng)); const rc = await tx.wait(); const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered"); return { rootKeyId: Number(log.args.keyId), rootPk: pk, scheme }; } // Issue a session with a genuine Falcon PoP (relayed by anyone). async function issueSession(root, { sessionAddr, scopeHash, spendCap, expiry, rng, caller = relayer }) { const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce; const challenge = await did.sessionChallenge(root.rootKeyId, nonce, sessionAddr, scopeHash, spendCap, expiry); const pop = genuine(root.scheme, root.rootPk, challenge, rng); const tx = await did.connect(caller).issueSession(root.rootKeyId, sessionAddr, scopeHash, spendCap, expiry, pop); const rc = await tx.wait(); const log = rc.logs.map((l) => did.interface.parseLog(l)).find((p) => p && p.name === "SessionIssued"); return Number(log.args.sessionId); } async function futureTs(secs = 3600) { const b = await ethers.provider.getBlock("latest"); return b.timestamp + secs; } // Full setup: Falcon root + DID agent + one session, ready to emit receipts. async function setupAgentSession(seed, scopeLabel = "infer:v1") { const rng = makeRng(seed); const root = await registerFalconRoot(1, owner, rng); await did.createAgent(root.rootKeyId); const session = ethers.Wallet.createRandom(); const scopeHash = ethers.keccak256(ethers.toUtf8Bytes(scopeLabel)); const sid = await issueSession(root, { sessionAddr: session.address, scopeHash, spendCap: 1000n, expiry: await futureTs(5000), rng, }); return { root, session, scopeHash, sid, rng }; } // Sign + emit a receipt for `outputHash` under session `sid` with `scope`. // `signWallet` defaults to the session key; `signHash` lets us sign a DIFFERENT // hash than the one submitted (to force a tampered-output rejection). async function emitReceipt(sid, scope, outputHash, session, opts = {}) { const nonce = await receipt.receiptNonceOf(sid); const agentId = (await did.getSession(sid)).rootKeyId; const signHash = opts.signHash ?? outputHash; const digest = await receipt.receiptDigest(agentId, sid, signHash, scope, nonce); const wallet = opts.wallet ?? session; const sig = ecdsaSign(wallet, digest); return receipt.connect(opts.caller ?? relayer).emitReceipt(sid, scope, outputHash, sig); } // ----------------------------------------------------------------------- // Deployment & wiring // ----------------------------------------------------------------------- describe("deployment", function () { it("wires the DID + read-only economic cross-links and the domain separator", async function () { expect(await receipt.did()).to.equal(await did.getAddress()); expect(await receipt.agentBond()).to.equal(await bond.getAddress()); expect(await receipt.aiReputation()).to.equal(await rep.getAddress()); expect(await receipt.RECEIPT_DOMAIN()).to.equal( ethers.keccak256(ethers.toUtf8Bytes("AereAgentActionReceipt.v1.receipt")) ); expect(await receipt.receiptCount()).to.equal(0n); }); it("reverts on a zero DID", async function () { const AR = await ethers.getContractFactory("AereAgentActionReceipt"); await expect( AR.deploy(ethers.ZeroAddress, await bond.getAddress(), await rep.getAddress()) ).to.be.revertedWithCustomError(AR, "ZeroDID"); }); it("bondKeyOf(rootKeyId) is the canonical bytes32(rootKeyId) cross-wire key", async function () { expect(await receipt.bondKeyOf(7)).to.equal(ethers.zeroPadValue("0x07", 32)); }); }); // ----------------------------------------------------------------------- // emit + verify (happy path) // ----------------------------------------------------------------------- describe("emit + verify", function () { it("records a provenance receipt signed by the session key and re-verifies it", async function () { const { session, scopeHash, sid, root } = await setupAgentSession("emit-happy"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("model produced: hello world")); await expect(emitReceipt(sid, scopeHash, outputHash, session)) .to.emit(receipt, "ReceiptEmitted") .withArgs(0, root.rootKeyId, sid, session.address, outputHash, scopeHash, 0); expect(await receipt.receiptCount()).to.equal(1n); // Stored record. const r = await receipt.getReceipt(0); expect(r.agentId).to.equal(BigInt(root.rootKeyId)); expect(r.sessionId).to.equal(BigInt(sid)); expect(r.signer).to.equal(session.address); expect(r.outputHash).to.equal(outputHash); expect(r.scope).to.equal(scopeHash); expect(r.nonce).to.equal(0n); // One-read independent verification: signature genuine + session live now. const v = await receipt.verifyReceipt(0); expect(v.sigValid).to.equal(true); expect(v.sessionLiveNow).to.equal(true); expect(v.agentId).to.equal(BigInt(root.rootKeyId)); expect(v.signer).to.equal(session.address); expect(v.outputHash).to.equal(outputHash); // verifyReceiptOutput matches the true output, rejects any other. expect(await receipt.verifyReceiptOutput(0, outputHash)).to.equal(true); expect(await receipt.verifyReceiptOutput(0, ethers.keccak256(ethers.toUtf8Bytes("different")))).to.equal(false); // Indexing by agent + session. expect((await receipt.receiptsOf(root.rootKeyId)).map(Number)).to.deep.equal([0]); expect((await receipt.receiptsOfSession(sid)).map(Number)).to.deep.equal([0]); }); it("per-session nonce advances so many distinct receipts can be recorded", async function () { const { session, scopeHash, sid } = await setupAgentSession("emit-multi"); expect(await receipt.receiptNonceOf(sid)).to.equal(0n); const h1 = ethers.keccak256(ethers.toUtf8Bytes("out-1")); const h2 = ethers.keccak256(ethers.toUtf8Bytes("out-2")); await emitReceipt(sid, scopeHash, h1, session); await emitReceipt(sid, scopeHash, h2, session); expect(await receipt.receiptNonceOf(sid)).to.equal(2n); expect(await receipt.receiptCount()).to.equal(2n); expect((await receipt.getReceipt(0)).nonce).to.equal(0n); expect((await receipt.getReceipt(1)).nonce).to.equal(1n); expect(await receipt.verifyReceiptOutput(0, h1)).to.equal(true); expect(await receipt.verifyReceiptOutput(1, h2)).to.equal(true); }); it("emitting is permissionless — the session signature is the sole authority", async function () { const { session, scopeHash, sid } = await setupAgentSession("emit-relay"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("relayed output")); // `other` relays and pays gas; authorship is still the session key. await emitReceipt(sid, scopeHash, outputHash, session, { caller: other }); expect((await receipt.getReceipt(0)).signer).to.equal(session.address); }); }); // ----------------------------------------------------------------------- // tampered outputHash rejected // ----------------------------------------------------------------------- describe("tampered outputHash rejected", function () { it("rejects a receipt whose submitted outputHash differs from the signed one", async function () { const { session, scopeHash, sid } = await setupAgentSession("tamper"); const realOutput = ethers.keccak256(ethers.toUtf8Bytes("genuine output")); const tampered = ethers.keccak256(ethers.toUtf8Bytes("tampered output")); // Session signs the REAL output hash, but a tampered hash is submitted. await expect( emitReceipt(sid, scopeHash, tampered, session, { signHash: realOutput }) ).to.be.revertedWithCustomError(receipt, "BadSessionSignature"); expect(await receipt.receiptCount()).to.equal(0n); }); it("rejects a signature by a non-session key", async function () { const { session, scopeHash, sid } = await setupAgentSession("tamper-signer"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("out")); const stranger = ethers.Wallet.createRandom(); await expect( emitReceipt(sid, scopeHash, outputHash, session, { wallet: stranger }) ).to.be.revertedWithCustomError(receipt, "BadSessionSignature"); }); it("rejects a malformed (wrong-length) signature", async function () { const { scopeHash, sid } = await setupAgentSession("tamper-malformed"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("out")); await expect( receipt.connect(relayer).emitReceipt(sid, scopeHash, outputHash, "0x1234") ).to.be.revertedWithCustomError(receipt, "BadSessionSignature"); }); it("rejects an empty (zero) output hash", async function () { const { session, scopeHash, sid } = await setupAgentSession("tamper-empty"); await expect( emitReceipt(sid, scopeHash, ethers.ZeroHash, session) ).to.be.revertedWithCustomError(receipt, "EmptyOutputHash"); }); it("rejects a scope that does not match the session scope", async function () { const { session, scopeHash, sid } = await setupAgentSession("tamper-scope"); const wrongScope = ethers.keccak256(ethers.toUtf8Bytes("other:scope")); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("out")); await expect( emitReceipt(sid, wrongScope, outputHash, session) ).to.be.revertedWithCustomError(receipt, "ScopeMismatch"); }); }); // ----------------------------------------------------------------------- // revoked / invalid agent rejected // ----------------------------------------------------------------------- describe("revoked / invalid session rejected", function () { it("rejects an emit under a session the controller has revoked", async function () { const { session, scopeHash, sid } = await setupAgentSession("revoke-session"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("out")); // A genuine receipt works before revocation. await emitReceipt(sid, scopeHash, outputHash, session); // Controller fast-revokes the session. await did.connect(owner).revokeSession(sid); await expect( emitReceipt(sid, scopeHash, ethers.keccak256(ethers.toUtf8Bytes("out2")), session) ).to.be.revertedWithCustomError(receipt, "RevokedOrInvalidSession"); }); it("rejects an emit after the Falcon ROOT is revoked (all its sessions die)", async function () { const { session, scopeHash, sid, root } = await setupAgentSession("revoke-root"); await reg.connect(owner).revokeKey(root.rootKeyId); await expect( emitReceipt(sid, scopeHash, ethers.keccak256(ethers.toUtf8Bytes("out")), session) ).to.be.revertedWithCustomError(receipt, "RevokedOrInvalidSession"); }); it("rejects an emit under an expired session", async function () { const { session, scopeHash, sid } = await setupAgentSession("expire"); await network.provider.send("evm_increaseTime", [6000]); await network.provider.send("evm_mine", []); await expect( emitReceipt(sid, scopeHash, ethers.keccak256(ethers.toUtf8Bytes("out")), session) ).to.be.revertedWithCustomError(receipt, "RevokedOrInvalidSession"); }); it("rejects an emit under an unknown session id", async function () { await expect( receipt.connect(relayer).emitReceipt(999, ethers.ZeroHash, ethers.keccak256(ethers.toUtf8Bytes("x")), "0x" + "00".repeat(65)) ).to.be.revertedWithCustomError(receipt, "RevokedOrInvalidSession"); }); it("a pre-existing receipt stays genuine but flips sessionLiveNow=false after revocation", async function () { const { session, scopeHash, sid } = await setupAgentSession("revoke-after"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("still-genuine")); await emitReceipt(sid, scopeHash, outputHash, session); let v = await receipt.verifyReceipt(0); expect(v.sigValid).to.equal(true); expect(v.sessionLiveNow).to.equal(true); await did.connect(owner).revokeSession(sid); v = await receipt.verifyReceipt(0); // The signature is still cryptographically valid (provenance is permanent)... expect(v.sigValid).to.equal(true); // ...but the authoring session is no longer live. expect(v.sessionLiveNow).to.equal(false); // The output is still verifiable against the recorded hash. expect(await receipt.verifyReceiptOutput(0, outputHash)).to.equal(true); }); }); // ----------------------------------------------------------------------- // reputation / bond cross-read // ----------------------------------------------------------------------- describe("reputation + bond cross-read", function () { it("provenanceOf surfaces the agent's live stake + reputation + slashing history", async function () { const { session, scopeHash, sid, root } = await setupAgentSession("crossread"); const bondKey = await receipt.bondKeyOf(root.rootKeyId); // bytes32(rootKeyId) // Operator (= DID controller = owner) posts a bond under the canonical key. await bond.connect(owner).postBond(bondKey, TEN_AERE); // Attestor records 5 positive observations for (operator, bondKey). for (let i = 0; i < 5; i++) { const ev = ethers.keccak256(ethers.toUtf8Bytes(`ev-pos-${i}`)); await rep.connect(attestor).attest(owner.address, bondKey, 1, ev, ""); } // One dispute. await rep.connect(attestor).attest(owner.address, bondKey, -1, ethers.keccak256(ethers.toUtf8Bytes("ev-dispute")), ""); // Oracle (owner) slashes 2 AERE (20% of 10, under the 25% per-call cap). await bond.connect(owner).slash(owner.address, bondKey, ONE_AERE * 2n, ethers.keccak256(ethers.toUtf8Bytes("s1")), ""); // Emit a receipt so provenanceOf has something to bind the cross-read to. const outputHash = ethers.keccak256(ethers.toUtf8Bytes("scored output")); await emitReceipt(sid, scopeHash, outputHash, session); // Expected score: 5*10 - 2*10 - 1*20 = 10 (matches AereAIReputation formula). const expectedScore = await rep.scoreOf(owner.address, bondKey); expect(expectedScore).to.equal(10n); const p = await receipt.provenanceOf(0); expect(p.agentId).to.equal(BigInt(root.rootKeyId)); expect(p.operator).to.equal(owner.address); expect(p.signer).to.equal(session.address); expect(p.outputHash).to.equal(outputHash); expect(p.scope).to.equal(scopeHash); expect(p.sigValid).to.equal(true); expect(p.sessionLiveNow).to.equal(true); // Cross-reads match the underlying contracts exactly. expect(p.reputationScore).to.equal(expectedScore); expect(p.bondAmount).to.equal(TEN_AERE - ONE_AERE * 2n); // 8 AERE remaining expect(p.bondLifetimeSlashed).to.equal(ONE_AERE * 2n); expect(p.operatorTotalSlashed).to.equal(ONE_AERE * 2n); // operatorOf resolves the canonical operator (DID controller). expect(await receipt.operatorOf(root.rootKeyId)).to.equal(owner.address); }); it("cross-reads default to zero when the operator never posted a canonical bond", async function () { const { session, scopeHash, sid, root } = await setupAgentSession("crossread-zero"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("no-bond output")); await emitReceipt(sid, scopeHash, outputHash, session); const p = await receipt.provenanceOf(0); expect(p.operator).to.equal(owner.address); // controller still resolves expect(p.reputationScore).to.equal(0n); expect(p.bondAmount).to.equal(0n); expect(p.bondLifetimeSlashed).to.equal(0n); expect(p.operatorTotalSlashed).to.equal(0n); expect(p.sigValid).to.equal(true); }); it("provenanceOf still works when economic cross-links are unset (zero addresses)", async function () { // Deploy a receipt contract with no bond / reputation wiring. const AR = await ethers.getContractFactory("AereAgentActionReceipt"); const bare = await AR.deploy(await did.getAddress(), ethers.ZeroAddress, ethers.ZeroAddress); await bare.waitForDeployment(); const { session, scopeHash, sid } = await setupAgentSession("crossread-unset"); const outputHash = ethers.keccak256(ethers.toUtf8Bytes("bare output")); const nonce = await bare.receiptNonceOf(sid); const agentId = (await did.getSession(sid)).rootKeyId; const digest = await bare.receiptDigest(agentId, sid, outputHash, scopeHash, nonce); await bare.connect(relayer).emitReceipt(sid, scopeHash, outputHash, ecdsaSign(session, digest)); const p = await bare.provenanceOf(0); expect(p.sigValid).to.equal(true); expect(p.reputationScore).to.equal(0n); expect(p.bondAmount).to.equal(0n); }); }); // ----------------------------------------------------------------------- // view guards // ----------------------------------------------------------------------- describe("view guards", function () { it("getReceipt / verifyReceipt / provenanceOf revert on an unknown receiptId", async function () { await expect(receipt.getReceipt(0)).to.be.revertedWithCustomError(receipt, "UnknownReceipt"); await expect(receipt.verifyReceipt(0)).to.be.revertedWithCustomError(receipt, "UnknownReceipt"); await expect(receipt.provenanceOf(0)).to.be.revertedWithCustomError(receipt, "UnknownReceipt"); }); it("verifyReceiptOutput returns false (no revert) on an unknown receiptId", async function () { expect(await receipt.verifyReceiptOutput(0, ethers.ZeroHash)).to.equal(false); }); }); // ----------------------------------------------------------------------- // digest cross-check (contract view == local keccak of the abi.encode preimage) // ----------------------------------------------------------------------- describe("digest preimage", function () { it("receiptDigest matches keccak256(abi.encode(DOMAIN, chainid, this, agentId, sessionId, outputHash, scope, nonce))", async function () { const agentId = 3n; const sessionId = 5n; const outputHash = ethers.keccak256(ethers.toUtf8Bytes("o")); const scope = ethers.keccak256(ethers.toUtf8Bytes("s")); const nonce = 7n; const domain = await receipt.RECEIPT_DOMAIN(); const { chainId } = await ethers.provider.getNetwork(); const local = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode( ["bytes32", "uint256", "address", "uint256", "uint256", "bytes32", "bytes32", "uint64"], [domain, chainId, await receipt.getAddress(), agentId, sessionId, outputHash, scope, nonce] ) ); expect(await receipt.receiptDigest(agentId, sessionId, outputHash, scope, nonce)).to.equal(local); }); }); // ----------------------------------------------------------------------- // seeded invariant fuzz // ----------------------------------------------------------------------- describe("seeded invariant fuzz", function () { it("genuine receipts verify; tampered/wrong-signer always reject across random rounds", async function () { const { session, scopeHash, sid, root } = await setupAgentSession("fuzz-receipt-v1"); const rng = makeRng("fuzz-body-v1"); const ROUNDS = 20; const emitted = []; for (let i = 0; i < ROUNDS; i++) { const outputHash = ethers.hexlify(rng.bytes(32)); // (a) A genuine receipt is accepted and re-verifies. await emitReceipt(sid, scopeHash, outputHash, session); const rid = emitted.length; emitted.push(outputHash); const v = await receipt.verifyReceipt(rid); expect(v.sigValid).to.equal(true); expect(v.sessionLiveNow).to.equal(true); expect(await receipt.verifyReceiptOutput(rid, outputHash)).to.equal(true); // A one-bit-different hash never matches. const flipped = ethers.getBytes(outputHash); flipped[rng.int(32)] ^= 0x01; expect(await receipt.verifyReceiptOutput(rid, ethers.hexlify(flipped))).to.equal(false); // (b) A tampered submit (sign hash A, submit hash B) always reverts. { const signed = ethers.hexlify(rng.bytes(32)); let submitted = ethers.getBytes(signed); submitted[rng.int(32)] ^= 0x01; await expect( emitReceipt(sid, scopeHash, ethers.hexlify(submitted), session, { signHash: signed }) ).to.be.revertedWithCustomError(receipt, "BadSessionSignature"); } // (c) A stranger key never authorizes a receipt. { const stranger = ethers.Wallet.createRandom(); const h = ethers.hexlify(rng.bytes(32)); await expect( emitReceipt(sid, scopeHash, h, session, { wallet: stranger }) ).to.be.revertedWithCustomError(receipt, "BadSessionSignature"); } // (d) A wrong scope always reverts. { const badScope = ethers.hexlify(rng.bytes(32)); const h = ethers.hexlify(rng.bytes(32)); await expect( emitReceipt(sid, badScope, h, session) ).to.be.revertedWithCustomError(receipt, "ScopeMismatch"); } } // Every genuine receipt is indexed and still verifiable. const ids = (await receipt.receiptsOf(root.rootKeyId)).map(Number); expect(ids.length).to.equal(ROUNDS); expect(Number(await receipt.receiptCount())).to.equal(ROUNDS); for (let rid = 0; rid < ROUNDS; rid++) { expect(await receipt.verifyReceiptOutput(rid, emitted[rid])).to.equal(true); } }); }); });