// 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; } }