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.
289 lines
14 KiB
Solidity
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;
|
|
}
|
|
}
|
|
}
|