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