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.
607 lines
31 KiB
Solidity
607 lines
31 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import {ISP1Verifier} from "../zkverify/ISP1Verifier.sol";
|
|
|
|
/**
|
|
* @title AereComputeMarketV3, the proof-carrying DePIN compute marketplace for Aere Network
|
|
*
|
|
* @notice The unified verification and settlement rail for decentralized compute (DePIN for AI
|
|
* and verifiable compute). A REQUESTER posts a job (a spec-hash commitment, a reward held
|
|
* in escrow, a deadline, and a required VERIFICATION MODE); a PROVIDER claims it, runs the
|
|
* work OFF-CHAIN, and submits a result whose acceptance depends on the job's mode. A
|
|
* provider is paid only against verifiable evidence, never on trust alone.
|
|
*
|
|
* WHAT V3 UNIFIES OVER V1 / V2 (V2 = the coordination layer with a single optimistic +
|
|
* Foundation-arbitration path). V3 makes the VERIFICATION RAIL a first-class, per-job
|
|
* choice and adds a post-quantum settlement gate.
|
|
*
|
|
* THREE VERIFICATION MODES (per job, chosen by the requester).
|
|
* REPLAY. The result is a deterministic function of the input. The full result bytes are
|
|
* emitted on submission so any watcher can re-run the spec and check keccak256(localResult)
|
|
* against the on-chain resultHash. Accepted after a challenge window unless a challenger
|
|
* posts a matching bond and disputes; the dispute (with the challenger's recomputed
|
|
* resultHash on record) is resolved by the configured arbiter.
|
|
* ZK_VERIFIED. The result carries a zero-knowledge proof verified ON-CHAIN through the
|
|
* existing deployed SP1 gateway verifier. Payment is released in the SAME transaction as
|
|
* the proof, and ONLY if the proof verifies. There is no other code path that pays a
|
|
* ZK_VERIFIED job, so it is impossible to pay one without a valid proof.
|
|
* OPTIMISTIC. The result is accepted after a challenge window unless a challenger posts a
|
|
* bond and disputes. A successful dispute slashes the PROVIDER bond to the CHALLENGER and
|
|
* refunds the reward to the requester.
|
|
*
|
|
* POST-QUANTUM SETTLEMENT PATH (optional per job). If a job sets pqcSettlement, the final
|
|
* release of funds to the provider additionally requires a Falcon-512 signature by the job's
|
|
* registered settlement key over a domain-separated settlement digest (chainId, this
|
|
* contract, jobId, provider, token, amount, resultHash), verified IN FULL on-chain by the
|
|
* live native precompile at 0x0AE1. The payout authorization is therefore post-quantum: a
|
|
* quantum adversary who forged the classical tx sender still cannot authorize the payout
|
|
* without the Falcon private key. Fail-closed: a tampered or missing signature reverts the
|
|
* release and nothing is paid.
|
|
*
|
|
* SETTLEMENT ASSET. Rewards settle in native AERE (token == address(0)) or an allowlisted
|
|
* ERC-20. No new token is introduced. Provider and challenger BONDS are always native AERE
|
|
* (the chain staking asset). Solvency is tracked per asset.
|
|
*
|
|
* FAIL-CLOSED GUARANTEES.
|
|
* - No double-pay: every job reaches a terminal status (Paid / Refunded / Slashed) exactly
|
|
* once; all fund-moving entrypoints are reentrancy-guarded and follow checks-effects-
|
|
* interactions.
|
|
* - No pay-without-proof in ZK_VERIFIED: settlement is inside submitResultZK, after the SP1
|
|
* gateway verifyProof call (which reverts on an invalid proof).
|
|
* - Escrow solvency invariant: for every asset, the contract balance is always at least the
|
|
* sum of open job rewards plus posted bonds in that asset (totalLiabilities). See isSolvent.
|
|
* - No admin drain: there is no owner and no sweep. The arbiter can only route a DISPUTED
|
|
* job's escrow between its requester, provider and challenger; governance can only flip a
|
|
* token allowlist bool. Neither can withdraw escrowed funds.
|
|
*
|
|
* HONEST SCOPE. This contract is the on-chain verification and settlement RAIL. Real GPU /
|
|
* accelerator supply, the off-chain provers, and the real SP1 proving are EXTERNAL and are
|
|
* adoption, not code (see the accompanying doc, marked [MEASURE]). The post-quantum settlement
|
|
* gate authorizes PAYOUTS; it does NOT make Aere consensus post-quantum (blocks remain Besu
|
|
* QBFT with classical secp256k1). The zk verifier is the existing deployed SP1 gateway; this
|
|
* contract references its interface and calls it, it does not reimplement it.
|
|
*/
|
|
contract AereComputeMarketV3 is ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
// ----------------------------- verification modes ----------------------------
|
|
enum Mode {
|
|
REPLAY, // 0 deterministic recompute, optimistic challenge window
|
|
ZK_VERIFIED, // 1 on-chain SP1 proof, pay only on a valid proof
|
|
OPTIMISTIC // 2 general optimistic, challenge window with provider bond slashing
|
|
}
|
|
|
|
// ----------------------------- job lifecycle ---------------------------------
|
|
enum Status {
|
|
None, // 0 never posted
|
|
Open, // 1 posted, escrowed, unclaimed
|
|
Claimed, // 2 claimed by a provider, running off-chain
|
|
Submitted, // 3 result submitted (REPLAY / OPTIMISTIC), in the challenge window
|
|
Disputed, // 4 challenged, awaiting arbiter resolution
|
|
Paid, // 5 terminal, provider paid
|
|
Refunded, // 6 terminal, requester refunded
|
|
Slashed // 7 terminal, dispute upheld, provider bond slashed to challenger
|
|
}
|
|
|
|
// ----------------------------- live precompile -------------------------------
|
|
/// @notice Live Falcon-512 verification precompile on Aere mainnet 2800 (activated block 9,189,161).
|
|
address public constant FALCON512_PRECOMPILE = address(0x0AE1);
|
|
uint256 internal constant FALCON512_PK_LEN = 897;
|
|
uint8 internal constant FALCON512_PK_HEADER = 0x09; // 0x00 | logn=9
|
|
uint256 internal constant FALCON_NONCE_LEN = 40;
|
|
|
|
/// @notice Domain separator for the Falcon settlement authorization digest.
|
|
bytes32 public constant SETTLEMENT_DOMAIN = keccak256("AereComputeMarketV3.v1.settlement");
|
|
|
|
// ----------------------------- immutable wiring ------------------------------
|
|
/// @notice The deployed SP1 gateway verifier used to check ZK_VERIFIED proofs on-chain.
|
|
ISP1Verifier public immutable ZK_VERIFIER;
|
|
/// @notice Resolves DISPUTED jobs. Can only route a disputed job's escrow; cannot drain.
|
|
address public immutable ARBITER;
|
|
/// @notice Can flip the ERC-20 reward-token allowlist. Cannot move funds. Native AERE is always allowed.
|
|
address public immutable GOVERNANCE;
|
|
|
|
struct Job {
|
|
address requester;
|
|
address provider;
|
|
address token; // address(0) = native AERE, else an allowlisted ERC-20
|
|
uint256 reward; // escrowed reward, zeroed on terminal settlement
|
|
uint256 providerBond; // native AERE bond posted by the provider (REPLAY / OPTIMISTIC)
|
|
bytes32 specHash; // keccak256 of the workload spec
|
|
bytes32 resultHash; // keccak256 of the result (set on submission)
|
|
bytes32 zkProgramVKey; // ZK_VERIFIED: the pinned SP1 program verification key
|
|
Mode mode;
|
|
Status status;
|
|
bool pqcSettlement; // require a Falcon-512 authorization to release the payout
|
|
uint64 deadline; // submission deadline
|
|
uint64 challengeWindow; // REPLAY / OPTIMISTIC challenge window (seconds)
|
|
uint64 submittedAt; // timestamp of result submission
|
|
}
|
|
|
|
/// @notice Parameters for posting a job. A struct keeps the postJob stack shallow.
|
|
struct JobParams {
|
|
address token; // address(0) native AERE, else an allowlisted ERC-20
|
|
uint256 reward; // reward amount (must equal msg.value when token is native)
|
|
uint256 providerBond; // required provider bond for REPLAY / OPTIMISTIC (native AERE)
|
|
bytes32 specHash; // keccak256 of the workload spec
|
|
bytes32 zkProgramVKey; // ZK_VERIFIED only: the SP1 program vkey to verify against
|
|
Mode mode;
|
|
uint64 deadline; // absolute unix time by which a result must be submitted
|
|
uint64 challengeWindow; // REPLAY / OPTIMISTIC challenge window (seconds)
|
|
bool pqcSettlement; // require a Falcon-512 payout authorization
|
|
bytes falconPubKey; // 897-byte Falcon-512 settlement key (required iff pqcSettlement)
|
|
}
|
|
|
|
mapping(uint256 => Job) public jobs;
|
|
mapping(uint256 => bytes) public falconPubKeyOf; // jobId => Falcon-512 settlement key (pqc jobs)
|
|
mapping(uint256 => uint256) public requiredBondOf; // jobId => native-AERE bond a provider must post to claim
|
|
mapping(uint256 => address) public challengerOf; // jobId => active challenger
|
|
mapping(uint256 => uint256) public challengerBondOf; // jobId => challenger bond (native AERE)
|
|
mapping(uint256 => bytes32) public challengeResultHashOf; // jobId => challenger's recomputed resultHash (REPLAY)
|
|
uint256 public jobCount;
|
|
|
|
/// @notice ERC-20 reward tokens the requester may escrow (native AERE is always allowed).
|
|
mapping(address => bool) public tokenAllowed;
|
|
|
|
/// @notice Total open obligations per asset: sum of open job rewards plus posted bonds. The
|
|
/// contract balance in each asset is always at least this (see isSolvent).
|
|
mapping(address => uint256) public totalLiabilities;
|
|
|
|
// -------------------------------- events -------------------------------------
|
|
event TokenAllowed(address indexed token, bool allowed);
|
|
event JobPosted(
|
|
uint256 indexed id,
|
|
address indexed requester,
|
|
address indexed token,
|
|
uint256 reward,
|
|
Mode mode,
|
|
bytes32 specHash,
|
|
uint64 deadline,
|
|
bool pqcSettlement
|
|
);
|
|
event JobClaimed(uint256 indexed id, address indexed provider, uint256 providerBond);
|
|
// Result bytes emitted for REPLAY / OPTIMISTIC so challengers can re-run and check determinism.
|
|
event ResultSubmitted(uint256 indexed id, bytes32 resultHash, bytes result);
|
|
event ResultVerifiedZK(uint256 indexed id, bytes32 resultHash, bytes32 zkProgramVKey);
|
|
event JobDisputed(uint256 indexed id, address indexed challenger, uint256 bond, bytes32 recomputedResultHash);
|
|
event DisputeResolved(uint256 indexed id, bool providerWon);
|
|
event JobPaid(uint256 indexed id, address indexed provider, address indexed token, uint256 reward, uint256 bondReturned);
|
|
event JobRefunded(uint256 indexed id, address indexed requester, address indexed token, uint256 reward);
|
|
event JobSlashed(uint256 indexed id, address indexed challenger, uint256 slashedProviderBond);
|
|
event PQCSettlementAuthorized(uint256 indexed id, bytes32 settlementDigest);
|
|
|
|
// -------------------------------- errors -------------------------------------
|
|
error ZeroAddress();
|
|
error VerifierHasNoCode();
|
|
error NotArbiter();
|
|
error NotGovernance();
|
|
error TokenNotAllowed(address token);
|
|
error BadReward();
|
|
error BadValue(); // msg.value mismatch
|
|
error BadSpec();
|
|
error BadDeadline();
|
|
error BadWindow();
|
|
error BadBond();
|
|
error BadVKey();
|
|
error BadFalconKey();
|
|
error WrongStatus();
|
|
error NotRequester();
|
|
error NotJobProvider();
|
|
error DeadlinePassed();
|
|
error DeadlineNotPassed();
|
|
error WindowOpen();
|
|
error WindowClosed();
|
|
error BadPublicValues();
|
|
error ResultMismatch();
|
|
error NotPQCAuthorized();
|
|
error PQCSigNotAllowed(); // pqc signature supplied for a non-pqc job
|
|
error TransferFailed();
|
|
|
|
modifier onlyArbiter() {
|
|
if (msg.sender != ARBITER) revert NotArbiter();
|
|
_;
|
|
}
|
|
|
|
modifier onlyGovernance() {
|
|
if (msg.sender != GOVERNANCE) revert NotGovernance();
|
|
_;
|
|
}
|
|
|
|
constructor(address zkVerifier, address arbiter, address governance) {
|
|
if (zkVerifier == address(0) || arbiter == address(0) || governance == address(0)) revert ZeroAddress();
|
|
// A code-less ZK verifier would make verifyProof (which returns no data) succeed silently, so a
|
|
// ZK_VERIFIED job would pay the provider with NO valid proof. Guard it exactly as the sibling
|
|
// AereFinalityCertificateVerifier does. The deployed value must be the live SP1 gateway.
|
|
if (zkVerifier.code.length == 0) revert VerifierHasNoCode();
|
|
ZK_VERIFIER = ISP1Verifier(zkVerifier);
|
|
ARBITER = arbiter;
|
|
GOVERNANCE = governance;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Token allowlist (governance)
|
|
// =========================================================================
|
|
|
|
/// @notice Allow or disallow an ERC-20 as a reward asset. Governance-only. Cannot move funds.
|
|
/// Native AERE (address(0)) is always allowed and is not tracked here.
|
|
function setTokenAllowed(address token, bool allowed) external onlyGovernance {
|
|
if (token == address(0)) revert ZeroAddress();
|
|
tokenAllowed[token] = allowed;
|
|
emit TokenAllowed(token, allowed);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Post a job
|
|
// =========================================================================
|
|
|
|
/// @notice Post a job and escrow its reward. Native AERE requires msg.value == reward; an
|
|
/// allowlisted ERC-20 requires msg.value == 0 and a prior approval to this contract.
|
|
function postJob(JobParams calldata p) external payable nonReentrant returns (uint256 id) {
|
|
if (p.reward == 0) revert BadReward();
|
|
if (p.specHash == bytes32(0)) revert BadSpec();
|
|
if (p.deadline <= block.timestamp) revert BadDeadline();
|
|
|
|
// Mode-specific validation.
|
|
if (p.mode == Mode.ZK_VERIFIED) {
|
|
if (p.zkProgramVKey == bytes32(0)) revert BadVKey();
|
|
// ZK jobs carry no provider bond: the proof is the guarantee.
|
|
if (p.providerBond != 0) revert BadBond();
|
|
} else {
|
|
// REPLAY / OPTIMISTIC need a challenge window and a non-zero provider bond to slash.
|
|
if (p.challengeWindow == 0) revert BadWindow();
|
|
if (p.providerBond == 0) revert BadBond();
|
|
}
|
|
|
|
// Post-quantum settlement key validation.
|
|
if (p.pqcSettlement) {
|
|
if (p.falconPubKey.length != FALCON512_PK_LEN || uint8(p.falconPubKey[0]) != FALCON512_PK_HEADER) {
|
|
revert BadFalconKey();
|
|
}
|
|
} else if (p.falconPubKey.length != 0) {
|
|
revert BadFalconKey();
|
|
}
|
|
|
|
// Escrow the reward (interaction before state so a fee-on-transfer token cannot under-fund).
|
|
if (p.token == address(0)) {
|
|
if (msg.value != p.reward) revert BadValue();
|
|
} else {
|
|
if (!tokenAllowed[p.token]) revert TokenNotAllowed(p.token);
|
|
if (msg.value != 0) revert BadValue();
|
|
uint256 balBefore = IERC20(p.token).balanceOf(address(this));
|
|
IERC20(p.token).safeTransferFrom(msg.sender, address(this), p.reward);
|
|
uint256 received = IERC20(p.token).balanceOf(address(this)) - balBefore;
|
|
if (received != p.reward) revert BadReward(); // reject fee-on-transfer / rebasing tokens
|
|
}
|
|
|
|
id = jobCount++;
|
|
jobs[id] = Job({
|
|
requester: msg.sender,
|
|
provider: address(0),
|
|
token: p.token,
|
|
reward: p.reward,
|
|
providerBond: 0,
|
|
specHash: p.specHash,
|
|
resultHash: bytes32(0),
|
|
zkProgramVKey: p.zkProgramVKey,
|
|
mode: p.mode,
|
|
status: Status.Open,
|
|
pqcSettlement: p.pqcSettlement,
|
|
deadline: p.deadline,
|
|
challengeWindow: p.challengeWindow,
|
|
submittedAt: 0
|
|
});
|
|
totalLiabilities[p.token] += p.reward;
|
|
if (p.mode != Mode.ZK_VERIFIED) requiredBondOf[id] = p.providerBond;
|
|
if (p.pqcSettlement) falconPubKeyOf[id] = p.falconPubKey;
|
|
|
|
emit JobPosted(id, msg.sender, p.token, p.reward, p.mode, p.specHash, p.deadline, p.pqcSettlement);
|
|
}
|
|
|
|
/// @notice Requester reclaims the escrow of an unclaimed job at any time.
|
|
function cancelJob(uint256 id) external nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.requester != msg.sender) revert NotRequester();
|
|
if (j.status != Status.Open) revert WrongStatus();
|
|
j.status = Status.Refunded;
|
|
uint256 amt = j.reward;
|
|
j.reward = 0;
|
|
totalLiabilities[j.token] -= amt;
|
|
_payout(j.token, msg.sender, amt);
|
|
emit JobRefunded(id, msg.sender, j.token, amt);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Claim a job
|
|
// =========================================================================
|
|
|
|
/// @notice A provider claims an open job. For REPLAY / OPTIMISTIC the provider must post exactly
|
|
/// the job's required native-AERE bond (msg.value). For ZK_VERIFIED, msg.value must be 0.
|
|
function claimJob(uint256 id) external payable nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.status != Status.Open) revert WrongStatus();
|
|
if (block.timestamp > j.deadline) revert DeadlinePassed();
|
|
|
|
uint256 requiredBond = j.mode == Mode.ZK_VERIFIED ? 0 : requiredBondOf[id];
|
|
if (msg.value != requiredBond) revert BadValue();
|
|
|
|
j.provider = msg.sender;
|
|
j.providerBond = msg.value;
|
|
j.status = Status.Claimed;
|
|
if (msg.value != 0) totalLiabilities[address(0)] += msg.value;
|
|
|
|
emit JobClaimed(id, msg.sender, msg.value);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Submit a result (REPLAY / OPTIMISTIC)
|
|
// =========================================================================
|
|
|
|
/// @notice Provider submits the full result bytes for a REPLAY or OPTIMISTIC job. keccak256(result)
|
|
/// is stored as the on-chain anchor and the bytes are emitted so anyone can re-run (REPLAY)
|
|
/// or check (OPTIMISTIC) the work. The challenge window starts now.
|
|
function submitResult(uint256 id, bytes calldata result) external {
|
|
Job storage j = jobs[id];
|
|
if (j.provider != msg.sender) revert NotJobProvider();
|
|
if (j.status != Status.Claimed) revert WrongStatus();
|
|
if (j.mode == Mode.ZK_VERIFIED) revert WrongStatus(); // ZK jobs settle via submitResultZK
|
|
if (block.timestamp > j.deadline) revert DeadlinePassed();
|
|
|
|
bytes32 resultHash = keccak256(result);
|
|
j.resultHash = resultHash;
|
|
j.status = Status.Submitted;
|
|
j.submittedAt = uint64(block.timestamp);
|
|
emit ResultSubmitted(id, resultHash, result);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Submit a result (ZK_VERIFIED, pay on valid proof)
|
|
// =========================================================================
|
|
|
|
/// @notice Provider submits a ZK_VERIFIED result with an SP1 proof. The proof is verified on-chain
|
|
/// through the deployed SP1 gateway; the reward is released to the provider in THIS
|
|
/// transaction and ONLY if the proof verifies. There is no other path that pays a
|
|
/// ZK_VERIFIED job, so payment without a valid proof is impossible.
|
|
/// @param id the job id.
|
|
/// @param publicValues the SP1 public values, ABI-encoded as (bytes32 specHash, bytes32 resultHash).
|
|
/// specHash must equal the job's committed spec; resultHash is recorded.
|
|
/// @param proofBytes the SP1 proof; the first 4 bytes select the gateway route.
|
|
/// @param pqcSig Falcon-512 settlement authorization (only if the job set pqcSettlement; empty otherwise).
|
|
function submitResultZK(uint256 id, bytes calldata publicValues, bytes calldata proofBytes, bytes calldata pqcSig)
|
|
external
|
|
nonReentrant
|
|
{
|
|
Job storage j = jobs[id];
|
|
if (j.provider != msg.sender) revert NotJobProvider();
|
|
if (j.status != Status.Claimed) revert WrongStatus();
|
|
if (j.mode != Mode.ZK_VERIFIED) revert WrongStatus();
|
|
if (block.timestamp > j.deadline) revert DeadlinePassed();
|
|
|
|
// Bind the proof's public values to THIS job's spec, and record the proven result.
|
|
if (publicValues.length != 64) revert BadPublicValues();
|
|
(bytes32 provenSpecHash, bytes32 provenResultHash) = abi.decode(publicValues, (bytes32, bytes32));
|
|
if (provenSpecHash != j.specHash) revert BadPublicValues();
|
|
j.resultHash = provenResultHash;
|
|
|
|
// On-chain verification through the deployed SP1 gateway. Reverts on an invalid proof, so a
|
|
// rejected proof reverts the whole transaction and pays nothing (fail-closed).
|
|
ZK_VERIFIER.verifyProof(j.zkProgramVKey, publicValues, proofBytes);
|
|
|
|
emit ResultVerifiedZK(id, provenResultHash, j.zkProgramVKey);
|
|
|
|
// Settle immediately: pay only now that the proof has verified.
|
|
_settleToProvider(id, j, pqcSig);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Accept / finalize (REPLAY / OPTIMISTIC)
|
|
// =========================================================================
|
|
|
|
/// @notice Requester accepts a submitted result early (before the window closes) and pays the provider.
|
|
function acceptResult(uint256 id, bytes calldata pqcSig) external nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.requester != msg.sender) revert NotRequester();
|
|
if (j.status != Status.Submitted) revert WrongStatus();
|
|
_settleToProvider(id, j, pqcSig);
|
|
}
|
|
|
|
/// @notice After the challenge window closes with no dispute, anyone can finalize and pay the provider.
|
|
function finalize(uint256 id, bytes calldata pqcSig) external nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.status != Status.Submitted) revert WrongStatus();
|
|
if (block.timestamp < uint256(j.submittedAt) + j.challengeWindow) revert WindowOpen();
|
|
_settleToProvider(id, j, pqcSig);
|
|
}
|
|
|
|
/// @notice If a claimed job blows its deadline without a submission, the requester reclaims the
|
|
/// reward and the provider bond is slashed to the requester (provider fault).
|
|
function reclaimExpired(uint256 id) external nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.requester != msg.sender) revert NotRequester();
|
|
if (j.status != Status.Claimed) revert WrongStatus();
|
|
if (block.timestamp <= j.deadline) revert DeadlineNotPassed();
|
|
|
|
j.status = Status.Refunded;
|
|
uint256 reward = j.reward;
|
|
uint256 bond = j.providerBond;
|
|
j.reward = 0;
|
|
j.providerBond = 0;
|
|
totalLiabilities[j.token] -= reward;
|
|
if (bond != 0) totalLiabilities[address(0)] -= bond;
|
|
|
|
_payout(j.token, msg.sender, reward);
|
|
if (bond != 0) _payout(address(0), msg.sender, bond); // provider bond slashed to requester
|
|
emit JobRefunded(id, msg.sender, j.token, reward);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Disputes
|
|
// =========================================================================
|
|
|
|
/// @notice A challenger disputes a submitted REPLAY / OPTIMISTIC result within the window, posting
|
|
/// a native-AERE bond equal to the provider bond. For REPLAY the challenger records the
|
|
/// resultHash they recomputed, so the arbiter (and any observer) can verify determinism.
|
|
function dispute(uint256 id, bytes32 recomputedResultHash) external payable nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.status != Status.Submitted) revert WrongStatus();
|
|
if (block.timestamp >= uint256(j.submittedAt) + j.challengeWindow) revert WindowClosed();
|
|
if (msg.value != j.providerBond) revert BadValue();
|
|
|
|
j.status = Status.Disputed;
|
|
challengerOf[id] = msg.sender;
|
|
challengerBondOf[id] = msg.value;
|
|
challengeResultHashOf[id] = recomputedResultHash;
|
|
totalLiabilities[address(0)] += msg.value;
|
|
|
|
emit JobDisputed(id, msg.sender, msg.value, recomputedResultHash);
|
|
}
|
|
|
|
/// @notice The arbiter resolves a disputed job. It may only route the escrow between the job's
|
|
/// requester, provider and challenger; it can never withdraw funds elsewhere.
|
|
/// providerWon == true : result upheld. Provider takes the reward, its bond back, and the
|
|
/// challenger's forfeited bond.
|
|
/// providerWon == false: dispute upheld. Reward refunded to the requester; the PROVIDER
|
|
/// bond is slashed to the challenger, who also gets its own bond back.
|
|
function resolveDispute(uint256 id, bool providerWon) external onlyArbiter nonReentrant {
|
|
Job storage j = jobs[id];
|
|
if (j.status != Status.Disputed) revert WrongStatus();
|
|
|
|
address challenger = challengerOf[id];
|
|
uint256 chBond = challengerBondOf[id];
|
|
uint256 reward = j.reward;
|
|
uint256 pBond = j.providerBond;
|
|
address token = j.token;
|
|
address provider = j.provider;
|
|
address requester = j.requester;
|
|
|
|
// Effects first.
|
|
j.reward = 0;
|
|
j.providerBond = 0;
|
|
challengerBondOf[id] = 0;
|
|
totalLiabilities[token] -= reward;
|
|
totalLiabilities[address(0)] -= (pBond + chBond);
|
|
|
|
if (providerWon) {
|
|
j.status = Status.Paid;
|
|
_payout(token, provider, reward);
|
|
if (pBond != 0) _payout(address(0), provider, pBond); // bond returned
|
|
if (chBond != 0) _payout(address(0), provider, chBond); // challenger forfeits
|
|
emit JobPaid(id, provider, token, reward, pBond);
|
|
} else {
|
|
j.status = Status.Slashed;
|
|
_payout(token, requester, reward); // reward refunded
|
|
if (chBond != 0) _payout(address(0), challenger, chBond); // challenger bond returned
|
|
if (pBond != 0) _payout(address(0), challenger, pBond); // provider bond slashed to challenger
|
|
emit JobSlashed(id, challenger, pBond);
|
|
}
|
|
emit DisputeResolved(id, providerWon);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Settlement (shared internal)
|
|
// =========================================================================
|
|
|
|
/// @dev Release a job's reward (and return the provider bond) to the provider. If the job requires
|
|
/// a post-quantum settlement authorization, a valid Falcon-512 signature by the job's
|
|
/// settlement key over the settlement digest is required; a tampered or missing signature
|
|
/// reverts and nothing is paid (fail-closed). Sets the terminal status before any transfer.
|
|
function _settleToProvider(uint256 id, Job storage j, bytes calldata pqcSig) internal {
|
|
address provider = j.provider;
|
|
address token = j.token;
|
|
uint256 reward = j.reward;
|
|
uint256 bond = j.providerBond;
|
|
|
|
if (j.pqcSettlement) {
|
|
bytes32 digest = settlementDigest(id, provider, token, reward, j.resultHash);
|
|
if (!_falconVerify(falconPubKeyOf[id], digest, pqcSig)) revert NotPQCAuthorized();
|
|
emit PQCSettlementAuthorized(id, digest);
|
|
} else if (pqcSig.length != 0) {
|
|
revert PQCSigNotAllowed();
|
|
}
|
|
|
|
// Effects.
|
|
j.status = Status.Paid;
|
|
j.reward = 0;
|
|
j.providerBond = 0;
|
|
totalLiabilities[token] -= reward;
|
|
if (bond != 0) totalLiabilities[address(0)] -= bond;
|
|
|
|
// Interactions.
|
|
_payout(token, provider, reward);
|
|
if (bond != 0) _payout(address(0), provider, bond);
|
|
emit JobPaid(id, provider, token, reward, bond);
|
|
}
|
|
|
|
/// @dev Transfer `amt` of `token` (address(0) = native AERE) to `to`. No-op on a zero amount.
|
|
function _payout(address token, address to, uint256 amt) internal {
|
|
if (amt == 0) return;
|
|
if (token == address(0)) {
|
|
(bool ok,) = to.call{value: amt}("");
|
|
if (!ok) revert TransferFailed();
|
|
} else {
|
|
IERC20(token).safeTransfer(to, amt);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Post-quantum settlement authorization
|
|
// =========================================================================
|
|
|
|
/// @notice The 32-byte digest a settlement key must Falcon-512-sign to authorize a payout. Binds
|
|
/// chainId + this contract + the job id + provider + token + amount + resultHash, so an
|
|
/// authorization can never be replayed to another job, chain, contract, or payout figure.
|
|
function settlementDigest(uint256 id, address provider, address token, uint256 amount, bytes32 resultHash)
|
|
public
|
|
view
|
|
returns (bytes32)
|
|
{
|
|
return keccak256(abi.encode(SETTLEMENT_DOMAIN, block.chainid, address(this), id, provider, token, amount, resultHash));
|
|
}
|
|
|
|
/// @dev Verify a Falcon-512 signature by `pubKey` over the 32-byte `message` through the live
|
|
/// precompile at 0x0AE1. Byte-identical input encoding to AerePQCMessageVerifier
|
|
/// (input = pk || sm, sm = sigLen(2, big-endian) || nonce(40) || message || esig). Fail-closed:
|
|
/// returns false (never reverts) on any malformed input or an empty / zero precompile return.
|
|
function _falconVerify(bytes memory pubKey, bytes32 message, bytes memory signature) internal view returns (bool) {
|
|
if (pubKey.length != FALCON512_PK_LEN) return false;
|
|
if (signature.length <= FALCON_NONCE_LEN) return false;
|
|
uint256 sigLen = signature.length - FALCON_NONCE_LEN; // esig length
|
|
if (sigLen > type(uint16).max) return false;
|
|
|
|
bytes memory nonce = _slice(signature, 0, FALCON_NONCE_LEN);
|
|
bytes memory esig = _slice(signature, FALCON_NONCE_LEN, sigLen);
|
|
bytes memory msgBytes = abi.encodePacked(message); // 32 bytes
|
|
bytes memory sm = abi.encodePacked(uint16(sigLen), nonce, msgBytes, esig);
|
|
bytes memory input = abi.encodePacked(pubKey, sm);
|
|
|
|
(bool ok, bytes memory ret) = FALCON512_PRECOMPILE.staticcall(input);
|
|
return ok && ret.length >= 32 && ret[31] == 0x01;
|
|
}
|
|
|
|
function _slice(bytes memory data, uint256 start, uint256 len) internal pure returns (bytes memory out) {
|
|
out = new bytes(len);
|
|
for (uint256 i = 0; i < len; i++) {
|
|
out[i] = data[start + i];
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Views
|
|
// =========================================================================
|
|
|
|
/// @notice The escrow-solvency invariant for an asset (address(0) = native AERE): the contract
|
|
/// balance is at least the sum of open job rewards plus posted bonds in that asset.
|
|
function isSolvent(address token) public view returns (bool) {
|
|
uint256 bal = token == address(0) ? address(this).balance : IERC20(token).balanceOf(address(this));
|
|
return bal >= totalLiabilities[token];
|
|
}
|
|
|
|
/// @notice Read a full job record.
|
|
function getJob(uint256 id) external view returns (Job memory) {
|
|
return jobs[id];
|
|
}
|
|
}
|