aere-contracts/contracts/erc8004/AereAP2MandateVerifier.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

229 lines
13 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/// @dev The one AerePQCKeyRegistry read used to verify a mandate's post-quantum signature. Live at
/// 0x1eCa…3691 on chain 2800; verifyWithKey routes to the native precompile for the key's
/// scheme (0x0AE1 Falcon-512) and verifies in full on-chain, fail-closed.
interface IAerePQCKeyRegistryMandate {
function verifyWithKey(uint256 keyId, bytes32 message, bytes calldata signature) external view returns (bool);
function statusOf(uint256 keyId) external view returns (uint8);
function schemeOf(uint256 keyId) external view returns (uint8);
}
/**
* @title AereAP2MandateVerifier, a post-quantum x402 / AP2 payment-mandate verifier
*
* @notice A minimal verifier for AP2-style payment MANDATES whose authorizing signature is
* POST-QUANTUM (Falcon-512), verified in full on-chain by Aere Network's LIVE native
* precompile (0x0AE1) through the already-deployed AerePQCKeyRegistry. Settlement of the
* authorized spend is routed through the EXISTING AERE402 rail (AERE402FacilitatorPQC /
* AERE402FacilitatorV2), which is NOT redeployed or modified here.
*
* WHAT AN AP2 / x402 MANDATE IS. A signed, standing authorization of the form
* "agent X may spend up to N of token T per period P for purpose Q". In AP2 (Google) and
* x402 (Coinbase) today that authorization is an ECDSA signature, which a quantum computer
* forges. Here the authorizing key is a Falcon-512 key in AerePQCKeyRegistry, so the mandate
* is quantum-durable. The struct and the verify path are below.
*
* THE STRUCT. A Mandate binds the paying agent's Falcon key, the token, the spend cap, the
* validity window, an opaque purpose tag, and a mandate nonce (so an agent can issue several
* independent standing mandates). Its `mandateHash` binds chainId + this contract so a
* signed mandate cannot be replayed against another verifier or chain.
*
* THE VERIFY PATH.
* - verifyMandate(m, falconSig): pure on-chain PQC check that the agent's Falcon key
* signed `m` (view; anyone can check for free via eth_call).
* - authorizeSpend(m, amount, falconSig): the enforced path. Verifies the Falcon mandate
* signature via the live precompile, checks the validity window and the cumulative cap,
* records the spend against the mandate, and emits MandateAuthorized. Reverts fail-closed
* on an invalid / tampered signature, an out-of-window mandate, or a cap overflow.
*
* SETTLEMENT ROUTING. authorizeSpend records that `amount` is authorized under the mandate;
* the actual token movement + 25-bps fee to AereSink is performed by the live AERE402 rail.
* To keep the mandate's cumulative cap honest (only real settlements should consume it),
* authorizeSpend is gated to an immutable SETTLEMENT_EXECUTOR when one is configured (the
* AERE402 facilitator, which calls authorizeSpend atomically with settlement). When
* SETTLEMENT_EXECUTOR is the zero address the accounting path is open (for verification and
* testing); the pure PQC check verifyMandate is always open.
*
* [DEPLOY CONSTRAINT, accepted-and-documented risk] A Falcon mandate signature is a STANDING
* authorization by design: one signature authorizes repeated spends up to the cap, and the
* signature is public. So in the OPEN accounting path (SETTLEMENT_EXECUTOR == address(0)) ANY
* address that observes the signature can replay authorizeSpend to consume the mandate's
* remaining cap WITHOUT a real settlement (cap-exhaustion griefing; no custody, no theft, since
* this contract moves no funds). The mitigation is built in and MUST be used in production: set
* SETTLEMENT_EXECUTOR to the AERE402 facilitator so cap consumption only ever happens ALONGSIDE
* a real settlement (the gate is enforced in authorizeSpend and proven in the tests). The open
* path is intended ONLY for eth_call verification and unit testing and MUST NOT be used for a
* production deployment. A per-authorization nonce is deliberately NOT added here because it
* would break the intended standing-mandate ("sign once, spend repeatedly up to the cap")
* semantics; the executor gate closes the vector without that trade-off.
*
* @dev [VERIFY: confirm against the finalized x402 / AP2 mandate schema] The x402 and AP2 mandate
* field sets are still evolving (AP2 distinguishes Intent vs Cart mandates; x402 carries an
* HTTP-402 payment payload). This struct captures the common "spend up to N per period for
* purpose Q" authorization; reconcile the exact field set and encoding with the final specs
* before claiming wire-level conformance.
*
* HONEST SCOPE. Application-layer payment authorization only. Does NOT change AERE consensus
* (still Besu QBFT classical ECDSA). Holds NO funds (the cap is accounting, not custody; the
* AERE402 rail custodies and moves value). No admin.
*/
contract AereAP2MandateVerifier {
/// @notice Falcon-512 is the mandate signing scheme (live precompile 0x0AE1).
uint8 public constant SCHEME_FALCON512 = 1;
uint8 internal constant REGISTRY_STATUS_ACTIVE = 1;
/// @notice Domain separator binding a mandate hash to this verifier.
bytes32 public constant MANDATE_DOMAIN = keccak256("AereAP2MandateVerifier.v1.mandate");
/// @notice The LIVE AerePQCKeyRegistry that verifies the Falcon mandate signature via 0x0AE1.
IAerePQCKeyRegistryMandate public immutable KEY_REGISTRY;
/// @notice The AERE402 settlement executor allowed to consume a mandate's cap. Zero = open path.
address public immutable SETTLEMENT_EXECUTOR;
/// @notice An AP2 / x402 payment mandate: "agentKey may spend up to maxAmount of token per the
/// [periodStart, periodEnd] window for `purpose`". mandateNonce distinguishes an agent's
/// several standing mandates.
struct Mandate {
uint256 agentKeyId; // the paying agent's Falcon-512 key in AerePQCKeyRegistry
address token; // the settlement token the cap is denominated in
uint256 maxAmount; // cumulative spend cap over the window
uint64 periodStart; // unix seconds, inclusive
uint64 periodEnd; // unix seconds, inclusive (>= periodStart)
bytes32 purpose; // opaque purpose tag, e.g. keccak256("inference:anthropic")
uint256 mandateNonce; // per-agent mandate index (lets an agent hold several mandates)
}
/// @notice mandateHash => cumulative amount already authorized under that mandate.
mapping(bytes32 => uint256) public spentUnder;
event MandateAuthorized(
bytes32 indexed mandateHash,
uint256 indexed agentKeyId,
address indexed token,
uint256 amount,
uint256 cumulative,
bytes32 purpose
);
error ZeroAddress();
error NotSettlementExecutor();
error AgentKeyNotActiveFalcon(uint256 agentKeyId);
error InvalidMandateWindow(uint64 periodStart, uint64 periodEnd);
error MandateNotYetValid(uint64 periodStart);
error MandateExpired(uint64 periodEnd);
error ZeroAmount();
error MandateCapExceeded(uint256 cap, uint256 attempted);
error MandateSignatureInvalid();
constructor(address keyRegistry_, address settlementExecutor_) {
if (keyRegistry_ == address(0)) revert ZeroAddress();
KEY_REGISTRY = IAerePQCKeyRegistryMandate(keyRegistry_);
SETTLEMENT_EXECUTOR = settlementExecutor_; // zero allowed = open accounting path
}
// ==========================================================================
// Mandate hash + pure verify
// ==========================================================================
/// @notice The canonical hash a Falcon mandate signature must sign. Binds chainId + this
/// verifier so a mandate cannot be replayed against another verifier or chain.
function mandateHash(Mandate calldata m) public view returns (bytes32) {
return keccak256(
abi.encode(
MANDATE_DOMAIN,
block.chainid,
address(this),
m.agentKeyId,
m.token,
m.maxAmount,
m.periodStart,
m.periodEnd,
m.purpose,
m.mandateNonce
)
);
}
/// @notice Pure on-chain post-quantum check that the agent's Falcon key signed this mandate.
/// View, permissionless: anyone can verify a mandate via eth_call for free. Returns
/// false (never reverts) if the key is not an ACTIVE Falcon key or the signature is
/// invalid.
function verifyMandate(Mandate calldata m, bytes calldata falconSig) external view returns (bool) {
if (KEY_REGISTRY.statusOf(m.agentKeyId) != REGISTRY_STATUS_ACTIVE) return false;
if (KEY_REGISTRY.schemeOf(m.agentKeyId) != SCHEME_FALCON512) return false;
return KEY_REGISTRY.verifyWithKey(m.agentKeyId, mandateHash(m), falconSig);
}
// ==========================================================================
// Authorize (enforced path)
// ==========================================================================
/**
* @notice Authorize `amount` of spend under a Falcon-signed mandate. Verifies the post-quantum
* mandate signature on-chain via the live precompile, enforces the validity window and
* the cumulative cap, records the spend, and emits MandateAuthorized. Fail-closed on any
* invalid / tampered signature, an out-of-window mandate, or a cap overflow.
*
* Gated to SETTLEMENT_EXECUTOR when one is configured, so a mandate's cap is only ever
* consumed alongside a real AERE402 settlement (the executor calls this atomically).
* PRODUCTION MUST configure SETTLEMENT_EXECUTOR: in the open path (executor == 0) the public
* standing signature is replayable by anyone to exhaust the cap (see the contract-level
* DEPLOY CONSTRAINT note). The open path is verification / test only.
*
* @param m the mandate being drawn against.
* @param amount the spend to authorize now (charged against the mandate's cumulative cap).
* @param falconSig the agent's Falcon-512 signature over mandateHash(m).
* @return mh the mandate hash the spend was recorded under.
*/
function authorizeSpend(Mandate calldata m, uint256 amount, bytes calldata falconSig)
external
returns (bytes32 mh)
{
if (SETTLEMENT_EXECUTOR != address(0) && msg.sender != SETTLEMENT_EXECUTOR) revert NotSettlementExecutor();
if (amount == 0) revert ZeroAmount();
// Window checks.
if (m.periodEnd < m.periodStart) revert InvalidMandateWindow(m.periodStart, m.periodEnd);
if (block.timestamp < m.periodStart) revert MandateNotYetValid(m.periodStart);
if (block.timestamp > m.periodEnd) revert MandateExpired(m.periodEnd);
// The mandate signer must be an ACTIVE Falcon key (rotating / revoking it halts the mandate).
if (
KEY_REGISTRY.statusOf(m.agentKeyId) != REGISTRY_STATUS_ACTIVE
|| KEY_REGISTRY.schemeOf(m.agentKeyId) != SCHEME_FALCON512
) revert AgentKeyNotActiveFalcon(m.agentKeyId);
mh = mandateHash(m);
// Full on-chain post-quantum verification of the mandate authorization.
if (!KEY_REGISTRY.verifyWithKey(m.agentKeyId, mh, falconSig)) revert MandateSignatureInvalid();
// Cap check (overflow-safe) then record.
uint256 already = spentUnder[mh];
uint256 next = already + amount;
if (next < already || next > m.maxAmount) revert MandateCapExceeded(m.maxAmount, next);
spentUnder[mh] = next;
emit MandateAuthorized(mh, m.agentKeyId, m.token, amount, next, m.purpose);
}
// ==========================================================================
// Views
// ==========================================================================
/// @notice Remaining spendable amount under a mandate (0 if exhausted). Does NOT check the
/// time window or signature; use verifyMandate / authorizeSpend for the full gate.
function remainingUnder(Mandate calldata m) external view returns (uint256) {
uint256 already = spentUnder[mandateHash(m)];
return already >= m.maxAmount ? 0 : m.maxAmount - already;
}
/// @notice True iff `m` is within its validity window at the current block time.
function isWithinWindow(Mandate calldata m) external view returns (bool) {
return block.timestamp >= m.periodStart && block.timestamp <= m.periodEnd && m.periodEnd >= m.periodStart;
}
}