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.
577 lines
27 KiB
JavaScript
577 lines
27 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
});
|
|
});
|