// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; /** * @title AereComputeMarketV2 - DePIN compute coordination + settlement rail * @notice The ON-CHAIN coordination and settlement layer for a decentralized * compute marketplace (DePIN for AI and verifiable compute). Providers * stake native AERE and advertise capacity; requesters escrow AERE for a * job; the provider runs the work OFF-CHAIN and submits a result; the * requester confirms (or a timeout auto-confirms) and payment releases. * A dispute window lets the requester challenge, with Foundation * arbitration and provider-stake slashing. * * WHAT V2 ADDS OVER V1 (0xf0c8178a5d9feb0f70C5f184e79edeEDaddcF350): * 1. Self-contained jobs. `postJob` takes the FULL workload spec as * bytes and emits it in the JobPosted event, so ANY provider daemon * watching the chain learns exactly what to compute with no off-chain * channel. The contract stores only keccak256(spec) as the anchor. * 2. Verifiable determinism. `submitResult` takes the FULL result bytes * and emits them; the contract stores keccak256(result). The requester * (or anyone) re-runs the deterministic workload from the on-chain * spec and checks keccak256(localResult) == the on-chain resultHash. * 3. A distinct Disputed status. Once a job is disputed it can no longer * be re-submitted by the provider to dodge arbitration (a V1 flaw); * only the Foundation can resolve it. * 4. Bootstrap-configurable MIN_STAKE (constructor immutable) so the * stake floor is a governance parameter rather than a hardcode. * * SCOPE (honest): this contract is the trust and settlement RAIL. The * actual GPU/compute supply is off-chain and requires real hardware * providers. The rail is fully operational (anyone can stake and run the * reference provider daemon to earn), but large-scale supply is adoption, * exactly like validator decentralization. No new token: staking, escrow * and payment are all in native AERE. */ contract AereComputeMarketV2 is Ownable, ReentrancyGuard { uint256 public immutable MIN_STAKE; // stake floor to be a provider (set at deploy) uint64 public constant CONFIRM_WINDOW = 3 days; // requester confirm/dispute window uint16 public constant SLASH_BPS = 5000; // 50% of stake slashed on a lost dispute address public immutable FOUNDATION; // dispute arbiter struct Provider { bool active; uint256 stake; uint32 pricePerUnit; // informational, AERE-wei per compute unit uint32 activeJobs; // outstanding claimed/submitted/disputed jobs string endpoint; // provider API endpoint (off-chain, optional) } enum Status { None, Open, Claimed, Submitted, Disputed, Paid, Refunded, Slashed } struct Job { address requester; address provider; uint256 payment; // escrowed AERE bytes32 specHash; // keccak256 of the workload spec (spec bytes emitted in event) bytes32 resultHash; // keccak256 of the result (result bytes emitted in event) Status status; uint64 submittedAt; } mapping(address => Provider) public providers; mapping(uint256 => Job) public jobs; uint256 public jobCount; uint256 public slashPool; // slashed stake, sweepable by Foundation to the sink/treasury event ProviderRegistered(address indexed provider, uint256 stake, uint32 pricePerUnit, string endpoint); event ProviderDeregistered(address indexed provider, uint256 stakeReturned); // spec bytes are emitted (non-indexed) so any watcher can reconstruct the workload. event JobPosted(uint256 indexed id, address indexed requester, uint256 payment, bytes32 specHash, bytes spec); event JobClaimed(uint256 indexed id, address indexed provider); // result bytes are emitted so the requester can verify determinism against specHash off-chain. event JobSubmitted(uint256 indexed id, bytes32 resultHash, bytes result); event JobPaid(uint256 indexed id, address indexed provider, uint256 payment); event JobRefunded(uint256 indexed id, address indexed requester, uint256 payment); event JobDisputed(uint256 indexed id); event DisputeResolved(uint256 indexed id, bool providerWon); error BadStake(); error NotProvider(); error HasActiveJobs(); error ZeroPayment(); error WrongStatus(); error NotRequester(); error NotJobProvider(); error WindowOpen(); error WindowClosed(); error ZeroAddress(); error EmptySpec(); constructor(address foundation, uint256 minStake) { if (foundation == address(0)) revert ZeroAddress(); if (minStake == 0) revert BadStake(); FOUNDATION = foundation; MIN_STAKE = minStake; } modifier onlyFoundation() { if (msg.sender != FOUNDATION) revert("not foundation"); _; } /* ------------------------------ providers ------------------------------- */ function registerProvider(uint32 pricePerUnit, string calldata endpoint) external payable { Provider storage p = providers[msg.sender]; if (p.stake + msg.value < MIN_STAKE) revert BadStake(); p.active = true; p.stake += msg.value; p.pricePerUnit = pricePerUnit; p.endpoint = endpoint; emit ProviderRegistered(msg.sender, p.stake, pricePerUnit, endpoint); } function deregisterProvider() external nonReentrant { Provider storage p = providers[msg.sender]; if (!p.active) revert NotProvider(); if (p.activeJobs != 0) revert HasActiveJobs(); uint256 amt = p.stake; p.active = false; p.stake = 0; (bool ok, ) = msg.sender.call{value: amt}(""); require(ok, "xfer"); emit ProviderDeregistered(msg.sender, amt); } /* -------------------------------- jobs ---------------------------------- */ /// @notice Post a job with its FULL workload spec. The spec bytes are emitted /// so any provider daemon can reconstruct and run it deterministically. function postJob(bytes calldata spec) external payable returns (uint256 id) { if (msg.value == 0) revert ZeroPayment(); if (spec.length == 0) revert EmptySpec(); bytes32 specHash = keccak256(spec); id = jobCount++; jobs[id] = Job({ requester: msg.sender, provider: address(0), payment: msg.value, specHash: specHash, resultHash: bytes32(0), status: Status.Open, submittedAt: 0 }); emit JobPosted(id, msg.sender, msg.value, specHash, spec); } /// @notice Requester cancels an unclaimed job and reclaims the escrow. 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.payment; j.payment = 0; (bool ok, ) = msg.sender.call{value: amt}(""); require(ok, "xfer"); emit JobRefunded(id, msg.sender, amt); } function claimJob(uint256 id) external { Job storage j = jobs[id]; if (j.status != Status.Open) revert WrongStatus(); Provider storage p = providers[msg.sender]; if (!p.active || p.stake < MIN_STAKE) revert NotProvider(); j.provider = msg.sender; j.status = Status.Claimed; p.activeJobs += 1; emit JobClaimed(id, msg.sender); } /// @notice Provider submits the FULL result bytes. keccak256(result) is stored /// on-chain as the verifiable anchor; the bytes are emitted for re-run. 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(); bytes32 resultHash = keccak256(result); j.resultHash = resultHash; j.status = Status.Submitted; j.submittedAt = uint64(block.timestamp); emit JobSubmitted(id, resultHash, result); } /// @notice Requester accepts the result, pay the provider immediately. function confirmJob(uint256 id) external nonReentrant { Job storage j = jobs[id]; if (j.requester != msg.sender) revert NotRequester(); if (j.status != Status.Submitted) revert WrongStatus(); _pay(id, j); } /// @notice After the confirm window with no dispute, anyone can settle, pay provider. function autoConfirm(uint256 id) external nonReentrant { Job storage j = jobs[id]; if (j.status != Status.Submitted) revert WrongStatus(); if (block.timestamp < j.submittedAt + CONFIRM_WINDOW) revert WindowOpen(); _pay(id, j); } function _pay(uint256 id, Job storage j) internal { j.status = Status.Paid; uint256 amt = j.payment; j.payment = 0; providers[j.provider].activeJobs -= 1; (bool ok, ) = j.provider.call{value: amt}(""); require(ok, "xfer"); emit JobPaid(id, j.provider, amt); } /* ------------------------------ disputes -------------------------------- */ /// @notice Requester challenges a submitted result within the confirm window. /// The job moves to Disputed and can no longer be re-submitted; only /// the Foundation can resolve it. function disputeJob(uint256 id) external { Job storage j = jobs[id]; if (j.requester != msg.sender) revert NotRequester(); if (j.status != Status.Submitted) revert WrongStatus(); if (block.timestamp >= j.submittedAt + CONFIRM_WINDOW) revert WindowClosed(); j.status = Status.Disputed; emit JobDisputed(id); } /// @notice Foundation arbitrates a disputed job. providerWon -> provider paid; /// else requester refunded AND provider stake slashed by SLASH_BPS. function resolveDispute(uint256 id, bool providerWon) external onlyFoundation nonReentrant { Job storage j = jobs[id]; if (j.status != Status.Disputed) revert WrongStatus(); Provider storage p = providers[j.provider]; p.activeJobs -= 1; uint256 amt = j.payment; j.payment = 0; if (providerWon) { j.status = Status.Paid; (bool ok, ) = j.provider.call{value: amt}(""); require(ok, "xfer"); emit JobPaid(id, j.provider, amt); } else { j.status = Status.Slashed; uint256 slash = (p.stake * SLASH_BPS) / 10_000; p.stake -= slash; slashPool += slash; (bool ok, ) = j.requester.call{value: amt}(""); require(ok, "xfer"); emit JobRefunded(id, j.requester, amt); } emit DisputeResolved(id, providerWon); } function sweepSlashPool(address to) external onlyFoundation nonReentrant { if (to == address(0)) revert ZeroAddress(); uint256 amt = slashPool; slashPool = 0; (bool ok, ) = to.call{value: amt}(""); require(ok, "xfer"); } /* -------------------------------- views --------------------------------- */ /// @notice Convenience getter for off-chain daemons. function getJob(uint256 id) external view returns (Job memory) { return jobs[id]; } }