aere-contracts/test/AereComputeMarketV3.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

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