aere-contracts/contracts/pqc/AerePQCOrderAuthorizer.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

289 lines
14 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import {PQCGaslessOrder, IAerePQCOrderAuthorizer} from "../intents/IAerePQCOrder.sol";
/**
* @title AerePQCOrderAuthorizer — a crypto-agility CONSUMER that authorizes ERC-7683
* cross-chain intents with post-quantum signatures (chain 2800)
*
* @notice This is the "PQC-order authorizer" for the AERE intent path. It turns a
* declarative ERC-7683 order plus a NIST post-quantum signature (Falcon-512/
* 1024, ML-DSA-44, SLH-DSA-128s) into an on-chain accept/reject, verified by
* AERE's LIVE native PQC precompiles behind the governed AereCryptoRegistry
* (0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5).
*
* Like {AereHybridAuthorizer}, it never pins a scheme. Every check first calls
* {AereCryptoRegistry.resolveActive} to follow the successor chain to the live
* ACTIVE row, then verifies through the registry's routed verifier. So when the
* Foundation retires a scheme on the REGISTRY with the standard swap
*
* 1. addAlgorithm(newScheme) // register the successor, ACTIVE
* 2. setSuccessor(oldId, newId) // point old -> new
* 3. setStatus(oldId, DEPRECATED|REVOKED)
*
* every settler that routes orders through this authorizer starts accepting the
* NEW scheme and rejecting the OLD one on the very next block, with NO redeploy
* of this authorizer or of any consuming SpokePool.
*
* @dev ORDER DIGEST. The 32-byte message the PQC key signs is a standard EIP-712
* digest, domain-separated to the CONSUMING SETTLER (its address is the
* verifyingContract), so one authorizer serves many pools and a signature for
* pool A never authorizes an order at pool B. The struct hash commits to the
* funder, the replay nonce, the chain, both deadlines, the requested algorithm
* id, the order-data type and payload hash, and the authorized public key. A
* solver's fill therefore settles only against an order a lattice/hash key
* actually authorized.
*
* @dev HONEST SCOPE. Application-layer authorization only; it holds no funds (no
* payable function, no custody, no selfdestruct). It changes NOTHING about AERE
* consensus: blocks are still produced and signed by Besu QBFT validators with
* classical ECDSA / secp256k1. All post-quantum verification is delegated to the
* SAME live native precompiles the registry routes to. This is NOT post-quantum
* consensus. The token custody and ERC-7683 order lifecycle live in the settler
* (AereSpokePool); this contract only answers "is this order PQC-authorized?".
*
* @dev POLICY. Only an ACTIVE signature scheme ever authorizes. {resolveActive}
* returns the first ACTIVE row on the successor chain, so a REVOKED id is never
* used (it is skipped for its ACTIVE successor, or, with no successor, the
* fail-closed check returns false); a DEPRECATED id with an ACTIVE successor
* routes to the successor; and a hash-only entry (e.g. SHAKE256) authorizes
* nothing because {AereCryptoRegistry.verify} refuses to route it.
*/
interface IAereCryptoRegistry {
/// @notice Follow the successor chain to the first ACTIVE id. Reverts on unknown id,
/// a dead-end with no ACTIVE row, or a cycle.
function resolveActive(uint256 algorithmId) external view returns (uint256);
/// @notice Fail-closed routed signature verification (returns false, never reverts,
/// for unknown/hash-only/UNKNOWN/REVOKED/malformed inputs).
function verify(uint256 algorithmId, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
external
view
returns (bool);
/// @notice True iff `id` is a signature scheme safe to adopt for NEW work (ACTIVE).
function isUsable(uint256 id) external view returns (bool);
}
contract AerePQCOrderAuthorizer is Ownable, IAerePQCOrderAuthorizer {
// ==========================================================================
// Immutable wiring
// ==========================================================================
/// @notice The governed crypto-agility registry this authorizer consumes.
IAereCryptoRegistry public immutable registry;
// ==========================================================================
// EIP-712 digest constants
// ==========================================================================
bytes32 private constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 private constant DOMAIN_NAME = keccak256("AerePQCOrder");
bytes32 private constant DOMAIN_VERSION = keccak256("1");
/// @notice The order struct typehash. `orderDataHash` = keccak256(orderData) and
/// `pubKeyHash` = keccak256(pubKey) keep the digest fixed-size while binding
/// the full payload and the authorized key.
bytes32 public constant PQC_ORDER_TYPEHASH = keccak256(
"AerePQCOrder(address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,uint256 algorithmId,bytes32 orderDataType,bytes32 orderDataHash,bytes32 pubKeyHash)"
);
// ==========================================================================
// Storage
// ==========================================================================
/**
* @notice The governed DEFAULT algorithm id used when an order sets `algorithmId == 0`.
* Owner-governed. It is re-resolved through {resolveActive} on every use, so
* it auto-follows a registry swap even before {migrate} crystallizes it.
*/
uint256 public defaultAlgorithmId;
// Authorization ledger (state-changing provenance surface; holds no value).
/// @notice Number of successful {authorizeOrder} calls recorded.
uint256 public authorizationCount;
/// @notice The ACTIVE algorithm id used by the most recent successful authorization.
uint256 public lastResolvedId;
/// @notice The order digest of the most recent successful authorization.
bytes32 public lastOrderDigest;
// ==========================================================================
// Events
// ==========================================================================
event DefaultAlgorithmSet(uint256 indexed id);
event DefaultMigrated(uint256 indexed oldId, uint256 indexed newId);
event OrderAuthorized(
address indexed settler, address indexed user, uint256 indexed resolvedId, bytes32 orderDigest, uint256 nonce
);
// ==========================================================================
// Errors
// ==========================================================================
error ZeroRegistry();
error UnusableAlgorithm(uint256 id); // default id does not resolve to a usable ACTIVE signature scheme
error NotCurrentDefault(uint256 got, uint256 current); // migrate(oldId) with oldId != defaultAlgorithmId
error OrderAuthorizationFailed(uint256 requestedId, uint256 resolvedId);
// ==========================================================================
// Constructor
// ==========================================================================
/**
* @param registry_ the AereCryptoRegistry to route through (non-zero).
* @param defaultAlgorithmId_ the initial governed default for algorithmId == 0 orders.
* It MUST resolve, via the registry, to a usable ACTIVE
* signature scheme now (fail-fast "safe default"), so the
* contract can never be born pointing at an unusable or
* hash-only default.
*/
constructor(address registry_, uint256 defaultAlgorithmId_) {
if (registry_ == address(0)) revert ZeroRegistry();
registry = IAereCryptoRegistry(registry_);
if (!_resolvesToUsable(defaultAlgorithmId_)) revert UnusableAlgorithm(defaultAlgorithmId_);
defaultAlgorithmId = defaultAlgorithmId_;
emit DefaultAlgorithmSet(defaultAlgorithmId_);
}
// ==========================================================================
// Owner: default governance
// ==========================================================================
/**
* @notice Set the governed default algorithm id (used for algorithmId == 0 orders).
* Owner only. The id must resolve to a usable ACTIVE signature scheme.
*/
function setDefaultAlgorithm(uint256 id) external onlyOwner {
if (!_resolvesToUsable(id)) revert UnusableAlgorithm(id);
defaultAlgorithmId = id;
emit DefaultAlgorithmSet(id);
}
/**
* @notice Owner helper: crystallize the default onto the ACTIVE successor of `oldId`
* after a registry swap. `oldId` must equal the current {defaultAlgorithmId}.
* Because every check already re-resolves, this is an optimization/clarity
* step, not a correctness requirement. Reverts (from {resolveActive}) if the
* chain dead-ends or cycles.
* @return newId the ACTIVE id the default now points at (== oldId if already ACTIVE).
*/
function migrate(uint256 oldId) external onlyOwner returns (uint256 newId) {
if (defaultAlgorithmId != oldId) revert NotCurrentDefault(oldId, defaultAlgorithmId);
newId = registry.resolveActive(oldId);
if (newId != oldId) {
defaultAlgorithmId = newId;
emit DefaultMigrated(oldId, newId);
}
}
// ==========================================================================
// Order digest
// ==========================================================================
/// @inheritdoc IAerePQCOrderAuthorizer
function orderDigest(address settler, PQCGaslessOrder calldata order, bytes calldata pubKey)
public
view
override
returns (bytes32)
{
bytes32 structHash = keccak256(
abi.encode(
PQC_ORDER_TYPEHASH,
order.user,
order.nonce,
order.originChainId,
order.openDeadline,
order.fillDeadline,
order.algorithmId,
order.orderDataType,
keccak256(order.orderData),
keccak256(pubKey)
)
);
return keccak256(abi.encodePacked("\x19\x01", _domainSeparator(settler), structHash));
}
// ==========================================================================
// Authorization (fail-closed view)
// ==========================================================================
/// @inheritdoc IAerePQCOrderAuthorizer
function checkOrder(address settler, PQCGaslessOrder calldata order, bytes calldata pubKey, bytes calldata signature)
public
view
override
returns (bool)
{
uint256 requestedId = order.algorithmId == 0 ? defaultAlgorithmId : order.algorithmId;
uint256 active;
try registry.resolveActive(requestedId) returns (uint256 a) {
active = a;
} catch {
return false; // unknown id, dead-end, or cycle -> not authorized
}
bytes32 digest = orderDigest(settler, order, pubKey);
try registry.verify(active, pubKey, digest, signature) returns (bool ok) {
return ok;
} catch {
return false;
}
}
// ==========================================================================
// Authorization (state-changing, recorded)
// ==========================================================================
/**
* @notice State-changing authorization for a PQC order under the ACTIVE scheme
* reachable from `order.algorithmId` (0 => governed default). Reverts if the
* successor chain has no ACTIVE row (surfacing the registry reason) or
* {OrderAuthorizationFailed} if the routed verifier returns false. On success
* it bumps {authorizationCount} and emits {OrderAuthorized}. This is a
* provenance surface for standalone use; the consuming settler verifies with
* the gas-light fail-closed {checkOrder} view during open().
* @return resolvedId the ACTIVE id the signature was verified under.
*/
function authorizeOrder(
address settler,
PQCGaslessOrder calldata order,
bytes calldata pubKey,
bytes calldata signature
) external returns (uint256 resolvedId) {
uint256 requestedId = order.algorithmId == 0 ? defaultAlgorithmId : order.algorithmId;
resolvedId = registry.resolveActive(requestedId); // reverts on unknown/dead-end/cycle
bytes32 digest = orderDigest(settler, order, pubKey);
bool ok = registry.verify(resolvedId, pubKey, digest, signature);
if (!ok) revert OrderAuthorizationFailed(requestedId, resolvedId);
authorizationCount += 1;
lastResolvedId = resolvedId;
lastOrderDigest = digest;
emit OrderAuthorized(settler, order.user, resolvedId, digest, order.nonce);
}
// ==========================================================================
// Internal
// ==========================================================================
/// @dev The EIP-712 domain separator bound to `settler` as the verifyingContract, so
/// a PQC order authorized for one pool cannot authorize an order at another.
function _domainSeparator(address settler) internal view returns (bytes32) {
return keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, settler));
}
/// @dev True iff `id` resolves (via the successor chain) to a usable ACTIVE signature
/// scheme. Fail-closed: a revert in resolveActive is treated as "not usable".
function _resolvesToUsable(uint256 id) internal view returns (bool) {
try registry.resolveActive(id) returns (uint256 active) {
return registry.isUsable(active);
} catch {
return false;
}
}
}