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.
604 lines
29 KiB
JavaScript
604 lines
29 KiB
JavaScript
// ERC-8004 + x402/AP2 conformance adapters over Aere's LIVE, already-deployed agent stack.
|
|
//
|
|
// The three adapters wrap (never redeploy) the live primitives:
|
|
// - AereIdentityRegistry8004 over AereAgentDID (agent identity / DID)
|
|
// - AereReputationRegistry8004 over AereAIReputation (attest / read feedback)
|
|
// - AereValidationRegistry8004 over AerePQCKeyRegistry (POST-QUANTUM request/respond proof)
|
|
// plus AereAP2MandateVerifier: a Falcon-512-signed x402/AP2 spend mandate.
|
|
//
|
|
// Aere has no PQC precompile in the local Hardhat EVM, so we install the repo's MockPQCPrecompile
|
|
// at 0x0AE1 (Falcon-512) and 0x0AE3 (ML-DSA-44) via hardhat_setCode, exactly as the fault-harness
|
|
// AereRecoveryRegistry test does. The mock parses the input at the LIVE precompile's spec offsets
|
|
// and accepts iff commitment == keccak256(pk || message), so a positive result here proves the
|
|
// adapter routed the correct input to the correct precompile and enforces fail-closed semantics.
|
|
//
|
|
// [MEASURE] The REAL precompile path (that on-chain 0x0AE1 / 0x0AE3 accept a genuine Falcon-512 /
|
|
// ML-DSA-44 signature) is exercised separately against LIVE mainnet 2800 via eth_call (the same way
|
|
// AerePQCKeyRegistry / AerePQCAttestation are proven) and is NOT re-proven inside this unit test.
|
|
// The adapters stay hard-wired to the live registry / precompiles for production.
|
|
//
|
|
// Run: npx hardhat test test/erc8004-adapters.test.js
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
const PRECOMPILE = {
|
|
1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
|
|
3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
|
|
};
|
|
|
|
const META = {
|
|
1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true }, // Falcon-512
|
|
3: { pkLen: 1312, sigLen: 2420, falcon: false }, // ML-DSA-44
|
|
};
|
|
|
|
const REG_STATUS_ACTIVE = 1;
|
|
|
|
// ---- deterministic seeded RNG (keccak hash-chain) so the test is fully reproducible ----
|
|
function makeRng(seedText) {
|
|
let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
|
|
return {
|
|
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 makePubKey(scheme, rng) {
|
|
const m = META[scheme];
|
|
const raw = rng.bytes(m.pkLen);
|
|
if (m.falcon) raw[0] = m.pkHeader; // Falcon requires the header byte
|
|
return ethers.hexlify(raw);
|
|
}
|
|
|
|
function commitmentFor(pubKey, message) {
|
|
return ethers.keccak256(ethers.concat([pubKey, message]));
|
|
}
|
|
|
|
// A genuine PQC signature envelope binding `message` to `pubKey` under the mock's semantics.
|
|
function genuine(scheme, pubKey, message, rng) {
|
|
const m = META[scheme];
|
|
const commitment = commitmentFor(pubKey, message);
|
|
if (m.falcon) {
|
|
// Falcon: envelope = nonce(40) || esig ; esig = esigHeader || commitment(32) => 73 bytes.
|
|
const nonce = rng.bytes(40);
|
|
const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]);
|
|
return ethers.hexlify(ethers.concat([nonce, esig]));
|
|
}
|
|
// ML-DSA / fixed: sig = commitment(32) || filler ; total sigLen bytes.
|
|
const filler = rng.bytes(m.sigLen - 32);
|
|
return ethers.hexlify(ethers.concat([commitment, filler]));
|
|
}
|
|
|
|
// Flip one byte inside the commitment region (a genuine forgery the precompile must reject).
|
|
function tamper(scheme, sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
const off = META[scheme].falcon ? 41 : 0; // Falcon commitment starts at 41; ML-DSA at 0.
|
|
b[off] ^= 0x01;
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
async function futureTs(secs) {
|
|
const b = await ethers.provider.getBlock("latest");
|
|
return b.timestamp + secs;
|
|
}
|
|
|
|
describe("ERC-8004 + x402/AP2 adapters over Aere's live agent stack", function () {
|
|
let owner, other, foundation;
|
|
let reg, did, identity, validation, mandate;
|
|
let mockCode;
|
|
|
|
before(async function () {
|
|
[owner, other, foundation] = await ethers.getSigners();
|
|
mockCode = {};
|
|
const F = await ethers.getContractFactory("MockPQCPrecompile");
|
|
for (const scheme of [1, 3]) {
|
|
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, 3]) {
|
|
await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
|
|
}
|
|
}
|
|
|
|
// Register a PQC key under `signer` in the live registry (proof-of-possession via the precompile).
|
|
async function registerKey(scheme, signer, rng) {
|
|
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 { keyId: Number(log.args.keyId), pk, scheme };
|
|
}
|
|
|
|
// A Falcon root -> DID agent -> registered ERC-8004 identity, all over the live primitives.
|
|
async function makeAgent(seed) {
|
|
const rng = makeRng(seed);
|
|
const root = await registerKey(1, owner, rng);
|
|
await did.connect(owner).createAgent(root.keyId);
|
|
const domain = `agent-${seed}.aere.network`;
|
|
const card = `https://${domain}/.well-known/agent-card.json`;
|
|
await identity.connect(owner).register(root.keyId, domain, card);
|
|
return { rng, agentId: root.keyId, rootPk: root.pk, domain, card };
|
|
}
|
|
|
|
beforeEach(async function () {
|
|
await installMocks();
|
|
|
|
reg = await (await ethers.getContractFactory("AerePQCKeyRegistry")).deploy();
|
|
await reg.waitForDeployment();
|
|
|
|
did = await (await ethers.getContractFactory("AereAgentDID")).deploy(
|
|
await reg.getAddress(),
|
|
ethers.ZeroAddress, // agentBond cross-link (informational; unused by the adapters)
|
|
ethers.ZeroAddress // aiReputation cross-link (informational)
|
|
);
|
|
await did.waitForDeployment();
|
|
|
|
identity = await (await ethers.getContractFactory("AereIdentityRegistry8004")).deploy(await did.getAddress());
|
|
await identity.waitForDeployment();
|
|
|
|
validation = await (await ethers.getContractFactory("AereValidationRegistry8004")).deploy(
|
|
await reg.getAddress(),
|
|
await did.getAddress()
|
|
);
|
|
await validation.waitForDeployment();
|
|
|
|
// Mandate verifier with an OPEN accounting path (SETTLEMENT_EXECUTOR = 0) for verification.
|
|
mandate = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy(
|
|
await reg.getAddress(),
|
|
ethers.ZeroAddress
|
|
);
|
|
await mandate.waitForDeployment();
|
|
});
|
|
|
|
// =========================================================================
|
|
// 1. Identity adapter over AereAgentDID
|
|
// =========================================================================
|
|
describe("AereIdentityRegistry8004 (identity: register / resolve)", function () {
|
|
it("wires the live DID and reads the registry from it", async function () {
|
|
expect(await identity.DID()).to.equal(await did.getAddress());
|
|
expect(await identity.KEY_REGISTRY()).to.equal(await reg.getAddress());
|
|
});
|
|
|
|
it("registers an existing DID agent with a DID string + AgentCard URI, and resolves it", async function () {
|
|
const a = await makeAgent("id-1");
|
|
|
|
const info = await identity.getAgent(a.agentId);
|
|
expect(info.agentId).to.equal(BigInt(a.agentId));
|
|
expect(info.did).to.equal(`did:aere:31337:${a.agentId}`); // chainId 31337 in Hardhat
|
|
expect(info.agentDomain).to.equal(a.domain);
|
|
expect(info.agentCardURI).to.equal(a.card);
|
|
expect(info.agentAddress).to.equal(owner.address); // current controller, read live from the registry
|
|
expect(info.registered).to.equal(true);
|
|
|
|
// Resolve by domain and by controller address.
|
|
const byDomain = await identity.resolveByDomain(a.domain);
|
|
expect(byDomain.agentId).to.equal(BigInt(a.agentId));
|
|
const byAddr = await identity.resolveByAddress(owner.address);
|
|
expect(byAddr.agentId).to.equal(BigInt(a.agentId));
|
|
|
|
expect(await identity.isRegistered(a.agentId)).to.equal(true);
|
|
expect(await identity.agentCount()).to.equal(1n);
|
|
});
|
|
|
|
it("only the agent's controller (Falcon-root owner) can register / update", async function () {
|
|
const rng = makeRng("id-ctrl");
|
|
const root = await registerKey(1, owner, rng);
|
|
await did.connect(owner).createAgent(root.keyId);
|
|
|
|
// A stranger cannot bind metadata to someone else's agent.
|
|
await expect(
|
|
identity.connect(other).register(root.keyId, "evil.aere.network", "ipfs://x")
|
|
).to.be.revertedWithCustomError(identity, "NotController");
|
|
|
|
// The controller can, then update.
|
|
await identity.connect(owner).register(root.keyId, "ok.aere.network", "ipfs://card1");
|
|
await expect(
|
|
identity.connect(other).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2")
|
|
).to.be.revertedWithCustomError(identity, "NotController");
|
|
await identity.connect(owner).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2");
|
|
const info = await identity.getAgent(root.keyId);
|
|
expect(info.agentDomain).to.equal("ok2.aere.network");
|
|
expect(info.agentCardURI).to.equal("ipfs://card2");
|
|
});
|
|
|
|
it("reverts for an unknown DID agent and rejects duplicate domains", async function () {
|
|
await expect(identity.connect(owner).register(999, "d.aere.network", "u")).to.be.revertedWithCustomError(
|
|
identity,
|
|
"UnknownAgent"
|
|
);
|
|
const a = await makeAgent("id-dup");
|
|
const rng = makeRng("id-dup2");
|
|
const root = await registerKey(1, owner, rng);
|
|
await did.connect(owner).createAgent(root.keyId);
|
|
await expect(identity.connect(owner).register(root.keyId, a.domain, "u")).to.be.revertedWithCustomError(
|
|
identity,
|
|
"DomainTaken"
|
|
);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 2. Reputation adapter over AereAIReputation (attest / read)
|
|
// =========================================================================
|
|
describe("AereReputationRegistry8004 (reputation: attest / read feedback)", function () {
|
|
let token, sink, bond, rep, repAdapter;
|
|
|
|
async function deployRep(attestorAdapter) {
|
|
token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("AERE", "AERE", 18);
|
|
await token.waitForDeployment();
|
|
sink = await (await ethers.getContractFactory("MockSinkSimple")).deploy();
|
|
await sink.waitForDeployment();
|
|
bond = await (await ethers.getContractFactory("AereAgentBond")).deploy(
|
|
await token.getAddress(),
|
|
await sink.getAddress(),
|
|
[foundation.address]
|
|
);
|
|
await bond.waitForDeployment();
|
|
|
|
if (attestorAdapter) {
|
|
// The live AereAIReputation attestor set is fixed at deploy with no add path, so to let the
|
|
// adapter WRITE feedback through it, the reputation instance must list the adapter as an
|
|
// attestor. Predict the adapter address so it can be authorized at reputation deploy.
|
|
const nonce = await owner.getNonce();
|
|
const predicted = ethers.getCreateAddress({ from: owner.address, nonce: nonce + 1 });
|
|
rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [predicted]);
|
|
await rep.waitForDeployment();
|
|
repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy(
|
|
await rep.getAddress(),
|
|
await identity.getAddress()
|
|
);
|
|
await repAdapter.waitForDeployment();
|
|
expect(await repAdapter.getAddress()).to.equal(predicted);
|
|
// M1: giveFeedback is gated to an owner-curated author allowlist. Authorize `other` so the
|
|
// existing feedback flows below (which submit as `other`) exercise the write path.
|
|
await repAdapter.connect(owner).addFeedbackAuthor(other.address);
|
|
} else {
|
|
// Reputation that does NOT authorize the adapter (mirrors the immutable mainnet Foundation set).
|
|
rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [
|
|
foundation.address,
|
|
]);
|
|
await rep.waitForDeployment();
|
|
repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy(
|
|
await rep.getAddress(),
|
|
await identity.getAddress()
|
|
);
|
|
await repAdapter.waitForDeployment();
|
|
// M1: authorize `other` as a feedback author (owner-curated allowlist).
|
|
await repAdapter.connect(owner).addFeedbackAuthor(other.address);
|
|
}
|
|
}
|
|
|
|
it("forwards ERC-8004 feedback into the live reputation and reads the score back", async function () {
|
|
const a = await makeAgent("rep-1");
|
|
await deployRep(true);
|
|
|
|
// Maps the ERC-8004 agentId to exactly one live (operator, agentKey) slot.
|
|
const [operator, agentKey] = await repAdapter.liveKeyOf(a.agentId);
|
|
expect(operator).to.equal(owner.address); // the agent's controller
|
|
expect(agentKey).to.equal(ethers.zeroPadValue(ethers.toBeHex(a.agentId), 32));
|
|
expect(await repAdapter.canWriteThrough()).to.equal(true);
|
|
|
|
// Attest a positive interaction through the adapter; it forwards to the live attest.
|
|
const ev1 = ethers.keccak256(ethers.toUtf8Bytes("evidence-1"));
|
|
await expect(repAdapter.connect(other).giveFeedback(a.agentId, 1, ev1, "ipfs://ev1")).to.emit(
|
|
repAdapter,
|
|
"NewFeedback"
|
|
);
|
|
|
|
// Read the canonical live score straight from AereAIReputation via the adapter.
|
|
// scoreOf = positiveCount*10 (no slash, no dispute) => 1 positive => 10.
|
|
expect(await repAdapter.getScore(a.agentId)).to.equal(10n);
|
|
const stats = await repAdapter.getStats(a.agentId);
|
|
expect(stats.positiveCount).to.equal(1n);
|
|
expect(stats.disputeCount).to.equal(0n);
|
|
|
|
// A dispute (-1) lowers the score; two positives raise it.
|
|
const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-2"));
|
|
await repAdapter.connect(other).giveFeedback(a.agentId, 1, ev2, "");
|
|
const ev3 = ethers.keccak256(ethers.toUtf8Bytes("evidence-3"));
|
|
await repAdapter.connect(other).giveFeedback(a.agentId, -1, ev3, "");
|
|
// positive=2 (=>20), dispute=1 (=>-20) => 0.
|
|
expect(await repAdapter.getScore(a.agentId)).to.equal(0n);
|
|
const stats2 = await repAdapter.getStats(a.agentId);
|
|
expect(stats2.positiveCount).to.equal(2n);
|
|
expect(stats2.disputeCount).to.equal(1n);
|
|
});
|
|
|
|
it("rejects feedback for an unregistered agent and an out-of-range delta", async function () {
|
|
await deployRep(true); // `other` is authorized as a feedback author in deployRep
|
|
await expect(repAdapter.connect(other).giveFeedback(12345, 1, ethers.id("x"), "")).to.be.revertedWithCustomError(
|
|
repAdapter,
|
|
"AgentNotRegistered"
|
|
);
|
|
const a = await makeAgent("rep-bad");
|
|
await expect(repAdapter.connect(other).giveFeedback(a.agentId, 2, ethers.id("x"), "")).to.be.revertedWithCustomError(
|
|
repAdapter,
|
|
"InvalidDelta"
|
|
);
|
|
});
|
|
|
|
// M1 (MEDIUM) fix: giveFeedback is gated to an owner-curated allowlist so the adapter cannot be an
|
|
// open reputation firehose once it is a live attestor. Proves: an unauthorized caller reverts, an
|
|
// authorized caller succeeds, revocation re-blocks, and only the owner may manage the allowlist.
|
|
it("gates giveFeedback to owner-authorized feedback authors (M1)", async function () {
|
|
const a = await makeAgent("rep-gate");
|
|
await deployRep(true); // adapter is a live attestor; deployRep authorizes `other`, not `foundation`
|
|
expect(await repAdapter.canWriteThrough()).to.equal(true);
|
|
|
|
// An UNAUTHORIZED caller cannot write, even though the adapter is a live attestor.
|
|
expect(await repAdapter.isAuthorizedFeedbackAuthor(foundation.address)).to.equal(false);
|
|
const ev = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate"));
|
|
await expect(
|
|
repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "")
|
|
).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor");
|
|
expect(await repAdapter.getScore(a.agentId)).to.equal(0n); // nothing written
|
|
|
|
// Owner authorizes `foundation`; the SAME call now succeeds and moves the live score.
|
|
await expect(repAdapter.connect(owner).addFeedbackAuthor(foundation.address))
|
|
.to.emit(repAdapter, "FeedbackAuthorSet")
|
|
.withArgs(foundation.address, true);
|
|
await expect(repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "")).to.emit(repAdapter, "NewFeedback");
|
|
expect(await repAdapter.getScore(a.agentId)).to.equal(10n);
|
|
|
|
// Owner revokes; the caller is fail-closed blocked again.
|
|
await repAdapter.connect(owner).removeFeedbackAuthor(foundation.address);
|
|
const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate-2"));
|
|
await expect(
|
|
repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev2, "")
|
|
).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor");
|
|
|
|
// The author allowlist is owner-only (OZ v4 Ownable: "Ownable: caller is not the owner").
|
|
await expect(repAdapter.connect(other).addFeedbackAuthor(other.address)).to.be.reverted;
|
|
});
|
|
|
|
it("is honest about the immutable attestor set: write-through reverts when the adapter is not authorized", async function () {
|
|
const a = await makeAgent("rep-noauth");
|
|
await deployRep(false); // reputation lists Foundation only, not the adapter (the mainnet case)
|
|
|
|
expect(await repAdapter.canWriteThrough()).to.equal(false);
|
|
// `other` is an authorized feedback author (deployRep), so it clears the caller gate and reaches
|
|
// the fail-closed attestor check on the live reputation.
|
|
await expect(
|
|
repAdapter.connect(other).giveFeedback(a.agentId, 1, ethers.id("ev"), "")
|
|
).to.be.revertedWithCustomError(repAdapter, "AdapterNotAttestor");
|
|
|
|
// The READ path still adapts the live contract with no authorization: score starts at 0.
|
|
expect(await repAdapter.getScore(a.agentId)).to.equal(0n);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 3. Validation adapter over the live PQC precompiles (the differentiator)
|
|
// =========================================================================
|
|
describe("AereValidationRegistry8004 (validation: request/respond with a PQC proof)", function () {
|
|
it("accepts a genuine Falcon-512 (0x0AE1) validation response and records it", async function () {
|
|
const a = await makeAgent("val-falcon");
|
|
const rng = makeRng("val-falcon-key");
|
|
const v = await registerKey(1, other, rng); // a Falcon-512 validator key
|
|
expect(await reg.statusOf(v.keyId)).to.equal(REG_STATUS_ACTIVE);
|
|
|
|
const dataHash = ethers.id("model-output-commitment-A");
|
|
const tx = await validation.requestValidation(v.keyId, a.agentId, dataHash);
|
|
const rc = await tx.wait();
|
|
const reqLog = rc.logs.map((l) => validation.interface.parseLog(l)).find((p) => p && p.name === "ValidationRequest");
|
|
const requestId = reqLog.args.requestId;
|
|
|
|
const response = 92; // ERC-8004 score 0..100
|
|
const challenge = await validation.responseChallenge(requestId, response);
|
|
const proof = genuine(1, v.pk, challenge, rng);
|
|
|
|
await expect(validation.respondValidation(requestId, response, proof))
|
|
.to.emit(validation, "ValidationResponse")
|
|
.withArgs(requestId, v.keyId, a.agentId, response, 1);
|
|
|
|
expect(await validation.isValidated(requestId)).to.equal(true);
|
|
const rec = await validation.getValidation(requestId);
|
|
expect(rec.status).to.equal(2); // Completed
|
|
expect(rec.response).to.equal(92n);
|
|
expect(rec.pqcScheme).to.equal(1n);
|
|
expect(rec.serverAgentId).to.equal(BigInt(a.agentId));
|
|
});
|
|
|
|
it("accepts a genuine ML-DSA-44 (0x0AE3) validation response too", async function () {
|
|
const a = await makeAgent("val-mldsa");
|
|
const rng = makeRng("val-mldsa-key");
|
|
const v = await registerKey(3, other, rng); // an ML-DSA-44 validator key
|
|
|
|
const dataHash = ethers.id("model-output-commitment-B");
|
|
const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
|
|
await validation.requestValidation(v.keyId, a.agentId, dataHash);
|
|
|
|
const response = 77;
|
|
const challenge = await validation.responseChallenge(requestId, response);
|
|
const proof = genuine(3, v.pk, challenge, rng);
|
|
await expect(validation.respondValidation(requestId, response, proof))
|
|
.to.emit(validation, "ValidationResponse")
|
|
.withArgs(requestId, v.keyId, a.agentId, response, 3);
|
|
expect(await validation.isValidated(requestId)).to.equal(true);
|
|
});
|
|
|
|
it("REJECTS a tampered proof fail-closed (nothing recorded)", async function () {
|
|
const a = await makeAgent("val-tamper");
|
|
const rng = makeRng("val-tamper-key");
|
|
const v = await registerKey(1, other, rng);
|
|
|
|
const dataHash = ethers.id("model-output-commitment-C");
|
|
const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
|
|
await validation.requestValidation(v.keyId, a.agentId, dataHash);
|
|
|
|
const response = 88;
|
|
const challenge = await validation.responseChallenge(requestId, response);
|
|
const good = genuine(1, v.pk, challenge, rng);
|
|
const bad = tamper(1, good);
|
|
|
|
await expect(validation.respondValidation(requestId, response, bad)).to.be.revertedWithCustomError(
|
|
validation,
|
|
"PQCValidationFailed"
|
|
);
|
|
// Fail-closed: request stays Pending, nothing recorded.
|
|
expect(await validation.isValidated(requestId)).to.equal(false);
|
|
const rec = await validation.getValidation(requestId);
|
|
expect(rec.status).to.equal(1); // Pending
|
|
});
|
|
|
|
it("REJECTS a proof bound to a DIFFERENT response score (binding), and treats a stale precompile as invalid", async function () {
|
|
const a = await makeAgent("val-bind");
|
|
const rng = makeRng("val-bind-key");
|
|
const v = await registerKey(1, other, rng);
|
|
|
|
const dataHash = ethers.id("model-output-commitment-D");
|
|
const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
|
|
await validation.requestValidation(v.keyId, a.agentId, dataHash);
|
|
|
|
// Sign the challenge for response=50, then try to submit response=51 with it.
|
|
const challenge50 = await validation.responseChallenge(requestId, 50);
|
|
const proof50 = genuine(1, v.pk, challenge50, rng);
|
|
await expect(validation.respondValidation(requestId, 51, proof50)).to.be.revertedWithCustomError(
|
|
validation,
|
|
"PQCValidationFailed"
|
|
);
|
|
|
|
// Wipe the precompile (pre-fork / stale chain): an empty account returns empty data => invalid.
|
|
await network.provider.send("hardhat_setCode", [PRECOMPILE[1], "0x"]);
|
|
await expect(validation.respondValidation(requestId, 50, proof50)).to.be.revertedWithCustomError(
|
|
validation,
|
|
"PQCValidationFailed"
|
|
);
|
|
expect(await validation.isValidated(requestId)).to.equal(false);
|
|
});
|
|
|
|
it("rejects requests with a non-PQC / inactive validator key or an unknown server agent", async function () {
|
|
const a = await makeAgent("val-guard");
|
|
const rng = makeRng("val-guard-key");
|
|
const v = await registerKey(1, other, rng);
|
|
|
|
// Unknown server agent.
|
|
await expect(validation.requestValidation(v.keyId, 424242, ethers.id("d"))).to.be.revertedWithCustomError(
|
|
validation,
|
|
"ServerAgentUnknown"
|
|
);
|
|
// Revoked validator key is no longer ACTIVE.
|
|
await reg.connect(other).revokeKey(v.keyId);
|
|
await expect(validation.requestValidation(v.keyId, a.agentId, ethers.id("d"))).to.be.revertedWithCustomError(
|
|
validation,
|
|
"ValidatorKeyNotActive"
|
|
);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 4. x402 / AP2 post-quantum mandate verifier
|
|
// =========================================================================
|
|
describe("AereAP2MandateVerifier (Falcon-512-signed x402/AP2 spend mandate)", function () {
|
|
async function makeMandate(overrides = {}) {
|
|
const rng = makeRng(overrides.seed || "mandate-key");
|
|
const agent = await registerKey(1, owner, rng); // the paying agent's Falcon-512 key
|
|
const start = overrides.periodStart ?? (await futureTs(-10));
|
|
const end = overrides.periodEnd ?? (await futureTs(3600));
|
|
const m = {
|
|
agentKeyId: agent.keyId,
|
|
token: overrides.token ?? "0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8", // live WAERE address (label only)
|
|
maxAmount: overrides.maxAmount ?? ethers.parseUnits("10", 18),
|
|
periodStart: start,
|
|
periodEnd: end,
|
|
purpose: overrides.purpose ?? ethers.id("inference:anthropic"),
|
|
mandateNonce: overrides.mandateNonce ?? 0,
|
|
};
|
|
return { rng, agent, m };
|
|
}
|
|
|
|
it("verifies a genuine Falcon mandate signature (pure view) and authorizes spend up to the cap", async function () {
|
|
const { rng, agent, m } = await makeMandate();
|
|
const mh = await mandate.mandateHash(m);
|
|
const sig = genuine(1, agent.pk, mh, rng);
|
|
|
|
expect(await mandate.verifyMandate(m, sig)).to.equal(true);
|
|
|
|
// Authorize two spends within the 10-unit cap.
|
|
await expect(mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig)).to.emit(mandate, "MandateAuthorized");
|
|
await mandate.authorizeSpend(m, ethers.parseUnits("3", 18), sig);
|
|
expect(await mandate.spentUnder(mh)).to.equal(ethers.parseUnits("7", 18));
|
|
expect(await mandate.remainingUnder(m)).to.equal(ethers.parseUnits("3", 18));
|
|
});
|
|
|
|
it("rejects spend over the cumulative cap", async function () {
|
|
const { rng, agent, m } = await makeMandate({ maxAmount: ethers.parseUnits("5", 18) });
|
|
const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
|
|
await mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig);
|
|
await expect(mandate.authorizeSpend(m, ethers.parseUnits("2", 18), sig)).to.be.revertedWithCustomError(
|
|
mandate,
|
|
"MandateCapExceeded"
|
|
);
|
|
});
|
|
|
|
it("rejects an expired mandate window", async function () {
|
|
const { rng, agent, m } = await makeMandate({ periodStart: await futureTs(-100), periodEnd: await futureTs(-10) });
|
|
const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
|
|
await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
|
|
mandate,
|
|
"MandateExpired"
|
|
);
|
|
});
|
|
|
|
it("REJECTS a tampered mandate signature fail-closed (verifyMandate false, authorize reverts)", async function () {
|
|
const { rng, agent, m } = await makeMandate();
|
|
const good = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
|
|
const bad = tamper(1, good);
|
|
|
|
expect(await mandate.verifyMandate(m, bad)).to.equal(false);
|
|
await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), bad)).to.be.revertedWithCustomError(
|
|
mandate,
|
|
"MandateSignatureInvalid"
|
|
);
|
|
expect(await mandate.spentUnder(await mandate.mandateHash(m))).to.equal(0n);
|
|
});
|
|
|
|
it("halts the mandate the instant the Falcon key is revoked (root lifecycle)", async function () {
|
|
const { rng, agent, m } = await makeMandate({ seed: "mandate-revoke" });
|
|
const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
|
|
await mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig); // works before revoke
|
|
|
|
await reg.connect(owner).revokeKey(agent.keyId);
|
|
expect(await mandate.verifyMandate(m, sig)).to.equal(false);
|
|
await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
|
|
mandate,
|
|
"AgentKeyNotActiveFalcon"
|
|
);
|
|
});
|
|
|
|
it("enforces the settlement-executor gate when one is configured", async function () {
|
|
// A verifier wired to a specific executor rejects authorizeSpend from anyone else.
|
|
const gated = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy(
|
|
await reg.getAddress(),
|
|
other.address // SETTLEMENT_EXECUTOR
|
|
);
|
|
await gated.waitForDeployment();
|
|
const { rng, agent, m } = await makeMandate({ seed: "mandate-gated" });
|
|
const sig = genuine(1, agent.pk, await gated.mandateHash(m), rng);
|
|
await expect(gated.connect(owner).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
|
|
gated,
|
|
"NotSettlementExecutor"
|
|
);
|
|
// The configured executor can.
|
|
await expect(gated.connect(other).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.emit(
|
|
gated,
|
|
"MandateAuthorized"
|
|
);
|
|
});
|
|
});
|
|
});
|