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.
266 lines
12 KiB
Solidity
266 lines
12 KiB
Solidity
// 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];
|
|
}
|
|
}
|