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.
345 lines
17 KiB
Solidity
345 lines
17 KiB
Solidity
// 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];
|
|
}
|
|
}
|