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.
537 lines
24 KiB
JavaScript
537 lines
24 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AereAgentDID — Falcon-root DID for AI agents issuing short-lived secp256k1
|
|
// session keys with scope + spend-policy + on-chain revocation.
|
|
//
|
|
// The ROOT authority is a Falcon key held in AerePQCKeyRegistry (proof-of-possession
|
|
// enforced there). We install MockPQCPrecompile at 0x0AE1..0x0AE4 (hardhat_setCode) so
|
|
// the registry's Falcon verification runs; the real precompile encoding is proven
|
|
// separately against the LIVE mainnet precompile via eth_call. Session ISSUANCE is
|
|
// gated by a Falcon PoP over an issuance challenge; session ACTIONS are cheap secp256k1
|
|
// (ecrecover). A genuine Falcon envelope has commitment == keccak256(pk || challenge).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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);
|
|
}
|
|
|
|
function tamperCommitment(scheme, sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
const idx = META[scheme].falcon ? 41 : 0;
|
|
b[idx] ^= 0x01;
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
// 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 AGENT_BOND = ethers.getAddress("0x32E0015F622a8719d1C380A87CE1a09bcd0cB86A"); // AereAgentBond (mainnet)
|
|
const AI_REPUTATION = ethers.getAddress("0x781ef746c08760aa854cDa4621d54db6734bfeBF"); // AereAIReputation
|
|
|
|
describe("AereAgentDID (Falcon-root DID + secp256k1 sessions + seeded fuzz)", function () {
|
|
let reg, did, owner, other, relayer, mockCode;
|
|
|
|
before(async function () {
|
|
[owner, other, relayer] = 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();
|
|
const R = await ethers.getContractFactory("AerePQCKeyRegistry");
|
|
reg = await R.deploy();
|
|
await reg.waitForDeployment();
|
|
const D = await ethers.getContractFactory("AereAgentDID");
|
|
did = await D.deploy(await reg.getAddress(), AGENT_BOND, AI_REPUTATION);
|
|
await did.waitForDeployment();
|
|
});
|
|
|
|
// 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 `caller`).
|
|
async function issueSession(
|
|
root,
|
|
{ sessionAddr, scopeHash, spendCap, expiry, rng, caller = relayer, signer = owner }
|
|
) {
|
|
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);
|
|
}
|
|
|
|
// Base expiry on the ON-CHAIN latest block timestamp (evm_increaseTime in earlier
|
|
// tests advances the node clock past wall time, so Date.now() would be stale).
|
|
async function futureTs(secs = 3600) {
|
|
const b = await ethers.provider.getBlock("latest");
|
|
return b.timestamp + secs;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Deployment & cross-links
|
|
// -----------------------------------------------------------------------
|
|
describe("deployment", function () {
|
|
it("wires the registry and exposes the read-only cross-links", async function () {
|
|
expect(await did.keyRegistry()).to.equal(await reg.getAddress());
|
|
expect(await did.agentBond()).to.equal(AGENT_BOND);
|
|
expect(await did.aiReputation()).to.equal(AI_REPUTATION);
|
|
});
|
|
|
|
it("reverts on a zero registry", async function () {
|
|
const D = await ethers.getContractFactory("AereAgentDID");
|
|
await expect(D.deploy(ethers.ZeroAddress, AGENT_BOND, AI_REPUTATION)).to.be.revertedWithCustomError(
|
|
D,
|
|
"ZeroRegistry"
|
|
);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// createAgent
|
|
// -----------------------------------------------------------------------
|
|
describe("createAgent", function () {
|
|
it("creates a DID for an ACTIVE Falcon root, callable only by the root controller", async function () {
|
|
const rng = makeRng("create");
|
|
const root = await registerFalconRoot(1, owner, rng);
|
|
// non-controller cannot create.
|
|
await expect(did.connect(other).createAgent(root.rootKeyId)).to.be.revertedWithCustomError(
|
|
did,
|
|
"NotRootController"
|
|
);
|
|
await expect(did.createAgent(root.rootKeyId)).to.emit(did, "AgentCreated");
|
|
const a = await did.getAgent(root.rootKeyId);
|
|
expect(a.controller).to.equal(owner.address);
|
|
expect(Number(a.sessionNonce)).to.equal(0);
|
|
expect(await did.agentExists(root.rootKeyId)).to.equal(true);
|
|
});
|
|
|
|
it("rejects a non-Falcon root key", async function () {
|
|
const rng = makeRng("nonfalcon");
|
|
// register an ML-DSA-44 key and try to use it as a root.
|
|
const pk = makePubKey(3, rng);
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const ch = await reg.popChallenge(owner.address, 3, pk, nonce);
|
|
const tx = await reg.registerKey(3, pk, genuine(3, pk, ch, rng));
|
|
const rc = await tx.wait();
|
|
const id = Number(
|
|
rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered").args.keyId
|
|
);
|
|
await expect(did.createAgent(id)).to.be.revertedWithCustomError(did, "RootKeyNotFalcon");
|
|
});
|
|
|
|
it("rejects a rotated-away (non-ACTIVE) root and a duplicate DID", async function () {
|
|
const rng = makeRng("dup");
|
|
const root = await registerFalconRoot(2, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
await expect(did.createAgent(root.rootKeyId)).to.be.revertedWithCustomError(did, "AgentExists");
|
|
|
|
// A separate root that we rotate away is no longer ACTIVE -> cannot create.
|
|
const root2 = await registerFalconRoot(1, owner, rng);
|
|
const np = makePubKey(1, rng);
|
|
const n = await reg.identityNonce(owner.address);
|
|
const ch = await reg.popChallenge(owner.address, 1, np, n);
|
|
await reg.rotateKey(root2.rootKeyId, 1, np, genuine(1, np, ch, rng));
|
|
await expect(did.createAgent(root2.rootKeyId)).to.be.revertedWithCustomError(did, "RootKeyNotActive");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// issueSession (Falcon-PoP gated, relayable)
|
|
// -----------------------------------------------------------------------
|
|
describe("issueSession", function () {
|
|
it("issues a session with a genuine Falcon PoP, relayable by anyone", async function () {
|
|
const rng = makeRng("issue");
|
|
const root = await registerFalconRoot(1, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
const session = ethers.Wallet.createRandom();
|
|
const scopeHash = ethers.keccak256(ethers.toUtf8Bytes("swap:read"));
|
|
const expiry = await futureTs();
|
|
|
|
const sid = await issueSession(root, {
|
|
sessionAddr: session.address,
|
|
scopeHash,
|
|
spendCap: 1000n,
|
|
expiry,
|
|
rng,
|
|
caller: relayer, // relayed, not the controller
|
|
});
|
|
expect(sid).to.equal(0);
|
|
const s = await did.getSession(sid);
|
|
expect(s.sessionAddr).to.equal(session.address);
|
|
expect(s.scopeHash).to.equal(scopeHash);
|
|
expect(s.spendCap).to.equal(1000n);
|
|
expect(s.revoked).to.equal(false);
|
|
expect(await did.isSessionValid(sid)).to.equal(true);
|
|
expect(Number((await did.getAgent(root.rootKeyId)).sessionNonce)).to.equal(1);
|
|
});
|
|
|
|
it("rejects a forged PoP and a stale/replayed PoP", async function () {
|
|
const rng = makeRng("issue-bad");
|
|
const root = await registerFalconRoot(2, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
const session = ethers.Wallet.createRandom();
|
|
const scopeHash = ethers.ZeroHash;
|
|
const expiry = await futureTs();
|
|
|
|
const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce;
|
|
const challenge = await did.sessionChallenge(root.rootKeyId, nonce, session.address, scopeHash, 500n, expiry);
|
|
const forged = tamperCommitment(2, genuine(2, root.rootPk, challenge, rng));
|
|
await expect(
|
|
did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, forged)
|
|
).to.be.revertedWithCustomError(did, "SessionAuthFailed");
|
|
|
|
// A genuine PoP for the CURRENT nonce works once...
|
|
const good = genuine(2, root.rootPk, challenge, rng);
|
|
await did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, good);
|
|
// ...and replaying it fails (nonce advanced -> different challenge).
|
|
await expect(
|
|
did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, good)
|
|
).to.be.revertedWithCustomError(did, "SessionAuthFailed");
|
|
});
|
|
|
|
it("rejects a past expiry, a zero session address, and an unknown agent", async function () {
|
|
const rng = makeRng("issue-params");
|
|
const root = await registerFalconRoot(1, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
const session = ethers.Wallet.createRandom();
|
|
const pastExpiry = 1; // in the past
|
|
const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce;
|
|
const ch = await did.sessionChallenge(root.rootKeyId, nonce, session.address, ethers.ZeroHash, 1n, pastExpiry);
|
|
const pop = genuine(1, root.rootPk, ch, rng);
|
|
await expect(
|
|
did.issueSession(root.rootKeyId, session.address, ethers.ZeroHash, 1n, pastExpiry, pop)
|
|
).to.be.revertedWithCustomError(did, "InvalidSessionParams");
|
|
// unknown agent.
|
|
await expect(
|
|
did.issueSession(999, session.address, ethers.ZeroHash, 1n, await futureTs(), "0x")
|
|
).to.be.revertedWithCustomError(did, "UnknownAgent");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// authorize (secp256k1 hot path: scope + spend cap + anti-replay)
|
|
// -----------------------------------------------------------------------
|
|
describe("authorize", function () {
|
|
let root, session, sid, scopeHash;
|
|
beforeEach(async function () {
|
|
const rng = makeRng("auth-setup");
|
|
root = await registerFalconRoot(1, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
session = ethers.Wallet.createRandom();
|
|
scopeHash = ethers.keccak256(ethers.toUtf8Bytes("trade"));
|
|
sid = await issueSession(root, {
|
|
sessionAddr: session.address,
|
|
scopeHash,
|
|
spendCap: 100n,
|
|
expiry: await futureTs(),
|
|
rng,
|
|
});
|
|
});
|
|
|
|
async function authorize(amount, actionLabel, opts = {}) {
|
|
const scope = opts.scope ?? scopeHash;
|
|
const actionHash = ethers.keccak256(ethers.toUtf8Bytes(actionLabel));
|
|
const nonce = opts.nonce ?? (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scope, actionHash, amount, nonce);
|
|
const wallet = opts.wallet ?? session;
|
|
const sig = ecdsaSign(wallet, digest);
|
|
return did.connect(relayer).authorize(sid, scope, actionHash, amount, sig);
|
|
}
|
|
|
|
it("authorizes a scoped action signed by the session key and records spend", async function () {
|
|
await expect(authorize(30n, "a1")).to.emit(did, "ActionAuthorized");
|
|
expect((await did.getSession(sid)).spent).to.equal(30n);
|
|
expect(Number((await did.getSession(sid)).actionNonce)).to.equal(1);
|
|
expect(await did.remainingSpend(sid)).to.equal(70n);
|
|
await authorize(70n, "a2");
|
|
expect((await did.getSession(sid)).spent).to.equal(100n);
|
|
expect(await did.remainingSpend(sid)).to.equal(0n);
|
|
});
|
|
|
|
it("enforces the spend cap (cumulative)", async function () {
|
|
await authorize(60n, "a1");
|
|
await expect(authorize(41n, "a2")).to.be.revertedWithCustomError(did, "SessionSpendExceeded");
|
|
// exactly hitting the cap is allowed.
|
|
await authorize(40n, "a3");
|
|
expect((await did.getSession(sid)).spent).to.equal(100n);
|
|
});
|
|
|
|
it("enforces scope: a non-matching scope tag reverts", async function () {
|
|
const wrongScope = ethers.keccak256(ethers.toUtf8Bytes("withdraw"));
|
|
await expect(authorize(1n, "x", { scope: wrongScope })).to.be.revertedWithCustomError(
|
|
did,
|
|
"SessionScopeViolation"
|
|
);
|
|
});
|
|
|
|
it("rejects a signature by a non-session key", async function () {
|
|
const stranger = ethers.Wallet.createRandom();
|
|
await expect(authorize(1n, "x", { wallet: stranger })).to.be.revertedWithCustomError(did, "SessionSigInvalid");
|
|
});
|
|
|
|
it("is anti-replay: reusing a consumed action nonce reverts", async function () {
|
|
// Sign at nonce 0, submit -> nonce becomes 1. Re-submitting the SAME signature
|
|
// (still nonce 0) now recovers a wrong digest -> SessionSigInvalid.
|
|
const actionHash = ethers.keccak256(ethers.toUtf8Bytes("replay"));
|
|
const digest0 = await did.actionDigest(sid, scopeHash, actionHash, 10n, 0);
|
|
const sig0 = ecdsaSign(session, digest0);
|
|
await did.authorize(sid, scopeHash, actionHash, 10n, sig0);
|
|
expect(Number((await did.getSession(sid)).actionNonce)).to.equal(1);
|
|
await expect(did.authorize(sid, scopeHash, actionHash, 10n, sig0)).to.be.revertedWithCustomError(
|
|
did,
|
|
"SessionSigInvalid"
|
|
);
|
|
});
|
|
|
|
it("rejects a malformed (wrong-length) signature", async function () {
|
|
const actionHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
await expect(did.authorize(sid, scopeHash, actionHash, 1n, "0x1234")).to.be.revertedWithCustomError(
|
|
did,
|
|
"SessionSigInvalid"
|
|
);
|
|
});
|
|
|
|
it("reverts on an unknown sessionId", async function () {
|
|
await expect(did.authorize(999, scopeHash, ethers.ZeroHash, 1n, "0x" + "00".repeat(65)))
|
|
.to.be.revertedWithCustomError(did, "UnknownSession");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// revocation & expiry & root-lifecycle cross-check
|
|
// -----------------------------------------------------------------------
|
|
describe("session validity: revoke / expiry / root lifecycle", function () {
|
|
let root, session, sid, scopeHash;
|
|
beforeEach(async function () {
|
|
const rng = makeRng("valid-setup");
|
|
root = await registerFalconRoot(1, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
session = ethers.Wallet.createRandom();
|
|
scopeHash = ethers.keccak256(ethers.toUtf8Bytes("s"));
|
|
sid = await issueSession(root, {
|
|
sessionAddr: session.address,
|
|
scopeHash,
|
|
spendCap: 100n,
|
|
expiry: await futureTs(1000),
|
|
rng,
|
|
});
|
|
this.rng = rng;
|
|
});
|
|
|
|
async function authorizeOnce(amount) {
|
|
const actionHash = ethers.keccak256(ethers.toUtf8Bytes("act"));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scopeHash, actionHash, amount, nonce);
|
|
return did.authorize(sid, scopeHash, actionHash, amount, ecdsaSign(session, digest));
|
|
}
|
|
|
|
it("controller can fast-revoke (cheap ECDSA); a stranger cannot", async function () {
|
|
await expect(did.connect(other).revokeSession(sid)).to.be.revertedWithCustomError(did, "NotSessionRevoker");
|
|
await expect(did.connect(owner).revokeSession(sid)).to.emit(did, "SessionRevoked");
|
|
expect(await did.isSessionValid(sid)).to.equal(false);
|
|
await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid");
|
|
// revoke is idempotent (no-op success on a second call).
|
|
await did.connect(owner).revokeSession(sid);
|
|
});
|
|
|
|
it("a session past its expiry is invalid", async function () {
|
|
expect(await did.isSessionValid(sid)).to.equal(true);
|
|
await network.provider.send("evm_increaseTime", [2000]);
|
|
await network.provider.send("evm_mine", []);
|
|
expect(await did.isSessionValid(sid)).to.equal(false);
|
|
await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid");
|
|
});
|
|
|
|
it("REVOKING the Falcon root invalidates all its sessions", async function () {
|
|
expect(await did.isSessionValid(sid)).to.equal(true);
|
|
await reg.connect(owner).revokeKey(root.rootKeyId);
|
|
expect(await did.isSessionValid(sid)).to.equal(false);
|
|
await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid");
|
|
});
|
|
|
|
it("ROTATING the Falcon root (root no longer ACTIVE) invalidates its sessions", async function () {
|
|
const rng = this.rng;
|
|
const np = makePubKey(1, rng);
|
|
const n = await reg.identityNonce(owner.address);
|
|
const ch = await reg.popChallenge(owner.address, 1, np, n);
|
|
await reg.rotateKey(root.rootKeyId, 1, np, genuine(1, np, ch, rng));
|
|
expect(await did.isSessionValid(sid)).to.equal(false);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Seeded invariant fuzz
|
|
// -----------------------------------------------------------------------
|
|
describe("seeded invariant fuzz", function () {
|
|
it("scope + spend-cap + anti-replay + revocation hold across random rounds", async function () {
|
|
const rng = makeRng("fuzz-did-v1");
|
|
const scheme = 2;
|
|
const root = await registerFalconRoot(scheme, owner, rng);
|
|
await did.createAgent(root.rootKeyId);
|
|
|
|
const ROUNDS = 24;
|
|
for (let r = 0; r < ROUNDS; r++) {
|
|
const session = ethers.Wallet.createRandom();
|
|
const scopeHash = ethers.hexlify(rng.bytes(32));
|
|
const cap = BigInt(50 + rng.int(200));
|
|
const sid = await issueSession(root, {
|
|
sessionAddr: session.address,
|
|
scopeHash,
|
|
spendCap: cap,
|
|
expiry: await futureTs(5000),
|
|
rng,
|
|
});
|
|
|
|
let spent = 0n;
|
|
// A few genuine scoped actions within cap succeed and accumulate.
|
|
const k = 1 + rng.int(3);
|
|
for (let i = 0; i < k; i++) {
|
|
const remaining = cap - spent;
|
|
if (remaining === 0n) break;
|
|
const amount = BigInt(1 + rng.int(Number(remaining > 40n ? 40n : remaining)));
|
|
const actionHash = ethers.hexlify(rng.bytes(32));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scopeHash, actionHash, amount, nonce);
|
|
await did.authorize(sid, scopeHash, actionHash, amount, ecdsaSign(session, digest));
|
|
spent += amount;
|
|
expect((await did.getSession(sid)).spent).to.equal(spent);
|
|
}
|
|
|
|
// (a) wrong scope always reverts.
|
|
{
|
|
const badScope = ethers.hexlify(rng.bytes(32));
|
|
const actionHash = ethers.hexlify(rng.bytes(32));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, badScope, actionHash, 1n, nonce);
|
|
await expect(
|
|
did.authorize(sid, badScope, actionHash, 1n, ecdsaSign(session, digest))
|
|
).to.be.revertedWithCustomError(did, "SessionScopeViolation");
|
|
}
|
|
// (b) exceeding the cap always reverts.
|
|
{
|
|
const over = cap - spent + 1n;
|
|
const actionHash = ethers.hexlify(rng.bytes(32));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scopeHash, actionHash, over, nonce);
|
|
await expect(
|
|
did.authorize(sid, scopeHash, actionHash, over, ecdsaSign(session, digest))
|
|
).to.be.revertedWithCustomError(did, "SessionSpendExceeded");
|
|
}
|
|
// (c) a stranger key never authorizes. Use amount 0 so the spend check (which
|
|
// runs before the signature check) always passes, isolating the SIG failure —
|
|
// the session's cap may already be exhausted this round.
|
|
{
|
|
const stranger = ethers.Wallet.createRandom();
|
|
const actionHash = ethers.hexlify(rng.bytes(32));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scopeHash, actionHash, 0n, nonce);
|
|
await expect(
|
|
did.authorize(sid, scopeHash, actionHash, 0n, ecdsaSign(stranger, digest))
|
|
).to.be.revertedWithCustomError(did, "SessionSigInvalid");
|
|
}
|
|
|
|
// Randomly revoke ~1/3 of sessions; a revoked session never authorizes again.
|
|
if (rng.int(3) === 0) {
|
|
await did.connect(owner).revokeSession(sid);
|
|
expect(await did.isSessionValid(sid)).to.equal(false);
|
|
const actionHash = ethers.hexlify(rng.bytes(32));
|
|
const nonce = (await did.getSession(sid)).actionNonce;
|
|
const digest = await did.actionDigest(sid, scopeHash, actionHash, 1n, nonce);
|
|
await expect(
|
|
did.authorize(sid, scopeHash, actionHash, 1n, ecdsaSign(session, digest))
|
|
).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid");
|
|
}
|
|
}
|
|
|
|
// sessionsOf reflects everything issued.
|
|
const list = await did.sessionsOf(root.rootKeyId);
|
|
expect(list.length).to.equal(ROUNDS);
|
|
expect(Number(await did.sessionCount())).to.equal(ROUNDS);
|
|
});
|
|
});
|
|
});
|