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.
541 lines
25 KiB
JavaScript
541 lines
25 KiB
JavaScript
// Hardhat tests for AereVectorStore (Aere VSM, verifiable semantic memory).
|
|
//
|
|
// Covers exactly the acceptance cases:
|
|
// - commit a store version (with a mock-Falcon-512-verified signature via 0x0AE1)
|
|
// - commit a second version (append-only history preserved)
|
|
// - a tampered Falcon signature is rejected fail-closed (nothing recorded)
|
|
// - a retrieval attestation against a valid version records; a missing version reverts
|
|
// - an inclusion proof for a committed vector verifies; a non-member fails
|
|
// - AERE402 paid-query rail: paidQuery settles through the real facilitator, and a priced
|
|
// store's attestRetrieval is gated on the payment receipt (unpaid reverts fail-closed)
|
|
// - classical (owner-signed) store commits + non-owner rejection
|
|
// - createStore fail-closed on a malformed Falcon key / inconsistent price config
|
|
//
|
|
// Falcon is MOCKED here: Hardhat has no PQC precompile, so we install MockPQCPrecompile
|
|
// (scheme 1) at 0x0AE1 via hardhat_setCode. The mock parses the input at the precompile's
|
|
// SPEC offsets and accepts iff commitment == keccak256(pk || message). The REAL Falcon-512
|
|
// encoding is separately proven against the LIVE mainnet precompile (0x0AE1) elsewhere in the
|
|
// repo (AerePQCAttestation). The embedding model + similarity search + vector data are
|
|
// off-chain [MEASURE]; this test exercises only the on-chain provenance / attestation / pay path.
|
|
//
|
|
// Run: npx hardhat test test/aere-vector-store.test.js
|
|
|
|
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
const ONE = 10n ** 18n;
|
|
const FALCON512 = ethers.getAddress("0x0000000000000000000000000000000000000ae1");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Falcon-512 mock-signature helpers (mirror AerePQCAttestation.test.js, scheme 1).
|
|
// ---------------------------------------------------------------------------
|
|
const PK_LEN = 897;
|
|
const PK_HEADER = 0x09;
|
|
const ESIG_HEADER = 0x29;
|
|
|
|
function randBytes(n, salt) {
|
|
// deterministic keccak hash-chain so the test is fully reproducible
|
|
const out = new Uint8Array(n);
|
|
let seed = ethers.keccak256(ethers.toUtf8Bytes("vsm-seed-" + salt));
|
|
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 makeFalconPubKey(salt) {
|
|
const raw = randBytes(PK_LEN, salt);
|
|
raw[0] = PK_HEADER; // Falcon-512 requires the header byte
|
|
return ethers.hexlify(raw);
|
|
}
|
|
|
|
function commitmentFor(pubKey, message) {
|
|
return ethers.keccak256(ethers.concat([pubKey, message]));
|
|
}
|
|
|
|
// Falcon envelope handed to commit(): nonce(40) || esig ; esig = 0x29 || commitment(32).
|
|
function falconEnvelope(commitment, salt) {
|
|
const nonce = randBytes(40, "nonce-" + salt);
|
|
const esig = ethers.concat([new Uint8Array([ESIG_HEADER]), ethers.getBytes(commitment)]);
|
|
return ethers.hexlify(ethers.concat([nonce, esig]));
|
|
}
|
|
|
|
function genuineFalconSig(pubKey, message, salt) {
|
|
return falconEnvelope(commitmentFor(pubKey, message), salt);
|
|
}
|
|
|
|
function tamperFalconSig(sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
b[41] ^= 0x01; // flip a byte inside the commitment region -> genuine forgery
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal OpenZeppelin-compatible Merkle tree (sorted-pair keccak256).
|
|
// ---------------------------------------------------------------------------
|
|
function hashPair(a, b) {
|
|
return BigInt(a) < BigInt(b)
|
|
? ethers.keccak256(ethers.concat([a, b]))
|
|
: ethers.keccak256(ethers.concat([b, a]));
|
|
}
|
|
|
|
function buildTree(leaves) {
|
|
const layers = [leaves.slice()];
|
|
while (layers[layers.length - 1].length > 1) {
|
|
const prev = layers[layers.length - 1];
|
|
const next = [];
|
|
for (let i = 0; i < prev.length; i += 2) {
|
|
if (i + 1 < prev.length) next.push(hashPair(prev[i], prev[i + 1]));
|
|
else next.push(prev[i]); // odd node: promote
|
|
}
|
|
layers.push(next);
|
|
}
|
|
return layers;
|
|
}
|
|
|
|
function treeRoot(layers) {
|
|
return layers[layers.length - 1][0];
|
|
}
|
|
|
|
function treeProof(layers, index) {
|
|
const proof = [];
|
|
let idx = index;
|
|
for (let l = 0; l < layers.length - 1; l++) {
|
|
const layer = layers[l];
|
|
const pairIndex = idx ^ 1;
|
|
if (pairIndex < layer.length) proof.push(layer[pairIndex]);
|
|
idx = Math.floor(idx / 2);
|
|
}
|
|
return proof;
|
|
}
|
|
|
|
function leafOf(label) {
|
|
return ethers.keccak256(ethers.toUtf8Bytes(label));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// EIP-712 AERE402 payment-auth signer (mirror agentic.test.js).
|
|
// ---------------------------------------------------------------------------
|
|
async function signAuth(signerWallet, fac, agentId, token, payee, amount, resourceId, nonce, deadline) {
|
|
const domain = {
|
|
name: "AERE402Facilitator",
|
|
version: "1",
|
|
chainId: (await ethers.provider.getNetwork()).chainId,
|
|
verifyingContract: await fac.getAddress(),
|
|
};
|
|
const types = {
|
|
PaymentAuth: [
|
|
{ name: "agentId", type: "uint256" },
|
|
{ name: "token", type: "address" },
|
|
{ name: "payee", type: "address" },
|
|
{ name: "amount", type: "uint256" },
|
|
{ name: "resourceId", type: "bytes32" },
|
|
{ name: "nonce", type: "uint256" },
|
|
{ name: "deadline", type: "uint256" },
|
|
],
|
|
};
|
|
const value = { agentId, token, payee, amount, resourceId, nonce, deadline };
|
|
return signerWallet.signTypedData(domain, types, value);
|
|
}
|
|
|
|
describe("AereVectorStore (Aere VSM)", function () {
|
|
let vsm, mockCode;
|
|
let deployer, owner, provider, operator, signer, consumer;
|
|
|
|
before(async function () {
|
|
// Capture MockPQCPrecompile (scheme 1) runtime bytecode for hardhat_setCode installs.
|
|
const F = await ethers.getContractFactory("MockPQCPrecompile");
|
|
const mock = await F.deploy(1);
|
|
await mock.waitForDeployment();
|
|
mockCode = await ethers.provider.getCode(await mock.getAddress());
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
[deployer, owner, provider, operator, signer, consumer] = await ethers.getSigners();
|
|
await network.provider.send("hardhat_setCode", [FALCON512, mockCode]);
|
|
|
|
// The AERE402 rail: AereAgent registry + facilitator + a simple sink, referenced (not
|
|
// redeployed) by the vector store. Facilitator address is pre-computed so the registry
|
|
// can pin it immutably.
|
|
const sink = await (await ethers.getContractFactory("MockSinkSimple")).deploy();
|
|
await sink.waitForDeployment();
|
|
const nonce0 = await ethers.provider.getTransactionCount(deployer.address);
|
|
const futureFac = ethers.getCreateAddress({ from: deployer.address, nonce: nonce0 + 1 });
|
|
const agent = await (await ethers.getContractFactory("AereAgent")).deploy(futureFac);
|
|
await agent.waitForDeployment();
|
|
const fac = await (await ethers.getContractFactory("AERE402Facilitator")).deploy(
|
|
await agent.getAddress(),
|
|
await sink.getAddress()
|
|
);
|
|
await fac.waitForDeployment();
|
|
expect((await fac.getAddress()).toLowerCase()).to.equal(futureFac.toLowerCase());
|
|
|
|
vsm = await (await ethers.getContractFactory("AereVectorStore")).deploy(await fac.getAddress());
|
|
await vsm.waitForDeployment();
|
|
|
|
// stash rail handles on the suite for the payment test
|
|
this.agent = agent;
|
|
this.fac = fac;
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 1 + 2: PQC commit v1, then v2 (append-only history preserved)
|
|
// -------------------------------------------------------------------------
|
|
it("commits a PQC store version 1, then version 2, preserving history append-only", async function () {
|
|
const storeId = ethers.id("agent-memory://claude/user-42");
|
|
const pk = makeFalconPubKey("store-a");
|
|
await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
|
|
// vectorRoot v1 = Merkle root over 4 vector commitments
|
|
const leavesV1 = ["v0", "v1", "v2", "v3"].map(leafOf);
|
|
const treeV1 = buildTree(leavesV1);
|
|
const rootV1 = treeRoot(treeV1);
|
|
const uriV1 = "ipfs://QmIndexV1";
|
|
|
|
const chal1 = await vsm.commitChallenge(storeId, 1, rootV1, 4, uriV1);
|
|
const sig1 = genuineFalconSig(pk, chal1, "s1");
|
|
await expect(vsm.connect(operator).commit(storeId, rootV1, 4, uriV1, sig1))
|
|
.to.emit(vsm, "Committed")
|
|
.withArgs(storeId, 1, rootV1, 4, chal1, uriV1);
|
|
|
|
expect(await vsm.versionCount(storeId)).to.equal(1n);
|
|
let cur = await vsm.currentRoot(storeId);
|
|
expect(cur.version).to.equal(1n);
|
|
expect(cur.vectorRoot).to.equal(rootV1);
|
|
expect(cur.vectorCount).to.equal(4n);
|
|
|
|
// version 2 with a larger index (5 vectors)
|
|
const leavesV2 = ["v0", "v1", "v2", "v3", "v4"].map(leafOf);
|
|
const rootV2 = treeRoot(buildTree(leavesV2));
|
|
const uriV2 = "ipfs://QmIndexV2";
|
|
const chal2 = await vsm.commitChallenge(storeId, 2, rootV2, 5, uriV2);
|
|
const sig2 = genuineFalconSig(pk, chal2, "s2");
|
|
await vsm.connect(operator).commit(storeId, rootV2, 5, uriV2, sig2);
|
|
|
|
expect(await vsm.versionCount(storeId)).to.equal(2n);
|
|
cur = await vsm.currentRoot(storeId);
|
|
expect(cur.version).to.equal(2n);
|
|
expect(cur.vectorRoot).to.equal(rootV2);
|
|
|
|
// append-only: version 1 is still exactly as committed (not overwritten)
|
|
const v1 = await vsm.getVersion(storeId, 1);
|
|
expect(v1.vectorRoot).to.equal(rootV1);
|
|
expect(v1.vectorCount).to.equal(4n);
|
|
expect(v1.uri).to.equal(uriV1);
|
|
const v2 = await vsm.getVersion(storeId, 2);
|
|
expect(v2.vectorRoot).to.equal(rootV2);
|
|
expect(await vsm.commitCount()).to.equal(2n);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 3: tampered Falcon signature rejected fail-closed
|
|
// -------------------------------------------------------------------------
|
|
it("rejects a tampered Falcon signature fail-closed (nothing recorded)", async function () {
|
|
const storeId = ethers.id("agent-memory://claude/tamper");
|
|
const pk = makeFalconPubKey("store-t");
|
|
await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
|
|
const root = treeRoot(buildTree(["a", "b"].map(leafOf)));
|
|
const uri = "ipfs://QmTamper";
|
|
const chal = await vsm.commitChallenge(storeId, 1, root, 2, uri);
|
|
const bad = tamperFalconSig(genuineFalconSig(pk, chal, "st"));
|
|
|
|
await expect(vsm.connect(operator).commit(storeId, root, 2, uri, bad)).to.be.revertedWithCustomError(
|
|
vsm,
|
|
"PQCVerificationFailed"
|
|
);
|
|
// fail-closed: no version, no head, commitCount unchanged
|
|
expect(await vsm.versionCount(storeId)).to.equal(0n);
|
|
await expect(vsm.currentRoot(storeId)).to.be.revertedWithCustomError(vsm, "UnknownVersion");
|
|
expect(await vsm.commitCount()).to.equal(0n);
|
|
|
|
// a signature for a DIFFERENT message (wrong version) is also rejected
|
|
const chalWrong = await vsm.commitChallenge(storeId, 2, root, 2, uri);
|
|
const sigWrongVersion = genuineFalconSig(pk, chalWrong, "sw");
|
|
await expect(
|
|
vsm.connect(operator).commit(storeId, root, 2, uri, sigWrongVersion)
|
|
).to.be.revertedWithCustomError(vsm, "PQCVerificationFailed");
|
|
|
|
// a signature by a DIFFERENT key is rejected
|
|
const otherPk = makeFalconPubKey("other");
|
|
const sigOtherKey = genuineFalconSig(otherPk, chal, "so");
|
|
await expect(
|
|
vsm.connect(operator).commit(storeId, root, 2, uri, sigOtherKey)
|
|
).to.be.revertedWithCustomError(vsm, "PQCVerificationFailed");
|
|
|
|
// control: the genuine signature still works after all the rejections
|
|
const good = genuineFalconSig(pk, chal, "sg");
|
|
await vsm.connect(operator).commit(storeId, root, 2, uri, good);
|
|
expect(await vsm.versionCount(storeId)).to.equal(1n);
|
|
});
|
|
|
|
it("treats an empty precompile return (pre-fork / stale node) as invalid", async function () {
|
|
const storeId = ethers.id("agent-memory://claude/prefork");
|
|
const pk = makeFalconPubKey("store-pf");
|
|
await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
const root = treeRoot(buildTree(["x", "y"].map(leafOf)));
|
|
const chal = await vsm.commitChallenge(storeId, 1, root, 2, "u");
|
|
const good = genuineFalconSig(pk, chal, "pf");
|
|
// wipe the precompile: an empty account returns success + empty returndata
|
|
await network.provider.send("hardhat_setCode", [FALCON512, "0x"]);
|
|
await expect(vsm.connect(operator).commit(storeId, root, 2, "u", good)).to.be.revertedWithCustomError(
|
|
vsm,
|
|
"PQCVerificationFailed"
|
|
);
|
|
expect(await vsm.versionCount(storeId)).to.equal(0n);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 4: retrieval attestation records against a valid version; missing version reverts
|
|
// -------------------------------------------------------------------------
|
|
it("records a retrieval attestation against a committed version and reverts on a missing version", async function () {
|
|
const storeId = ethers.id("agent-memory://claude/retrieval");
|
|
const pk = makeFalconPubKey("store-r");
|
|
await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
|
|
const leaves = ["r0", "r1", "r2", "r3"].map(leafOf);
|
|
const root = treeRoot(buildTree(leaves));
|
|
const chal = await vsm.commitChallenge(storeId, 1, root, 4, "ipfs://QmR");
|
|
await vsm.connect(operator).commit(storeId, root, 4, "ipfs://QmR", genuineFalconSig(pk, chal, "r"));
|
|
|
|
const queryHash = ethers.id("query: what did the user say about post-quantum?");
|
|
const resultRoot = treeRoot(buildTree(["r1", "r2"].map(leafOf))); // provider returned 2 results
|
|
|
|
// valid version 1: records
|
|
const tx = await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, ethers.ZeroHash);
|
|
const rc = await tx.wait();
|
|
const ev = rc.logs.map((l) => vsm.interface.parseLog(l)).find((p) => p && p.name === "RetrievalAttested");
|
|
const retrievalId = ev.args.retrievalId;
|
|
expect(await vsm.retrievalCount(storeId)).to.equal(1n);
|
|
|
|
const r = await vsm.getRetrieval(retrievalId);
|
|
expect(r.exists).to.equal(true);
|
|
expect(r.version).to.equal(1n);
|
|
expect(r.provider).to.equal(provider.address);
|
|
expect(r.queryHash).to.equal(queryHash);
|
|
expect(r.resultRoot).to.equal(resultRoot);
|
|
|
|
// missing version 2 (never committed): reverts fail-closed
|
|
await expect(
|
|
vsm.connect(provider).attestRetrieval(storeId, 2, queryHash, resultRoot, ethers.ZeroHash)
|
|
).to.be.revertedWithCustomError(vsm, "UnknownVersion");
|
|
// version 0 is also invalid
|
|
await expect(
|
|
vsm.connect(provider).attestRetrieval(storeId, 0, queryHash, resultRoot, ethers.ZeroHash)
|
|
).to.be.revertedWithCustomError(vsm, "UnknownVersion");
|
|
// still only one attestation recorded
|
|
expect(await vsm.retrievalCount(storeId)).to.equal(1n);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 5: inclusion proof for a committed vector verifies; non-member fails
|
|
// -------------------------------------------------------------------------
|
|
it("verifies an inclusion proof for a committed vector and rejects a non-member", async function () {
|
|
const storeId = ethers.id("agent-memory://claude/inclusion");
|
|
const pk = makeFalconPubKey("store-i");
|
|
await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
|
|
const labels = ["vec-alpha", "vec-beta", "vec-gamma", "vec-delta", "vec-epsilon"];
|
|
const leaves = labels.map(leafOf);
|
|
const tree = buildTree(leaves);
|
|
const root = treeRoot(tree);
|
|
const chal = await vsm.commitChallenge(storeId, 1, root, leaves.length, "ipfs://QmInc");
|
|
await vsm.connect(operator).commit(storeId, root, leaves.length, "ipfs://QmInc", genuineFalconSig(pk, chal, "i"));
|
|
|
|
// member: vec-gamma (index 2) proves against the committed vectorRoot
|
|
const memberLeaf = leaves[2];
|
|
const memberProof = treeProof(tree, 2);
|
|
expect(await vsm.verifyVectorInclusion(storeId, 1, memberLeaf, memberProof)).to.equal(true);
|
|
|
|
// non-member: a leaf that was never in the tree fails with the same-shaped proof
|
|
const nonMember = leafOf("vec-not-in-store");
|
|
expect(await vsm.verifyVectorInclusion(storeId, 1, nonMember, memberProof)).to.equal(false);
|
|
|
|
// result-root inclusion: provider attests a result set, consumer proves a returned result is in it
|
|
const queryHash = ethers.id("q: gamma?");
|
|
const resultLeaves = [leaves[2], leaves[4]]; // gamma + epsilon
|
|
const resultTree = buildTree(resultLeaves);
|
|
const resultRoot = treeRoot(resultTree);
|
|
const tx = await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, ethers.ZeroHash);
|
|
const rc = await tx.wait();
|
|
const retrievalId = rc.logs
|
|
.map((l) => vsm.interface.parseLog(l))
|
|
.find((p) => p && p.name === "RetrievalAttested").args.retrievalId;
|
|
|
|
expect(await vsm.verifyResultInclusion(retrievalId, resultLeaves[0], treeProof(resultTree, 0))).to.equal(true);
|
|
expect(await vsm.verifyResultInclusion(retrievalId, nonMember, treeProof(resultTree, 0))).to.equal(false);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 6: AERE402 paid-query rail gates attestRetrieval on a priced store
|
|
// -------------------------------------------------------------------------
|
|
it("meters a priced store on the AERE402 rail and gates attestRetrieval on payment", async function () {
|
|
const agent = this.agent;
|
|
const fac = this.fac;
|
|
|
|
// A classical, PRICED store: owner signs commits; queries cost 1 token; proceeds go to provider.
|
|
const token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("USD Coin e", "USDC.e", 18);
|
|
await token.waitForDeployment();
|
|
const storeId = ethers.id("agent-memory://claude/paid");
|
|
const price = 1n * ONE;
|
|
await vsm
|
|
.connect(owner)
|
|
.createStore(storeId, "0x", await token.getAddress(), price, provider.address);
|
|
|
|
// owner commits version 1 (classical auth: msg.sender == owner)
|
|
const leaves = ["p0", "p1", "p2"].map(leafOf);
|
|
const root = treeRoot(buildTree(leaves));
|
|
await vsm.connect(owner).commit(storeId, root, 3, "ipfs://QmPaid", "0x");
|
|
expect(await vsm.versionCount(storeId)).to.equal(1n);
|
|
|
|
// register + fund an AERE402 paying agent
|
|
const regTx = await agent
|
|
.connect(operator)
|
|
.registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 10n * ONE, 0, 0, "ipfs://agent");
|
|
const agentId = (await regTx.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
|
|
await token.mint(operator.address, 100n * ONE);
|
|
await token.connect(operator).approve(await agent.getAddress(), 100n * ONE);
|
|
await agent.connect(operator).topUp(agentId, await token.getAddress(), 100n * ONE);
|
|
|
|
const queryHash = ethers.id("query: recall my PQC preferences");
|
|
|
|
// unpaid retrieval on a priced store is rejected fail-closed
|
|
await expect(
|
|
vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, root, ethers.ZeroHash)
|
|
).to.be.revertedWithCustomError(vsm, "PaymentRequired");
|
|
|
|
// pay through the rail: the agent authorizes payment to the vector store (the payee)
|
|
const amount = price;
|
|
const rail_nonce = 1n;
|
|
const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
|
|
const resourceId = await vsm.queryResourceId(storeId, queryHash);
|
|
const sig = await signAuth(
|
|
signer,
|
|
fac,
|
|
agentId,
|
|
await token.getAddress(),
|
|
await vsm.getAddress(),
|
|
amount,
|
|
resourceId,
|
|
rail_nonce,
|
|
deadline
|
|
);
|
|
|
|
const provBefore = await token.balanceOf(provider.address);
|
|
const payTx = await vsm
|
|
.connect(consumer)
|
|
.paidQuery(storeId, queryHash, provider.address, agentId, amount, rail_nonce, deadline, sig);
|
|
const payRc = await payTx.wait();
|
|
const receiptId = payRc.logs
|
|
.map((l) => vsm.interface.parseLog(l))
|
|
.find((p) => p && p.name === "QueryPaid").args.receiptId;
|
|
|
|
// fee (25 bps) went to the sink; the remainder was forwarded to the provider (recipient)
|
|
const fee = (amount * 25n) / 10_000n;
|
|
expect((await token.balanceOf(provider.address)) - provBefore).to.equal(amount - fee);
|
|
expect(await fac.consumed(agentId, rail_nonce)).to.equal(true);
|
|
// the store custodies nothing across the call
|
|
expect(await token.balanceOf(await vsm.getAddress())).to.equal(0n);
|
|
|
|
const receipt = await vsm.getReceipt(receiptId);
|
|
expect(receipt.exists).to.equal(true);
|
|
expect(receipt.consumed).to.equal(false);
|
|
expect(receipt.storeId).to.equal(storeId);
|
|
expect(receipt.queryHash).to.equal(queryHash);
|
|
expect(receipt.amount).to.equal(amount);
|
|
expect(receipt.provider).to.equal(provider.address); // L2: receipt is bound to the intended provider
|
|
|
|
// L2 (LOW) fix: a THIRD PARTY (not the bound provider) cannot front-run and consume the receipt
|
|
// with a junk resultRoot to grief the honest provider. Even the payer (consumer) is not the
|
|
// bound provider, so it too is rejected; the receipt stays unconsumed and usable.
|
|
const junkRoot = ethers.id("junk-result-root-from-griefer");
|
|
await expect(
|
|
vsm.connect(operator).attestRetrieval(storeId, 1, queryHash, junkRoot, receiptId)
|
|
).to.be.revertedWithCustomError(vsm, "NotReceiptProvider");
|
|
await expect(
|
|
vsm.connect(consumer).attestRetrieval(storeId, 1, queryHash, junkRoot, receiptId)
|
|
).to.be.revertedWithCustomError(vsm, "NotReceiptProvider");
|
|
expect((await vsm.getReceipt(receiptId)).consumed).to.equal(false);
|
|
|
|
// now the retrieval attestation is allowed for the BOUND provider, and consumes the receipt one-shot
|
|
const resultRoot = treeRoot(buildTree([leaves[0], leaves[1]]));
|
|
await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptId);
|
|
expect(await vsm.retrievalCount(storeId)).to.equal(1n);
|
|
expect((await vsm.getReceipt(receiptId)).consumed).to.equal(true);
|
|
|
|
// the same receipt cannot back a second retrieval
|
|
await expect(
|
|
vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptId)
|
|
).to.be.revertedWithCustomError(vsm, "ReceiptConsumed");
|
|
|
|
// a FRESH receipt paid for query B cannot back a retrieval that claims query A (ReceiptMismatch)
|
|
const queryB = ethers.id("query: something else");
|
|
const nonceB = 2n;
|
|
const sigB = await signAuth(
|
|
signer,
|
|
fac,
|
|
agentId,
|
|
await token.getAddress(),
|
|
await vsm.getAddress(),
|
|
amount,
|
|
await vsm.queryResourceId(storeId, queryB),
|
|
nonceB,
|
|
deadline
|
|
);
|
|
const rcB = await (await vsm.connect(consumer).paidQuery(storeId, queryB, provider.address, agentId, amount, nonceB, deadline, sigB)).wait();
|
|
const receiptB = rcB.logs.map((l) => vsm.interface.parseLog(l)).find((p) => p && p.name === "QueryPaid").args.receiptId;
|
|
await expect(
|
|
vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptB)
|
|
).to.be.revertedWithCustomError(vsm, "ReceiptMismatch");
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 7: classical store owner auth + non-owner rejection
|
|
// -------------------------------------------------------------------------
|
|
it("classical store: only the owner may commit", async function () {
|
|
const storeId = ethers.id("agent-memory://classical");
|
|
await vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
|
|
const root = treeRoot(buildTree(["c0", "c1"].map(leafOf)));
|
|
// non-owner attempt rejected
|
|
await expect(vsm.connect(provider).commit(storeId, root, 2, "u", "0x")).to.be.revertedWithCustomError(
|
|
vsm,
|
|
"NotOwner"
|
|
);
|
|
// owner commit works (falconSig ignored for a classical store)
|
|
await vsm.connect(owner).commit(storeId, root, 2, "u", "0x");
|
|
expect(await vsm.versionCount(storeId)).to.equal(1n);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 8: createStore fail-closed on malformed config
|
|
// -------------------------------------------------------------------------
|
|
it("createStore rejects a malformed Falcon key and inconsistent price config", async function () {
|
|
const storeId = ethers.id("agent-memory://bad");
|
|
// wrong-length Falcon key
|
|
await expect(
|
|
vsm.connect(owner).createStore(storeId, "0x" + "09" + "00".repeat(800), ethers.ZeroAddress, 0, ethers.ZeroAddress)
|
|
).to.be.revertedWithCustomError(vsm, "InvalidPubKey");
|
|
// correct length but wrong header
|
|
await expect(
|
|
vsm.connect(owner).createStore(storeId, "0x" + "0a" + "00".repeat(896), ethers.ZeroAddress, 0, ethers.ZeroAddress)
|
|
).to.be.revertedWithCustomError(vsm, "InvalidPubKey");
|
|
// priced store without a token / recipient
|
|
await expect(
|
|
vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 1n * ONE, ethers.ZeroAddress)
|
|
).to.be.revertedWithCustomError(vsm, "InvalidConfig");
|
|
// free store that nonetheless sets a token
|
|
await expect(
|
|
vsm.connect(owner).createStore(storeId, "0x", owner.address, 0, ethers.ZeroAddress)
|
|
).to.be.revertedWithCustomError(vsm, "InvalidConfig");
|
|
// duplicate storeId rejected
|
|
await vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress);
|
|
await expect(
|
|
vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress)
|
|
).to.be.revertedWithCustomError(vsm, "StoreExists");
|
|
});
|
|
});
|