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

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];
}
}