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

492 lines
26 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title AereCryptoRegistry, a governed cryptographic-agility registry for AERE (chain 2800)
*
* @notice AERE has FIVE live native cryptographic precompiles on mainnet 2800
* (activated at block 9,189,161): four post-quantum SIGNATURE verifiers and one
* post-quantum HASH function. This registry turns the flat fact "we verify N
* schemes" into a governed table:
*
* algorithmId -> { verifier address, wire format, status, gas, successor }
*
* so a consumer integrates against ONE stable surface (this registry) instead of
* hardcoding a precompile address. When a scheme is deprecated, broken, or
* superseded, the Foundation (later the Timelock) flips its status and points it
* at a successor. Consumers that route through {resolveActive} follow the upgrade
* automatically, WITHOUT any consumer contract being redeployed.
*
* @dev HONEST SCOPE. This is an application-layer routing + policy registry. It does
* NOT change AERE consensus (blocks are still produced and signed by Besu QBFT
* validators with classical ECDSA / secp256k1). It calls the SAME live native
* precompiles that {AerePQCAttestation} calls, and it builds the SAME wire-format
* input those precompiles parse. No funds are ever held. There is no payable
* function, no selfdestruct, and no fund custody of any kind.
*
* @dev THE FIVE LIVE PRECOMPILES (chain 2800):
* scheme 1 = Falcon-512 at 0x...0AE1 (signature) pk 897, esig header 0x29
* scheme 2 = Falcon-1024 at 0x...0AE2 (signature) pk 1793, esig header 0x2A
* scheme 3 = ML-DSA-44 at 0x...0AE3 (signature) pk 1312, sig 2420 (FIPS 204)
* scheme 4 = SLH-DSA-128s at 0x...0AE4 (signature) pk 32, sig 7856 (FIPS 205)
* scheme 5 = SHAKE256 at 0x...0AE5 (HASH, NOT a signature scheme)
*
* @dev PRECOMPILE INPUT ENCODING (mirrors AerePQCAttestation._buildInput exactly):
*
* Falcon family (envelope format, selected iff esigHeader != 0):
* signature envelope = nonce(40) || esig (esig[0] == esigHeader)
* input = pk || sm
* sm = esigLen(uint16 big-endian) || nonce(40) || message(32) || esig
*
* Fixed-concatenation family (selected iff esigHeader == 0):
* signature = the raw fixed-length signature (sigLen bytes)
* input = pk || signature || message(32)
*
* Every verifier returns a 32-byte word: 0x..01 == valid, anything else invalid.
* An empty/short/zero return (e.g. a pre-fork address with no precompile) is
* treated as INVALID, so a stale route can never report a false positive.
*
* @dev WIRE-FORMAT DISCRIMINATOR. The registry selects the envelope (Falcon) format
* when the algorithm's `esigHeader` is non-zero, and the fixed-concatenation
* format otherwise. This keeps the registry agile across future schemes that
* share one of the two families (e.g. a new Falcon-like or a new fixed-length
* lattice scheme) without any code change: only a new row is added. A genuinely
* NEW wire family would require a registry upgrade, but consumers still never
* change their integration (they keep calling verify(id, ...)).
*/
contract AereCryptoRegistry is Ownable {
// ==========================================================================
// Types
// ==========================================================================
/// @notice Lifecycle of a registered algorithm.
/// UNKNOWN = never registered (the zero value; no stored row ever has this).
/// ACTIVE = registered and recommended for new use; verify() runs.
/// DEPRECATED= verifiable for legacy signatures but NOT recommended for new use;
/// verify() still runs, but isActive()==false and isUsable()==false.
/// REVOKED = broken/withdrawn; verify() always returns false. Terminal by policy.
enum Status {
UNKNOWN,
ACTIVE,
DEPRECATED,
REVOKED
}
/// @param name human-readable identifier, e.g. "Falcon-512".
/// @param scheme the AERE scheme id (1..5 for the live set; informational).
/// @param verifier the address staticcalled to verify (a precompile or contract).
/// @param pubKeyLen exact public-key length in bytes the verifier expects.
/// @param sigLen exact signature length for the fixed-concatenation family
/// (0 for the variable-length Falcon envelope family).
/// @param esigHeader Falcon esig header byte; NON-ZERO selects the Falcon envelope
/// wire format, ZERO selects the fixed-concatenation format.
/// @param status lifecycle status (see {Status}).
/// @param gasEstimate ADVISORY metadata only: a rough gas budget hint for consumers.
/// It does NOT cap or influence the actual staticcall gas.
/// @param successorId the algorithm id that supersedes this one (0 == none).
/// @param addedBlock block number the row was added at.
/// @param isSignature true for signature schemes; false for hash-only entries
/// (e.g. SHAKE256), which verify() refuses to route.
struct Algorithm {
string name;
uint8 scheme;
address verifier;
uint16 pubKeyLen;
uint32 sigLen;
uint8 esigHeader;
Status status;
uint256 gasEstimate;
uint256 successorId;
uint64 addedBlock;
bool isSignature;
}
// ==========================================================================
// Constants
// ==========================================================================
uint256 internal constant FALCON_NONCE_LEN = 40;
uint256 internal constant NAME_MAX_LEN = 64;
// Live precompile addresses on chain 2800 (used only by the seed function).
address internal constant PRECOMPILE_FALCON512 = address(0x0AE1);
address internal constant PRECOMPILE_FALCON1024 = address(0x0AE2);
address internal constant PRECOMPILE_MLDSA44 = address(0x0AE3);
address internal constant PRECOMPILE_SLHDSA128S = address(0x0AE4);
address internal constant PRECOMPILE_SHAKE256 = address(0x0AE5);
// Advisory gas-budget PLACEHOLDERS for the seeded live schemes. These are hints for
// consumers, not gas caps; the operator can refine them by adding a new row. The
// magnitudes reflect the native precompile costs on the Besu PQC fork, not the older
// pure-EVM verifiers.
uint256 internal constant GAS_FALCON512 = 120_000;
uint256 internal constant GAS_FALCON1024 = 145_000;
uint256 internal constant GAS_MLDSA44 = 351_000;
uint256 internal constant GAS_SLHDSA128S = 400_000;
uint256 internal constant GAS_SHAKE256 = 30_000;
// ==========================================================================
// Canonical scheme identifiers (crypto-agility, ADDITIVE)
// ==========================================================================
//
// The `scheme` field of an Algorithm row is an informational uint8 tag naming the NIST
// parameter set a row implements (see the struct doc). These named constants pin the
// CANONICAL scheme-ID space so governance, consumers, and off-chain tooling all agree
// on the number. Ids 1..4 match AerePQCKeyRegistry's SCHEME_* exactly; id 5 is this
// registry's SHAKE256 hash entry; ids 6..14 RESERVE future NIST parameter sets.
// Numbering rules and the full rationale live in docs/PQ-AGILITY-HARDENING-2026-07-18.md.
//
// HONESTY: declaring a scheme id here does NOT register, seed, activate, or route
// anything, and does NOT assert a verifier exists. Ids 1..5 are the only schemes with a
// live precompile on chain 2800 today (0x0AE1..0x0AE5, activated block 9,189,161). Ids
// 6..14 are RESERVED identifiers; a scheme becomes usable only when governance adds a
// row via addAlgorithm pointing at a real, conformance-gated verifier. These constants
// are purely additive reference values: seedLiveSchemes, verify, resolveActive, and
// every other function are unchanged, and the constants occupy no storage.
// Genesis block (ids 1..5): the five live schemes seedLiveSchemes registers.
uint8 public constant SCHEME_FALCON512 = 1; // Falcon-512 (0x0AE1, live, signature)
uint8 public constant SCHEME_FALCON1024 = 2; // Falcon-1024 (0x0AE2, live, signature)
uint8 public constant SCHEME_MLDSA44 = 3; // ML-DSA-44 FIPS 204 (0x0AE3, live, signature)
uint8 public constant SCHEME_SLHDSA128S = 4; // SLH-DSA-128s FIPS 205 (0x0AE4, live, signature)
uint8 public constant SCHEME_SHAKE256 = 5; // SHAKE256 FIPS 202 (0x0AE5, live, hash/XOF)
// Reserved future schemes (ids 6..14): NO live verifier yet, reserved numbers only.
uint8 public constant SCHEME_MLDSA65 = 6; // ML-DSA-65 FIPS 204 (signature)
uint8 public constant SCHEME_MLDSA87 = 7; // ML-DSA-87 FIPS 204 (signature)
uint8 public constant SCHEME_FALCON512_PADDED = 8; // Falcon-512 padded FN-DSA draft (signature, fixed-len)
uint8 public constant SCHEME_FALCON1024_PADDED = 9; // Falcon-1024 padded FN-DSA draft (signature, fixed-len)
uint8 public constant SCHEME_SLHDSA192S = 10; // SLH-DSA-192s FIPS 205 (signature)
uint8 public constant SCHEME_SLHDSA256S = 11; // SLH-DSA-256s FIPS 205 (signature)
uint8 public constant SCHEME_MLKEM512 = 12; // ML-KEM-512 FIPS 203 (KEM, non-signature, discovery-only)
uint8 public constant SCHEME_MLKEM768 = 13; // ML-KEM-768 FIPS 203 (KEM, non-signature, discovery-only)
uint8 public constant SCHEME_MLKEM1024 = 14; // ML-KEM-1024 FIPS 203 (KEM, non-signature, discovery-only)
// Auxiliary primitive (id 15): Falcon HashToPoint. Like SHAKE256 (id 5) this names a
// HASH / auxiliary primitive, NOT a signature or KEM parameter set, so any future
// registry row for it would be added isSignature == false and is never routed by the
// signature-only verify() path. Id 15 is the next append-only number after the reserved
// block (ids 1..14); the earlier soft "recommendation" in PQ-AGILITY-HARDENING-2026-07-18
// to earmark ids 15+ for SLH-DSA fast variants was non-binding (no constant was ever
// declared for those), so those parameter sets take the next free ids (16+) when a
// concrete verifier is proposed. Append-only integrity is intact: no existing id moves.
uint8 public constant SCHEME_FALCON_HASHTOPOINT = 15; // Falcon HashToPoint (SHAKE256 rejection sampler)
// ==========================================================================
// NEW native precompiles - TESTNET ONLY (chain 2801), NOT mainnet 2800
// ==========================================================================
//
// Two additional native precompiles were built on the AERE Besu PQC fork and are live on
// the AERE TESTNET ONLY. They are NOT activated on mainnet 2800 as of 2026-07-18: mainnet
// still has exactly the FIVE precompiles 0x0AE1..0x0AE5 (activated block 9,189,161). Their
// gated mainnet-activation plan is docs/PQC-PRECOMPILES-MAINNET-ACTIVATION-PLAN-2026-07-18.md
// (founder + external-audit gated). Wire formats (see AereMLKEM768.sol and AerePQCPrecompiles.sol):
//
// 0x0AE6 ML-KEM-768 (FIPS 203) deterministic Encaps:
// input = ek(1184) || m(32) (1216 bytes)
// output = ct(1088) || ss(32) (1120 bytes); EMPTY on malformed input
// KEM, NOT a signature. Canonical scheme id = SCHEME_MLKEM768 (13).
// 0x0AE7 Falcon HashToPoint (SHAKE256 rejection sampler):
// input = logn(1) || nonce(40) || message(rest), logn = 9 (n=512) or 10 (n=1024)
// output = 2n bytes = n big-endian uint16 coefficients in [0, q), q=12289; EMPTY on malformed
// Auxiliary hash/lattice primitive, NOT a signature. Canonical scheme id = SCHEME_FALCON_HASHTOPOINT (15).
//
// HONESTY / ADDITIVE: these are reference constants ONLY. They are NOT seeded by
// seedLiveSchemes, NOT routed by verify / resolveActive / precompileFor, and add no storage
// and no code path. Declaring them makes NO on-chain claim that either precompile exists on
// mainnet 2800; on mainnet a staticcall to 0x0AE6/0x0AE7 returns empty today, so every
// consumer wrapper fail-closes until a governance-gated activation. A registry row for either
// is created only after the activation + conformance gate in the plan doc passes (addAlgorithm,
// onlyOwner). Address-literal style matches the seeded PRECOMPILE_* constants above.
/// @notice TESTNET-ONLY address of the ML-KEM-768 (FIPS 203) deterministic-Encaps precompile
/// on the AERE Besu PQC fork. NOT on mainnet 2800. Reference value only: never seeded
/// or routed by this registry.
address public constant PRECOMPILE_MLKEM768_TESTNET = address(0x0AE6);
/// @notice TESTNET-ONLY address of the Falcon HashToPoint precompile on the AERE Besu PQC
/// fork. NOT on mainnet 2800. Reference value only: never seeded or routed here.
address public constant PRECOMPILE_FALCON_HASHTOPOINT_TESTNET = address(0x0AE7);
// ==========================================================================
// Storage
// ==========================================================================
// Algorithm ids are 1-based and monotonic. Id 0 is the "none"/unknown sentinel
// (also used as the "no successor" value), so it is never a valid row.
mapping(uint256 => Algorithm) private _algorithms;
uint256 private _count;
bool private _seeded;
// ==========================================================================
// Events
// ==========================================================================
event AlgorithmAdded(
uint256 indexed id, uint8 indexed scheme, address indexed verifier, string name, bool isSignature
);
event StatusChanged(uint256 indexed id, Status oldStatus, Status newStatus);
event SuccessorSet(uint256 indexed id, uint256 indexed successorId);
event LiveSchemesSeeded(uint256 count);
// ==========================================================================
// Errors
// ==========================================================================
error UnknownAlgorithm(uint256 id);
error InvalidAlgorithmParams();
error InvalidStatusTarget(); // attempt to set UNKNOWN
error AlgorithmRevoked(uint256 id); // REVOKED is terminal
error InvalidSuccessor(); // self-successor
error NoActiveSuccessor(uint256 id); // resolveActive dead-ends with no ACTIVE row
error SuccessorCycle(uint256 id); // resolveActive detected a cycle
error UnknownScheme(uint8 scheme);
error AlreadySeeded();
// ==========================================================================
// Owner: registration
// ==========================================================================
/**
* @notice Register a new algorithm row. Owner only. Returns the fresh monotonic id.
* The row is created ACTIVE with no successor.
* @dev Length sanity: name in (0, 64]; verifier != 0; for a signature scheme
* pubKeyLen != 0, and for the fixed-concatenation family (esigHeader == 0)
* sigLen != 0. Hash-only entries (isSignature == false) skip the sig checks.
*/
function addAlgorithm(
string calldata name,
uint8 scheme,
address verifier,
uint16 pubKeyLen,
uint32 sigLen,
uint8 esigHeader,
uint256 gasEstimate,
bool isSignature
) external onlyOwner returns (uint256 id) {
return _addAlgorithm(name, scheme, verifier, pubKeyLen, sigLen, esigHeader, gasEstimate, isSignature);
}
function _addAlgorithm(
string memory name,
uint8 scheme,
address verifier,
uint16 pubKeyLen,
uint32 sigLen,
uint8 esigHeader,
uint256 gasEstimate,
bool isSignature
) internal returns (uint256 id) {
uint256 nameLen = bytes(name).length;
if (nameLen == 0 || nameLen > NAME_MAX_LEN) revert InvalidAlgorithmParams();
if (verifier == address(0)) revert InvalidAlgorithmParams();
if (isSignature) {
if (pubKeyLen == 0) revert InvalidAlgorithmParams();
// Fixed-concatenation family needs an exact signature length.
if (esigHeader == 0 && sigLen == 0) revert InvalidAlgorithmParams();
}
_count += 1;
id = _count;
_algorithms[id] = Algorithm({
name: name,
scheme: scheme,
verifier: verifier,
pubKeyLen: pubKeyLen,
sigLen: sigLen,
esigHeader: esigHeader,
status: Status.ACTIVE,
gasEstimate: gasEstimate,
successorId: 0,
addedBlock: uint64(block.number),
isSignature: isSignature
});
emit AlgorithmAdded(id, scheme, verifier, name, isSignature);
}
/**
* @notice Change an algorithm's lifecycle status. Owner only.
* @dev May set ACTIVE (reactivate a DEPRECATED row), DEPRECATED, or REVOKED.
* Cannot set UNKNOWN (the sentinel). REVOKED is TERMINAL: a revoked row can
* never be changed again. To bring back a fixed variant of a revoked scheme,
* add a NEW algorithm id and point the revoked row's successor at it.
*/
function setStatus(uint256 id, Status newStatus) external onlyOwner {
if (!_exists(id)) revert UnknownAlgorithm(id);
if (newStatus == Status.UNKNOWN) revert InvalidStatusTarget();
Algorithm storage a = _algorithms[id];
if (a.status == Status.REVOKED) revert AlgorithmRevoked(id);
Status old = a.status;
a.status = newStatus;
emit StatusChanged(id, old, newStatus);
}
/**
* @notice Point `id`'s successor at `successorId` (the row that supersedes it). Owner
* only. Both must exist and must differ. Longer cycles are tolerated at write
* time but are caught and reverted by {resolveActive}'s bounded walk.
*/
function setSuccessor(uint256 id, uint256 successorId) external onlyOwner {
if (!_exists(id)) revert UnknownAlgorithm(id);
if (!_exists(successorId)) revert UnknownAlgorithm(successorId);
if (successorId == id) revert InvalidSuccessor();
_algorithms[id].successorId = successorId;
emit SuccessorSet(id, successorId);
}
/**
* @notice Seed the five live chain-2800 schemes. Owner only, one-shot (idempotent
* guard). Registers Falcon-512/1024, ML-DSA-44 and SLH-DSA-128s as signature
* schemes pointing at their live precompiles, plus SHAKE256 as a hash-only
* entry (isSignature == false) that {verify} refuses to route.
*/
function seedLiveSchemes() external onlyOwner {
if (_seeded) revert AlreadySeeded();
_seeded = true;
_addAlgorithm("Falcon-512", 1, PRECOMPILE_FALCON512, 897, 0, 0x29, GAS_FALCON512, true);
_addAlgorithm("Falcon-1024", 2, PRECOMPILE_FALCON1024, 1793, 0, 0x2A, GAS_FALCON1024, true);
_addAlgorithm("ML-DSA-44", 3, PRECOMPILE_MLDSA44, 1312, 2420, 0, GAS_MLDSA44, true);
_addAlgorithm("SLH-DSA-128s", 4, PRECOMPILE_SLHDSA128S, 32, 7856, 0, GAS_SLHDSA128S, true);
_addAlgorithm("SHAKE256", 5, PRECOMPILE_SHAKE256, 0, 0, 0, GAS_SHAKE256, false);
emit LiveSchemesSeeded(_count);
}
// ==========================================================================
// Verification
// ==========================================================================
/**
* @notice Verify a signature for `algorithmId` against the routed verifier.
*
* POLICY (fail-closed): returns FALSE, never reverts, when the id is unknown,
* the row is a hash-only entry (isSignature == false), the status is UNKNOWN
* or REVOKED, or the supplied lengths are malformed. A DEPRECATED row still
* verifies (legacy support); check {isUsable} before adopting a scheme for new
* work. Returns TRUE iff the routed verifier answers 0x..01.
*
* @param algorithmId the registry id to verify under.
* @param pubKey the raw public key for the scheme (exactly pubKeyLen bytes).
* @param messageHash the 32-byte message that was signed.
* @param signature the signature envelope for the scheme's wire family.
*/
function verify(uint256 algorithmId, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
external
view
returns (bool)
{
if (!_exists(algorithmId)) return false;
Algorithm storage a = _algorithms[algorithmId];
if (!a.isSignature) return false;
Status st = a.status;
if (st == Status.UNKNOWN || st == Status.REVOKED) return false; // ACTIVE or DEPRECATED proceed
if (pubKey.length != a.pubKeyLen) return false;
bytes memory input;
if (a.esigHeader != 0) {
// Falcon envelope family: signature = nonce(40) || esig.
if (signature.length <= FALCON_NONCE_LEN) return false;
uint256 esigLen = signature.length - FALCON_NONCE_LEN;
if (esigLen > type(uint16).max) return false;
bytes calldata nonce = signature[0:FALCON_NONCE_LEN];
bytes calldata esig = signature[FALCON_NONCE_LEN:];
// sm = esigLen(uint16 BE) || nonce(40) || message(32) || esig
bytes memory sm = abi.encodePacked(uint16(esigLen), nonce, messageHash, esig);
input = abi.encodePacked(pubKey, sm);
} else {
// Fixed-concatenation family: input = pk || sig || message.
if (signature.length != a.sigLen) return false;
input = abi.encodePacked(pubKey, signature, messageHash);
}
(bool ok, bytes memory ret) = a.verifier.staticcall(input);
return ok && ret.length >= 32 && ret[31] == 0x01;
}
/**
* @notice Follow the successor chain from `algorithmId` to the first ACTIVE row and
* return its id. Reverts {NoActiveSuccessor} if the chain dead-ends without an
* ACTIVE row, and {SuccessorCycle} if a cycle is detected. The walk is bounded
* by the number of registered algorithms, so it always terminates.
*/
function resolveActive(uint256 algorithmId) external view returns (uint256) {
uint256 id = algorithmId;
// At most _count distinct rows exist; a walk longer than that is a cycle.
for (uint256 hops = 0; hops <= _count; hops++) {
if (!_exists(id)) revert UnknownAlgorithm(id);
Algorithm storage a = _algorithms[id];
if (a.status == Status.ACTIVE) return id;
uint256 next = a.successorId;
if (next == 0) revert NoActiveSuccessor(algorithmId);
id = next;
}
revert SuccessorCycle(algorithmId);
}
// ==========================================================================
// Views
// ==========================================================================
/// @notice The full row for `id`. Reverts if `id` does not exist.
function getAlgorithm(uint256 id) external view returns (Algorithm memory) {
if (!_exists(id)) revert UnknownAlgorithm(id);
return _algorithms[id];
}
/// @notice Number of registered algorithms. Ids run 1..algorithmCount().
function algorithmCount() external view returns (uint256) {
return _count;
}
/// @notice Lifecycle status of `id`; UNKNOWN for any id that was never registered.
function statusOf(uint256 id) external view returns (Status) {
if (!_exists(id)) return Status.UNKNOWN;
return _algorithms[id].status;
}
/// @notice True iff `id` exists and is ACTIVE.
function isActive(uint256 id) external view returns (bool) {
return _exists(id) && _algorithms[id].status == Status.ACTIVE;
}
/**
* @notice True iff `id` is a signature scheme that is safe to adopt for NEW work:
* it exists, isSignature == true, and status == ACTIVE. A DEPRECATED row is
* still verifiable (see {verify}) but is NOT usable, and a hash-only entry is
* never a usable signature verifier.
*/
function isUsable(uint256 id) external view returns (bool) {
if (!_exists(id)) return false;
Algorithm storage a = _algorithms[id];
return a.isSignature && a.status == Status.ACTIVE;
}
/// @notice The verifier address currently routing an ACTIVE algorithm for `scheme`.
/// Reverts {UnknownScheme} if no ACTIVE row serves that scheme.
function precompileFor(uint8 scheme) external view returns (address) {
uint256 n = _count;
for (uint256 id = 1; id <= n; id++) {
Algorithm storage a = _algorithms[id];
if (a.scheme == scheme && a.status == Status.ACTIVE) return a.verifier;
}
revert UnknownScheme(scheme);
}
/// @notice Whether the one-shot {seedLiveSchemes} has already run.
function seeded() external view returns (bool) {
return _seeded;
}
// ==========================================================================
// Internal
// ==========================================================================
function _exists(uint256 id) internal view returns (bool) {
return id != 0 && id <= _count;
}
}