aere-contracts/test/aere-vector-store.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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");
});
});