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.
578 lines
28 KiB
JavaScript
578 lines
28 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AereComputeMarketV3: proof-carrying DePIN compute marketplace.
|
|
//
|
|
// The three verification modes, the Falcon-512 post-quantum settlement gate, and
|
|
// the escrow-solvency invariant are exercised against faithful test doubles:
|
|
//
|
|
// * MockSp1Verifier - the deployed SP1 gateway stand-in. verifyProof
|
|
// REVERTS on an invalid proof (exact production semantics), so a ZK_VERIFIED
|
|
// job cannot pay without a valid proof.
|
|
// * MockPQCPrecompile(1) - installed at 0x0AE1 via hardhat_setCode. Parses
|
|
// the Falcon-512 SPEC offsets and accepts iff the embedded commitment equals
|
|
// keccak256(pk || message). The market builds the precompile input itself, so a
|
|
// positive here proves the market's real precompile-calling path is correct.
|
|
// * MockERC20Lending - an allowlisted ERC-20 reward asset.
|
|
//
|
|
// Real Falcon crypto and real SP1 proving are external [MEASURE]; the wire
|
|
// encoding of the Falcon input is separately KAT-proven in AerePQCMessageVerifier
|
|
// and against the live mainnet precompile.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const AE1 = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512 precompile
|
|
const ZERO = ethers.ZeroAddress;
|
|
|
|
const Mode = { REPLAY: 0, ZK_VERIFIED: 1, OPTIMISTIC: 2 };
|
|
const Status = {
|
|
None: 0, Open: 1, Claimed: 2, Submitted: 3, Disputed: 4, Paid: 5, Refunded: 6, Slashed: 7,
|
|
};
|
|
|
|
const SETTLEMENT_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AereComputeMarketV3.v1.settlement"));
|
|
|
|
// Deterministic seeded RNG (keccak hash-chain), 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;
|
|
},
|
|
};
|
|
}
|
|
|
|
// A well-formed Falcon-512 public key (897 bytes, header 0x09).
|
|
function falconPubKey(rng) {
|
|
const raw = rng.bytes(897);
|
|
raw[0] = 0x09;
|
|
return ethers.hexlify(raw);
|
|
}
|
|
|
|
// Build a Falcon envelope the mock treats as a genuine signature by `pk` over the
|
|
// 32-byte `message`: nonce(40) || esig ; esig = 0x29 || keccak256(pk || message).
|
|
function falconMockEnvelope(pk, message32, rng) {
|
|
const commitment = ethers.keccak256(ethers.concat([pk, message32]));
|
|
const nonce = rng.bytes(40);
|
|
const esig = ethers.concat([new Uint8Array([0x29]), commitment]);
|
|
return ethers.hexlify(ethers.concat([nonce, esig]));
|
|
}
|
|
|
|
function abi() {
|
|
return ethers.AbiCoder.defaultAbiCoder();
|
|
}
|
|
|
|
// Reproduce settlementDigest(id, provider, token, amount, resultHash) off-chain.
|
|
function settlementDigest(chainId, market, id, provider, token, amount, resultHash) {
|
|
return ethers.keccak256(
|
|
abi().encode(
|
|
["bytes32", "uint256", "address", "uint256", "address", "address", "uint256", "bytes32"],
|
|
[SETTLEMENT_DOMAIN, chainId, market, id, provider, token, amount, resultHash]
|
|
)
|
|
);
|
|
}
|
|
|
|
describe("AereComputeMarketV3", function () {
|
|
let market, sp1, token;
|
|
let deployer, requester, provider, challenger, arbiter, governance, other;
|
|
let chainId;
|
|
let falconMockCode;
|
|
|
|
before(async function () {
|
|
// Compile + cache the Falcon-512 mock precompile code once.
|
|
const M = await ethers.getContractFactory("MockPQCPrecompile");
|
|
const f = await M.deploy(1); // scheme 1 = Falcon-512
|
|
await f.waitForDeployment();
|
|
falconMockCode = await ethers.provider.getCode(await f.getAddress());
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
[deployer, requester, provider, challenger, arbiter, governance, other] = await ethers.getSigners();
|
|
chainId = (await ethers.provider.getNetwork()).chainId;
|
|
|
|
// Install the faithful Falcon-512 mock at the live precompile address.
|
|
await network.provider.send("hardhat_setCode", [AE1, falconMockCode]);
|
|
|
|
sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
|
|
await sp1.waitForDeployment();
|
|
|
|
token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("USD Coin e", "USDC.e", 6);
|
|
await token.waitForDeployment();
|
|
|
|
market = await (await ethers.getContractFactory("AereComputeMarketV3")).deploy(
|
|
await sp1.getAddress(),
|
|
arbiter.address,
|
|
governance.address
|
|
);
|
|
await market.waitForDeployment();
|
|
});
|
|
|
|
// Assert the escrow-solvency invariant for both assets: contract balance >= liabilities.
|
|
async function assertSolvent() {
|
|
expect(await market.isSolvent(ZERO), "native solvency").to.equal(true);
|
|
expect(await market.isSolvent(await token.getAddress()), "token solvency").to.equal(true);
|
|
// Cross-check the actual balances against the tracked liabilities.
|
|
const mAddr = await market.getAddress();
|
|
const nativeBal = await ethers.provider.getBalance(mAddr);
|
|
const nativeLia = await market.totalLiabilities(ZERO);
|
|
expect(nativeBal >= nativeLia, "native bal>=liab").to.equal(true);
|
|
const tokBal = await token.balanceOf(mAddr);
|
|
const tokLia = await market.totalLiabilities(await token.getAddress());
|
|
expect(tokBal >= tokLia, "token bal>=liab").to.equal(true);
|
|
}
|
|
|
|
function baseParams(overrides) {
|
|
return Object.assign(
|
|
{
|
|
token: ZERO,
|
|
reward: ethers.parseEther("10"),
|
|
providerBond: ethers.parseEther("2"),
|
|
specHash: ethers.keccak256(ethers.toUtf8Bytes("spec:matmul-128x128")),
|
|
zkProgramVKey: ethers.ZeroHash,
|
|
mode: Mode.OPTIMISTIC,
|
|
deadline: 0, // filled below
|
|
challengeWindow: 3600,
|
|
pqcSettlement: false,
|
|
falconPubKey: "0x",
|
|
},
|
|
overrides || {}
|
|
);
|
|
}
|
|
|
|
async function post(p, valueOverride) {
|
|
const now = (await ethers.provider.getBlock("latest")).timestamp;
|
|
const params = baseParams(p);
|
|
if (params.deadline === 0) params.deadline = now + 7 * 24 * 3600;
|
|
const id = await market.jobCount();
|
|
const value = valueOverride !== undefined ? valueOverride : (params.token === ZERO ? params.reward : 0n);
|
|
await market.connect(requester).postJob(params, { value });
|
|
return id;
|
|
}
|
|
|
|
// =========================================================================
|
|
// 0. Construction guards (L1 fail-open)
|
|
// =========================================================================
|
|
describe("construction guards", function () {
|
|
// L1 (LOW) fix: verifyProof returns void, so a code-less ZK verifier makes the call succeed
|
|
// silently and a ZK_VERIFIED job would pay with NO valid proof (fund loss). The constructor must
|
|
// reject a code-less verifier, exactly like the sibling AereFinalityCertificateVerifier.
|
|
it("rejects a code-less ZK verifier at construction (L1 fail-open guard)", async function () {
|
|
const F = await ethers.getContractFactory("AereComputeMarketV3");
|
|
// A non-zero EOA has no code.
|
|
await expect(
|
|
F.deploy(other.address, arbiter.address, governance.address)
|
|
).to.be.revertedWithCustomError(market, "VerifierHasNoCode");
|
|
// A zero verifier still reverts on the earlier ZeroAddress check.
|
|
await expect(
|
|
F.deploy(ZERO, arbiter.address, governance.address)
|
|
).to.be.revertedWithCustomError(market, "ZeroAddress");
|
|
// Control: the real (code-bearing) mock gateway deploys fine.
|
|
const ok = await F.deploy(await sp1.getAddress(), arbiter.address, governance.address);
|
|
await ok.waitForDeployment();
|
|
expect(await ok.ZK_VERIFIER()).to.equal(await sp1.getAddress());
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 1. ZK_VERIFIED: pay only on a valid proof, reject an invalid one
|
|
// =========================================================================
|
|
describe("ZK_VERIFIED mode", function () {
|
|
const vkey = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-verifiable-compute-v1"));
|
|
|
|
async function postZK() {
|
|
return post({
|
|
mode: Mode.ZK_VERIFIED,
|
|
reward: ethers.parseEther("10"),
|
|
providerBond: 0n,
|
|
zkProgramVKey: vkey,
|
|
challengeWindow: 0,
|
|
});
|
|
}
|
|
|
|
it("pays the provider on a valid proof, and only then", async function () {
|
|
const id = await postZK();
|
|
await assertSolvent();
|
|
await market.connect(provider).claimJob(id, { value: 0 });
|
|
await assertSolvent();
|
|
|
|
const specHash = (await market.getJob(id)).specHash;
|
|
const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:0xC0FFEE"));
|
|
const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]);
|
|
const proof = "0x1122334455667788";
|
|
|
|
// Register this exact (vkey, publicValues, proof) as valid in the gateway.
|
|
const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
|
|
await sp1.setValid(marker, true);
|
|
|
|
const before = await ethers.provider.getBalance(provider.address);
|
|
// A third party submits on the provider's behalf? No: only the job provider may submit.
|
|
await expect(market.connect(provider).submitResultZK(id, publicValues, proof, "0x"))
|
|
.to.emit(market, "ResultVerifiedZK")
|
|
.and.to.emit(market, "JobPaid");
|
|
const after = await ethers.provider.getBalance(provider.address);
|
|
|
|
const j = await market.getJob(id);
|
|
expect(Number(j.status)).to.equal(Status.Paid);
|
|
expect(j.resultHash).to.equal(resultHash);
|
|
// Provider received ~10 AERE (minus its own gas for the submit tx).
|
|
expect(after > before).to.equal(true);
|
|
await assertSolvent();
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(0n);
|
|
});
|
|
|
|
it("rejects an invalid proof and pays nothing (fail-closed)", async function () {
|
|
const id = await postZK();
|
|
await market.connect(provider).claimJob(id, { value: 0 });
|
|
|
|
const specHash = (await market.getJob(id)).specHash;
|
|
const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:fabricated"));
|
|
const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]);
|
|
const proof = "0xdeadbeef"; // never registered as valid
|
|
|
|
await expect(
|
|
market.connect(provider).submitResultZK(id, publicValues, proof, "0x")
|
|
).to.be.revertedWithCustomError(sp1, "MockInvalidProof");
|
|
|
|
const j = await market.getJob(id);
|
|
expect(Number(j.status)).to.equal(Status.Claimed); // unchanged, no payment
|
|
await assertSolvent();
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("10"));
|
|
});
|
|
|
|
it("rejects a proof whose public values bind a different spec", async function () {
|
|
const id = await postZK();
|
|
await market.connect(provider).claimJob(id, { value: 0 });
|
|
|
|
const wrongSpec = ethers.keccak256(ethers.toUtf8Bytes("spec:some-other-job"));
|
|
const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result"));
|
|
const publicValues = abi().encode(["bytes32", "bytes32"], [wrongSpec, resultHash]);
|
|
const proof = "0x1234";
|
|
const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
|
|
await sp1.setValid(marker, true); // even a "valid" proof for the wrong spec must be rejected
|
|
|
|
await expect(
|
|
market.connect(provider).submitResultZK(id, publicValues, proof, "0x")
|
|
).to.be.revertedWithCustomError(market, "BadPublicValues");
|
|
await assertSolvent();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 2. OPTIMISTIC mode: finalize after the window; dispute slashes on a bad result
|
|
// =========================================================================
|
|
describe("OPTIMISTIC mode", function () {
|
|
it("post + escrow + claim + submit + finalize pays provider and returns its bond", async function () {
|
|
const id = await post({ mode: Mode.OPTIMISTIC });
|
|
await assertSolvent();
|
|
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await assertSolvent();
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("12")); // 10 reward + 2 bond
|
|
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("optimistic-result"));
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted);
|
|
|
|
// Cannot finalize before the window closes.
|
|
await expect(market.connect(other).finalize(id, "0x")).to.be.revertedWithCustomError(market, "WindowOpen");
|
|
|
|
await network.provider.send("evm_increaseTime", [3601]);
|
|
await network.provider.send("evm_mine", []);
|
|
|
|
const before = await ethers.provider.getBalance(provider.address);
|
|
// A third party finalizes so the provider pays no gas: delta is exactly reward + bond.
|
|
await expect(market.connect(other).finalize(id, "0x")).to.emit(market, "JobPaid");
|
|
const after = await ethers.provider.getBalance(provider.address);
|
|
expect(after - before).to.equal(ethers.parseEther("12"));
|
|
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(0n);
|
|
await assertSolvent();
|
|
});
|
|
|
|
it("a successful dispute slashes the provider bond to the challenger and refunds the requester", async function () {
|
|
const id = await post({ mode: Mode.OPTIMISTIC });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("bad-result"));
|
|
await assertSolvent();
|
|
|
|
// Challenger disputes within the window, posting a matching bond.
|
|
await expect(
|
|
market.connect(challenger).dispute(id, ethers.ZeroHash, { value: ethers.parseEther("2") })
|
|
).to.emit(market, "JobDisputed");
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Disputed);
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("14")); // 10 + 2 + 2
|
|
await assertSolvent();
|
|
|
|
// Only the arbiter can resolve.
|
|
await expect(market.connect(other).resolveDispute(id, false)).to.be.revertedWithCustomError(market, "NotArbiter");
|
|
|
|
const reqBefore = await ethers.provider.getBalance(requester.address);
|
|
const chBefore = await ethers.provider.getBalance(challenger.address);
|
|
|
|
// Arbiter upholds the dispute (provider lost). Resolve is sent by a neutral account so the
|
|
// requester/challenger balance deltas are exact.
|
|
await market.connect(arbiter).resolveDispute(id, false);
|
|
|
|
const reqAfter = await ethers.provider.getBalance(requester.address);
|
|
const chAfter = await ethers.provider.getBalance(challenger.address);
|
|
|
|
// Requester refunded the full reward.
|
|
expect(reqAfter - reqBefore).to.equal(ethers.parseEther("10"));
|
|
// Challenger gets its own bond back (2) plus the slashed provider bond (2) = 4.
|
|
expect(chAfter - chBefore).to.equal(ethers.parseEther("4"));
|
|
|
|
const j = await market.getJob(id);
|
|
expect(Number(j.status)).to.equal(Status.Slashed);
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(0n);
|
|
await assertSolvent();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 3. REPLAY mode: dispute path (deterministic recompute on record)
|
|
// =========================================================================
|
|
describe("REPLAY mode", function () {
|
|
it("records the challenger's recomputed hash; arbiter can uphold the provider", async function () {
|
|
const id = await post({ mode: Mode.REPLAY, challengeWindow: 1800, providerBond: ethers.parseEther("1") });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") });
|
|
const result = ethers.toUtf8Bytes("deterministic-output-42");
|
|
await market.connect(provider).submitResult(id, result);
|
|
const submittedHash = ethers.keccak256(result);
|
|
expect((await market.getJob(id)).resultHash).to.equal(submittedHash);
|
|
await assertSolvent();
|
|
|
|
// A challenger claims a different recomputation and disputes.
|
|
const challengerHash = ethers.keccak256(ethers.toUtf8Bytes("challenger-claims-this"));
|
|
await market.connect(challenger).dispute(id, challengerHash, { value: ethers.parseEther("1") });
|
|
expect(await market.challengeResultHashOf(id)).to.equal(challengerHash);
|
|
await assertSolvent();
|
|
|
|
// Off-chain re-run agrees with the provider: arbiter upholds the provider.
|
|
const provBefore = await ethers.provider.getBalance(provider.address);
|
|
await market.connect(arbiter).resolveDispute(id, true);
|
|
const provAfter = await ethers.provider.getBalance(provider.address);
|
|
|
|
// Provider gets reward (10) + own bond back (1) + challenger's forfeited bond (1) = 12.
|
|
expect(provAfter - provBefore).to.equal(ethers.parseEther("12"));
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(0n);
|
|
await assertSolvent();
|
|
});
|
|
|
|
it("re-run verification off-chain matches the on-chain resultHash for a good result", async function () {
|
|
const id = await post({ mode: Mode.REPLAY, challengeWindow: 60, providerBond: ethers.parseEther("1") });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") });
|
|
const result = ethers.toUtf8Bytes("f(input)=output");
|
|
await market.connect(provider).submitResult(id, result);
|
|
|
|
// Anyone re-runs f(input) locally and checks determinism against the on-chain anchor.
|
|
const onChain = (await market.getJob(id)).resultHash;
|
|
expect(onChain).to.equal(ethers.keccak256(result));
|
|
|
|
await network.provider.send("evm_increaseTime", [61]);
|
|
await network.provider.send("evm_mine", []);
|
|
await market.connect(other).finalize(id, "0x");
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
|
|
await assertSolvent();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 4. ERC-20 reward asset (allowlist)
|
|
// =========================================================================
|
|
describe("allowlisted ERC-20 reward", function () {
|
|
it("escrows and settles an allowlisted token; rejects a non-allowlisted one", async function () {
|
|
const tokenAddr = await token.getAddress();
|
|
await token.mint(requester.address, ethers.parseUnits("1000", 6));
|
|
await token.connect(requester).approve(await market.getAddress(), ethers.parseUnits("1000", 6));
|
|
|
|
// Not allowlisted yet.
|
|
await expect(
|
|
post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n)
|
|
).to.be.revertedWithCustomError(market, "TokenNotAllowed");
|
|
|
|
// Governance allowlists it (governance cannot move funds).
|
|
await expect(market.connect(other).setTokenAllowed(tokenAddr, true)).to.be.revertedWithCustomError(market, "NotGovernance");
|
|
await market.connect(governance).setTokenAllowed(tokenAddr, true);
|
|
|
|
const id = await post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n);
|
|
await assertSolvent();
|
|
expect(await market.totalLiabilities(tokenAddr)).to.equal(ethers.parseUnits("100", 6));
|
|
|
|
// Bond is native AERE even though the reward is an ERC-20.
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("erc20-result"));
|
|
await network.provider.send("evm_increaseTime", [3601]);
|
|
await network.provider.send("evm_mine", []);
|
|
|
|
const before = await token.balanceOf(provider.address);
|
|
await market.connect(other).finalize(id, "0x");
|
|
const after = await token.balanceOf(provider.address);
|
|
expect(after - before).to.equal(ethers.parseUnits("100", 6));
|
|
expect(await market.totalLiabilities(tokenAddr)).to.equal(0n);
|
|
await assertSolvent();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 5. Falcon-512 post-quantum settlement authorization
|
|
// =========================================================================
|
|
describe("PQC settlement (Falcon-512 via 0x0AE1)", function () {
|
|
it("finalizes only with a valid Falcon-512 authorization; a tampered one fails closed", async function () {
|
|
const rng = makeRng("pqc-settle");
|
|
const pk = falconPubKey(rng);
|
|
|
|
const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("pqc-guarded-result"));
|
|
await assertSolvent();
|
|
|
|
await network.provider.send("evm_increaseTime", [3601]);
|
|
await network.provider.send("evm_mine", []);
|
|
|
|
const j = await market.getJob(id);
|
|
const digest = settlementDigest(
|
|
chainId, await market.getAddress(), id, provider.address, ZERO, j.reward, j.resultHash
|
|
);
|
|
// Sanity: our off-chain digest matches the contract's.
|
|
expect(await market.settlementDigest(id, provider.address, ZERO, j.reward, j.resultHash)).to.equal(digest);
|
|
|
|
const goodSig = falconMockEnvelope(pk, digest, rng);
|
|
|
|
// Tampered signature: flip a byte inside the commitment. Payout reverts, nothing paid.
|
|
const bad = ethers.getBytes(goodSig);
|
|
bad[41] ^= 0x01; // esig = nonce(40) || 0x29 || commitment ; byte 41 is inside the commitment
|
|
await expect(
|
|
market.connect(other).finalize(id, ethers.hexlify(bad))
|
|
).to.be.revertedWithCustomError(market, "NotPQCAuthorized");
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted); // unchanged
|
|
await assertSolvent();
|
|
|
|
// Genuine Falcon-512 authorization: payout succeeds.
|
|
const before = await ethers.provider.getBalance(provider.address);
|
|
await expect(market.connect(other).finalize(id, goodSig))
|
|
.to.emit(market, "PQCSettlementAuthorized")
|
|
.and.to.emit(market, "JobPaid");
|
|
const after = await ethers.provider.getBalance(provider.address);
|
|
expect(after - before).to.equal(ethers.parseEther("12")); // reward 10 + bond 2
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
|
|
await assertSolvent();
|
|
});
|
|
|
|
it("rejects a Falcon authorization for the wrong amount (digest binds the payout)", async function () {
|
|
const rng = makeRng("pqc-amount");
|
|
const pk = falconPubKey(rng);
|
|
const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r"));
|
|
await network.provider.send("evm_increaseTime", [3601]);
|
|
await network.provider.send("evm_mine", []);
|
|
|
|
const j = await market.getJob(id);
|
|
// Sign a digest for the WRONG amount (5 AERE instead of 10).
|
|
const wrongDigest = settlementDigest(
|
|
chainId, await market.getAddress(), id, provider.address, ZERO, ethers.parseEther("5"), j.resultHash
|
|
);
|
|
const wrongSig = falconMockEnvelope(pk, wrongDigest, rng);
|
|
await expect(
|
|
market.connect(other).finalize(id, wrongSig)
|
|
).to.be.revertedWithCustomError(market, "NotPQCAuthorized");
|
|
await assertSolvent();
|
|
});
|
|
|
|
it("rejects a PQC signature supplied for a non-PQC job", async function () {
|
|
const rng = makeRng("nonpqc");
|
|
const pk = falconPubKey(rng);
|
|
const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: false });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r"));
|
|
await network.provider.send("evm_increaseTime", [3601]);
|
|
await network.provider.send("evm_mine", []);
|
|
const junkSig = falconMockEnvelope(pk, ethers.ZeroHash, rng);
|
|
await expect(
|
|
market.connect(other).finalize(id, junkSig)
|
|
).to.be.revertedWithCustomError(market, "PQCSigNotAllowed");
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 6. Escrow-solvency invariant across a mixed workload, and no-double-pay
|
|
// =========================================================================
|
|
describe("escrow solvency and no double-pay", function () {
|
|
it("holds the solvency invariant across many concurrent jobs and terminal states", async function () {
|
|
const vkey = ethers.keccak256(ethers.toUtf8Bytes("vk"));
|
|
// A batch of jobs in various modes and states.
|
|
const idA = await post({ mode: Mode.OPTIMISTIC });
|
|
const idB = await post({ mode: Mode.REPLAY, challengeWindow: 100, providerBond: ethers.parseEther("1") });
|
|
const idC = await post({ mode: Mode.ZK_VERIFIED, providerBond: 0n, zkProgramVKey: vkey, challengeWindow: 0 });
|
|
const idD = await post({ mode: Mode.OPTIMISTIC }); // will be cancelled while Open
|
|
await assertSolvent();
|
|
|
|
await market.connect(provider).claimJob(idA, { value: ethers.parseEther("2") });
|
|
await market.connect(provider).claimJob(idB, { value: ethers.parseEther("1") });
|
|
await market.connect(provider).claimJob(idC, { value: 0 });
|
|
await assertSolvent();
|
|
|
|
// Cancel D while Open.
|
|
await market.connect(requester).cancelJob(idD);
|
|
await assertSolvent();
|
|
|
|
// A submits + disputed + slashed.
|
|
await market.connect(provider).submitResult(idA, ethers.toUtf8Bytes("A"));
|
|
await market.connect(challenger).dispute(idA, ethers.ZeroHash, { value: ethers.parseEther("2") });
|
|
await market.connect(arbiter).resolveDispute(idA, false);
|
|
await assertSolvent();
|
|
|
|
// B submits + finalized.
|
|
await market.connect(provider).submitResult(idB, ethers.toUtf8Bytes("B"));
|
|
await network.provider.send("evm_increaseTime", [101]);
|
|
await network.provider.send("evm_mine", []);
|
|
await market.connect(other).finalize(idB, "0x");
|
|
await assertSolvent();
|
|
|
|
// C settles via ZK.
|
|
const specHashC = (await market.getJob(idC)).specHash;
|
|
const rhC = ethers.keccak256(ethers.toUtf8Bytes("C-result"));
|
|
const pvC = abi().encode(["bytes32", "bytes32"], [specHashC, rhC]);
|
|
const proofC = "0xabcdef01";
|
|
await sp1.setValid(ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, pvC, proofC])), true);
|
|
await market.connect(provider).submitResultZK(idC, pvC, proofC, "0x");
|
|
await assertSolvent();
|
|
|
|
// All liabilities cleared.
|
|
expect(await market.totalLiabilities(ZERO)).to.equal(0n);
|
|
|
|
// No double-pay: finalizing/settling a terminal job reverts.
|
|
await expect(market.connect(other).finalize(idB, "0x")).to.be.revertedWithCustomError(market, "WrongStatus");
|
|
await expect(market.connect(provider).submitResultZK(idC, pvC, proofC, "0x")).to.be.revertedWithCustomError(market, "WrongStatus");
|
|
await expect(market.connect(arbiter).resolveDispute(idA, true)).to.be.revertedWithCustomError(market, "WrongStatus");
|
|
});
|
|
|
|
it("reclaims a claimed job that blew its deadline, slashing the provider bond to the requester", async function () {
|
|
const now = (await ethers.provider.getBlock("latest")).timestamp;
|
|
const id = await post({ mode: Mode.OPTIMISTIC, deadline: now + 100 });
|
|
await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
|
|
await assertSolvent();
|
|
|
|
await network.provider.send("evm_increaseTime", [101]);
|
|
await network.provider.send("evm_mine", []);
|
|
|
|
const before = await ethers.provider.getBalance(requester.address);
|
|
const tx = await market.connect(requester).reclaimExpired(id);
|
|
const rc = await tx.wait();
|
|
const gas = rc.gasUsed * rc.gasPrice;
|
|
const after = await ethers.provider.getBalance(requester.address);
|
|
// Requester recovers reward (10) + slashed provider bond (2), minus gas.
|
|
expect(after - before + gas).to.equal(ethers.parseEther("12"));
|
|
expect(Number((await market.getJob(id)).status)).to.equal(Status.Refunded);
|
|
await assertSolvent();
|
|
});
|
|
});
|
|
});
|