// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title AerePQAttestationKeyRegistry, the post-quantum finality-attestation key * registry for Aere Network validators (chain 2800). * * @notice This registry holds, for each Aere Network validator, a HASH-BASED * attestation public key (Winternitz-XMSS / leanSig style, one-time or * few-time hash-based signatures). A hash-based signature is post-quantum * from hash collision and preimage resistance ALONE, so it survives a * cryptographically-relevant quantum adversary that can forge the classical * ECDSA (secp256k1) seals Aere consensus uses. This key is SEPARATE from a * validator's ECDSA consensus key: it is the PQ FINALITY key, used only for * an ADDITIVE post-quantum attestation over each finalized block. * * @notice The registry produces a deterministic `validatorSetRoot()`, a binding * commitment to the enrolled validators and their current attestation keys. * The off-chain aggregator reconstructs this same root, and * AereFinalityCertificateVerifier binds every certificate to it, so a * certificate proven against a stale or forged key set is rejected on-chain. * * @dev THE HONEST SCOPE BOUNDARY, read before trusting or describing anything. * * 1. THIS DOES NOT CHANGE AERE CONSENSUS. Blocks are STILL produced and * finalized by Besu QBFT validators signing classical ECDSA (secp256k1) * committed seals. This registry, and the certificate that reads it, are * PURELY ADDITIVE: validators keep signing ECDSA for consensus and ALSO * sign a hash-based attestation over each finalized block. Nothing here * flips consensus, and PQ finality is not live. Never imply consensus is * post-quantum. * * 2. NO ON-CHAIN HASH-BASED VERIFICATION HAPPENS HERE. This contract stores * attestation public keys and commits to them in a root. The hash-based * signatures themselves are verified OFF-CHAIN, inside a zkVM aggregation * circuit (the [MEASURE] research component, see * docs/AERE-PQ-FINALITY-CERTIFICATE.md), which recomputes this root from * the witnessed keys. This registry is the on-chain anchor for that root. * * 3. APPEND-ONLY, FAIL-CLOSED, NO SILENT REWRITE. Every key ever registered * is retained in `_keys` and reachable via the history views. A rotation * never overwrites a key's material: it APPENDS a new record, marks the * predecessor superseded (a metadata flag, not a deletion), and advances a * monotonic per-validator `keyEpoch`. One-time and few-time hash-based keys * MUST rotate as their signing capacity is used, so rotation is expected and * cheap. A validator may only register its OWN key (msg.sender == validator); * the owner cannot forge a validator's PQ key. * * 4. GOVERNED MEMBERSHIP, SELF-CUSTODIED KEYS. The owner (Aere Foundation, * later the Timelock) enrolls the addresses that ARE validators, mirroring * how QBFT consensus enrollment is curated off-chain via the Foundation * Ledger and QBFT extraData. Enrollment defines WHO counts toward the * finality quorum and fixes the root ordering. Each validator then registers * its own attestation key. There is no fund custody and no key-forgery path. */ contract AerePQAttestationKeyRegistry is Ownable { // ========================================================================== // Hash-based scheme labels (post-quantum from hashing alone) // ========================================================================== // // These name the hash-based signature family a validator's attestation key uses. // They are hash-based, so their post-quantum security rests only on the collision // and preimage resistance of the underlying hash, NOT on any lattice or discrete-log // assumption. The scheme is recorded and committed into the root; the actual // signature verification happens in the off-chain aggregation circuit. uint8 public constant SCHEME_XMSS = 1; // RFC 8391 XMSS (stateful, few-time hash-based) uint8 public constant SCHEME_WOTS = 2; // Winternitz OTS+ (one-time hash-based) uint8 public constant SCHEME_LEANSIG = 3; // leanSig / lean hash-based (Ethereum Lean Consensus lineage) uint8 public constant SCHEME_SLHDSA128S = 4; // SLH-DSA-128s (FIPS 205, stateless hash-based) uint8 public constant SCHEME_XMSSMT = 5; // XMSS^MT (RFC 8391 multi-tree, more few-time capacity) /// @dev Sane bounds for a hash-based attestation public key. Hash-based keys are /// SMALL (an XMSS public key is a hash root plus parameters, tens of bytes), /// so the generous upper bound only rejects obviously malformed input. uint256 internal constant MIN_PUBKEY_LEN = 32; uint256 internal constant MAX_PUBKEY_LEN = 2048; /// @dev Sentinel for "this validator has no current key". uint256 internal constant NO_KEY = type(uint256).max; // ========================================================================== // Types // ========================================================================== /// @notice An append-only attestation-key record. Never mutated except to flag it /// superseded when the owning validator rotates to a successor. struct KeyRecord { address validator; // the validator that registered this key uint8 scheme; // one of SCHEME_* uint32 epoch; // this validator's key-epoch when the key was registered (1-based) uint64 registeredBlock; // block the key was registered at uint64 supersededBlock; // block a successor superseded it (0 while current) bool superseded; // true once a successor key was registered bytes pubKey; // the raw hash-based attestation public key } /// @notice Per-validator membership + current-key pointer. struct Validator { bool enrolled; // true once the owner enrolled this address uint32 index; // position in `_validators` (fixes the root ordering) uint32 keyEpoch; // 0 = no key yet; increments on each (re)registration uint256 currentKeyId; // NO_KEY until the first key is registered } // ========================================================================== // Storage // ========================================================================== address[] private _validators; // enrollment order == root ordering mapping(address => Validator) private _validatorInfo; KeyRecord[] private _keys; // append-only; keyId is the array index mapping(address => uint256[]) private _validatorKeyHistory; // all keyIds per validator // ========================================================================== // Events // ========================================================================== event ValidatorEnrolled(address indexed validator, uint32 indexed index); event AttestationKeyRegistered( address indexed validator, uint256 indexed keyId, uint8 indexed scheme, uint32 epoch, uint256 supersededKeyId ); // ========================================================================== // Errors // ========================================================================== error AlreadyEnrolled(address validator); error NotEnrolled(address validator); error ZeroValidator(); error InvalidScheme(uint8 scheme); error InvalidPubKey(); error UnknownKey(uint256 keyId); // ========================================================================== // Owner: set membership // ========================================================================== /** * @notice Enroll `validator` into the Aere Network finality-attestation validator * set. Owner only. Enrollment defines WHO counts toward the PQ finality * quorum and fixes this validator's position in the root ordering. It does * NOT register a key: the validator registers its own attestation key next. * @dev Idempotent-guarded: re-enrolling a known validator reverts AlreadyEnrolled, * so the set is append-only and the ordering never silently shifts. */ function enrollValidator(address validator) external onlyOwner { if (validator == address(0)) revert ZeroValidator(); Validator storage v = _validatorInfo[validator]; if (v.enrolled) revert AlreadyEnrolled(validator); uint32 index = uint32(_validators.length); _validators.push(validator); v.enrolled = true; v.index = index; v.keyEpoch = 0; v.currentKeyId = NO_KEY; emit ValidatorEnrolled(validator, index); } // ========================================================================== // Validator: register / rotate own key // ========================================================================== /** * @notice Register (or rotate to) the caller's hash-based attestation public key. * Callable ONLY by an enrolled validator, for its OWN key. The first call * sets key-epoch 1; each later call appends a fresh record, marks the * previous key superseded, and advances the key-epoch, because one-time and * few-time hash-based keys must rotate as their signing capacity is spent. * Append-only: nothing is overwritten or deleted, and the full history is * queryable. * @param scheme one of SCHEME_* (a hash-based family). * @param pubKey the raw hash-based attestation public key (length-checked only). * @return keyId the identifier assigned to the new key. */ function registerAttestationKey(uint8 scheme, bytes calldata pubKey) external returns (uint256 keyId) { Validator storage v = _validatorInfo[msg.sender]; if (!v.enrolled) revert NotEnrolled(msg.sender); _validateKey(scheme, pubKey); uint256 supersededKeyId = NO_KEY; if (v.currentKeyId != NO_KEY) { supersededKeyId = v.currentKeyId; KeyRecord storage old = _keys[supersededKeyId]; old.superseded = true; old.supersededBlock = uint64(block.number); } uint32 newEpoch = v.keyEpoch + 1; keyId = _keys.length; _keys.push( KeyRecord({ validator: msg.sender, scheme: scheme, epoch: newEpoch, registeredBlock: uint64(block.number), supersededBlock: 0, superseded: false, pubKey: pubKey }) ); v.keyEpoch = newEpoch; v.currentKeyId = keyId; _validatorKeyHistory[msg.sender].push(keyId); emit AttestationKeyRegistered(msg.sender, keyId, scheme, newEpoch, supersededKeyId); } function _validateKey(uint8 scheme, bytes calldata pubKey) internal pure { if (scheme < SCHEME_XMSS || scheme > SCHEME_XMSSMT) revert InvalidScheme(scheme); if (pubKey.length < MIN_PUBKEY_LEN || pubKey.length > MAX_PUBKEY_LEN) revert InvalidPubKey(); } // ========================================================================== // Validator-set commitment (root) // ========================================================================== /** * @notice The deterministic commitment to the current validator set and their * current attestation keys. This is the value AereFinalityCertificateVerifier * binds every certificate to, and the value the off-chain aggregation circuit * MUST recompute from the witnessed keys. * * Definition (a flat, ordered keccak commitment): * leaf_i = keccak256(abi.encode(validator_i, keyEpoch_i, keyHash_i)) * root = keccak256(abi.encode(leaf_0, leaf_1, ..., leaf_{N-1})) * where keyHash_i = keccak256(currentPubKey_i), or bytes32(0) if validator_i * has not registered a key yet, and the ordering is enrollment order. * * @dev Because the root commits to each validator's address AND its current * key-epoch AND its current key material, ANY rotation (which bumps the * epoch and changes the key hash) changes the root. A certificate proven * against the pre-rotation root therefore stops matching automatically. A * Merkle tree is a drop-in alternative if inclusion proofs are wanted; a * flat commitment is sufficient because the circuit witnesses the whole set. */ function validatorSetRoot() public view returns (bytes32) { uint256 n = _validators.length; bytes32[] memory leaves = new bytes32[](n); for (uint256 i = 0; i < n; i++) { address a = _validators[i]; Validator storage v = _validatorInfo[a]; bytes32 keyHash = v.currentKeyId == NO_KEY ? bytes32(0) : keccak256(_keys[v.currentKeyId].pubKey); leaves[i] = keccak256(abi.encode(a, v.keyEpoch, keyHash)); } return keccak256(abi.encode(leaves)); } // ========================================================================== // Views // ========================================================================== /// @notice Number of enrolled validators (== N, the set size the quorum is over). function validatorCount() external view returns (uint256) { return _validators.length; } /// @notice The enrolled validator at a root-ordering position. Reverts if out of range. function validatorAt(uint256 index) external view returns (address) { if (index >= _validators.length) revert UnknownKey(index); return _validators[index]; } /// @notice The full enrolled validator set in root order. function validators() external view returns (address[] memory) { return _validators; } /// @notice True iff `validator` is enrolled in the set. function isEnrolled(address validator) external view returns (bool) { return _validatorInfo[validator].enrolled; } /// @notice The number of enrolled validators that currently have an attestation key. function activeKeyCount() public view returns (uint256 count) { uint256 n = _validators.length; for (uint256 i = 0; i < n; i++) { if (_validatorInfo[_validators[i]].currentKeyId != NO_KEY) count++; } } /// @notice True iff EVERY enrolled validator has registered a current attestation key. /// A certificate is only meaningful once the set is complete (a consumer that /// gates on PQ finality SHOULD check this alongside the root). function isSetComplete() external view returns (bool) { uint256 n = _validators.length; if (n == 0) return false; return activeKeyCount() == n; } /// @notice The current attestation key of `validator`. Reverts NotEnrolled if the /// address is not a validator; returns keyId == NO_KEY sentinel fields if the /// validator has not registered a key yet. function currentKey(address validator) external view returns (uint256 keyId, uint8 scheme, uint32 epoch, bytes memory pubKey) { Validator storage v = _validatorInfo[validator]; if (!v.enrolled) revert NotEnrolled(validator); keyId = v.currentKeyId; if (keyId == NO_KEY) return (NO_KEY, 0, 0, ""); KeyRecord storage k = _keys[keyId]; return (keyId, k.scheme, k.epoch, k.pubKey); } /// @notice The current key-epoch of `validator` (0 = no key registered yet). function keyEpochOf(address validator) external view returns (uint32) { return _validatorInfo[validator].keyEpoch; } /// @notice Total number of key records ever registered (keyIds run 0..keyCount-1). function keyCount() external view returns (uint256) { return _keys.length; } /// @notice Read an append-only key record by id. Reverts UnknownKey on a bad id. function getKey(uint256 keyId) external view returns ( address validator, uint8 scheme, uint32 epoch, uint64 registeredBlock, uint64 supersededBlock, bool superseded, bytes memory pubKey ) { if (keyId >= _keys.length) revert UnknownKey(keyId); KeyRecord storage k = _keys[keyId]; return (k.validator, k.scheme, k.epoch, k.registeredBlock, k.supersededBlock, k.superseded, k.pubKey); } /// @notice All keyIds ever registered by `validator`, oldest first (append-only history). function keyHistoryOf(address validator) external view returns (uint256[] memory) { return _validatorKeyHistory[validator]; } }