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