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

377 lines
17 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AereRecoveryRegistry, on-chain attested consensus-recovery evidence for Aere Network (chain 2800)
*
* @notice An append-only, fail-closed registry that records CONSENSUS RECOVERY events on
* Aere Network and binds each one to a real Falcon-512 signature verified in full,
* on-chain, by the LIVE native precompile at 0x0AE1. A recovery event is the triple
* (halt height, resume height, fault kind) that a fault caused: a validator-set drop
* below quorum, the resulting halt, and the subsequent in-place resume. An
* independent watchtower (an off-chain observer that holds a registered Falcon-512
* key) detects the halt, waits for quorum restoration, then submits the signed record
* here. No funds are ever held by this contract.
*
* WHY THIS EXISTS. Aere's fault-tolerance is characterized off-chain (see
* docs/FAULT-TOLERANCE-CHARACTERIZATION-2026-07-14.md: 0 forks over 100 adversarial
* cycles, 100/100 self-resync at the f=2 boundary). This contract turns that evidence
* into a tamper-evident, publicly auditable on-chain trail: each recorded recovery is
* authenticated by a post-quantum signature, cannot be silently rewritten, and cannot
* be forged by any operator or admin. It is the on-chain half of the fault-injection
* evidence suite that de-risks the (separately founder-gated and audit-gated)
* post-quantum consensus flip.
*
* HONEST SCOPE. This contract records ATTESTATIONS about recovery events; it does not
* itself observe consensus, halt the chain, or restore quorum, and it does not
* activate post-quantum consensus. Blocks are still produced and signed by Besu QBFT
* validators with ECDSA (secp256k1). The novelty is that the authenticity of a
* recovery record rests on a post-quantum signature (Falcon-512, NIST FIPS 206 draft
* line) verified by the live precompile activated on mainnet 2800 at block 9,189,161.
* The consensus flip stays founder-gated and audit-gated; this only makes its
* recovery evidence honest, reproducible, and quantum-durable.
*
* @dev FALCON-512 PRECOMPILE (0x0AE1) INPUT ENCODING (exactly what the live precompile parses):
* input = pk || sm
* pk = (0x00+logn) || packed_h (897 bytes, logn=9, header byte 0x09)
* sm = sigLen(2, big-endian) || nonce(40) || message(32) || esig
* esig = 0x29 || compressedSig, sigLen == esig.length
* Output: a 32-byte word, 0x..01 valid else 0x..00. An empty / zero / short return is
* treated as INVALID (this is the pre-fork behaviour of an address with no precompile,
* so a stale caller on an un-activated chain can never record). This is FAIL-CLOSED.
*
* @dev SIGNATURE ENVELOPE handed to submitRecovery():
* signature = nonce(40) || esig (esig starts 0x29, so length is 40 + 1 + sigBytes)
* The contract supplies the 32-byte message (the record hash) itself, so the signer can
* never sign a different record than the one this contract binds and records.
*
* @dev POST-QUANTUM AUTHORSHIP. Authorship of a recovery record is the Falcon-512 signature
* alone. submitRecovery() may be relayed by any msg.sender: only the holder of the
* registered operator private key can produce a valid signature over this contract's
* per-operator, per-nonce record hash, so a relayer only pays gas and can never forge
* or replay. Binding authorship to the ECDSA tx sender would defeat the purpose, since
* secp256k1 is exactly what a quantum computer breaks.
*
* @dev APPEND-ONLY. Records live in a push-only array; recordId is the array index and is
* immutable once written. There is NO function that overwrites, deletes, or edits a
* record, and there is NO owner, admin, or privileged role anywhere in this contract.
* A strictly increasing per-operator nonce is consumed on every write, so no
* (record, signature) pair can be replayed to rewrite history.
*/
contract AereRecoveryRegistry {
// ----- Falcon-512 (the only scheme this registry attests under) -----
/// @notice Live Falcon-512 verifier precompile on chain 2800.
address public constant PRECOMPILE_FALCON512 = address(0x0AE1);
uint256 internal constant FALCON512_PK_LEN = 897;
uint8 internal constant FALCON512_PK_HEADER = 0x09; // 0x00 | logn=9
uint8 internal constant FALCON512_ESIG_HEADER = 0x29; // 0x20 | logn=9
uint256 internal constant FALCON_NONCE_LEN = 40;
// ----- fault kinds -----
uint8 public constant FAULT_CRASH = 1; // one or more validators crash / go offline
uint8 public constant FAULT_NETSPLIT = 2; // network partition, minority side halts
uint8 public constant FAULT_BYZANTINE_PROPOSER = 3; // a proposer equivocates / signs invalidly
// ----- domain separator for the record hash the operator signs -----
bytes32 public constant RECORD_DOMAIN = keccak256("AereRecoveryRegistry.v1.record");
// ----- registered operator keys -----
struct Operator {
address registrant; // address that registered the key (informational; not an authority)
uint64 nonce; // strictly increasing per-operator replay counter
bytes pubKey; // the raw Falcon-512 public key (897 bytes)
}
Operator[] private _operators; // operatorId is the index into this array
// ----- recorded recovery events (append-only) -----
struct RecoveryRecord {
uint64 haltHeight; // block height at which progress stopped
uint64 resumeHeight; // block height at which progress resumed (>= haltHeight)
uint8 faultKind; // 1=CRASH, 2=NETSPLIT, 3=BYZANTINE_PROPOSER
uint16 nValidators; // validator-set size at the event (e.g. 7)
uint16 quorum; // commit quorum at the event (e.g. 5)
uint16 aliveDuringFault; // validators still alive during the fault window
uint256 operatorId; // the attesting operator key
uint64 nonce; // the per-operator nonce this record consumed
uint64 recordedAt; // block.number at which the record was written
bytes32 recordHash; // the exact 32-byte message the Falcon-512 signature verified over
}
RecoveryRecord[] private _records; // recordId is the index; push-only, never rewritten
// ----- events -----
event OperatorRegistered(uint256 indexed operatorId, address indexed registrant, uint256 pubKeyLen);
event RecoveryAttested(
uint256 indexed recordId,
uint256 indexed operatorId,
uint8 indexed faultKind,
uint64 haltHeight,
uint64 resumeHeight,
uint16 nValidators,
uint16 quorum,
uint16 aliveDuringFault,
uint64 nonce,
bytes32 recordHash
);
// ----- errors -----
error InvalidPubKey();
error UnknownOperator(uint256 operatorId);
error InvalidFaultKind(uint8 faultKind);
error InvalidHeights(uint64 haltHeight, uint64 resumeHeight);
error InvalidValidatorSet(uint16 nValidators, uint16 quorum, uint16 aliveDuringFault);
error InvalidSignatureLength();
error PQCVerificationFailed();
// ==========================================================================
// Operator registration
// ==========================================================================
/**
* @notice Register a Falcon-512 public key as a recovery-attesting operator (e.g. a
* watchtower). Permissionless: anyone may register a key. Authorship of every
* record submitted under this operatorId is the Falcon-512 signature alone, so a
* registrant is only a label, never an authority, and registering a key grants no
* power to forge a record. Multiple keys per address are allowed.
* @param pubKey the raw Falcon-512 public key (897 bytes, header byte 0x09).
* @return operatorId the identifier assigned to this key.
*/
function registerOperator(bytes calldata pubKey) external returns (uint256 operatorId) {
if (pubKey.length != FALCON512_PK_LEN || uint8(pubKey[0]) != FALCON512_PK_HEADER) revert InvalidPubKey();
operatorId = _operators.length;
_operators.push(Operator({registrant: msg.sender, nonce: 0, pubKey: pubKey}));
emit OperatorRegistered(operatorId, msg.sender, pubKey.length);
}
// ==========================================================================
// Recovery attestation
// ==========================================================================
/**
* @notice Record a consensus-recovery event, authenticated on-chain by the live Falcon-512
* precompile (0x0AE1). Verifies that `signature` is a valid Falcon-512 signature by
* the registered operator key over this contract's record hash:
*
* recordHash = keccak256(abi.encode(
* RECORD_DOMAIN, block.chainid, address(this), operatorId, nonce,
* haltHeight, resumeHeight, faultKind, nValidators, quorum, aliveDuringFault))
*
* where `nonce` is the operator's current per-operator nonce. On success the record
* is appended, the nonce is incremented, and the event is emitted. Reverts (records
* nothing, advances no nonce) on: an unknown operator, an invalid fault kind,
* inconsistent heights, an inconsistent validator set, or an invalid signature
* (FAIL-CLOSED). No admin path can bypass the signature check.
*
* @param operatorId the registered operator attesting this record.
* @param haltHeight block height at which progress stopped (>= 1).
* @param resumeHeight block height at which progress resumed (>= haltHeight, <= current block).
* @param faultKind 1=CRASH, 2=NETSPLIT, 3=BYZANTINE_PROPOSER.
* @param nValidators validator-set size at the event (e.g. 7).
* @param quorum commit quorum at the event (e.g. 5), 1 <= quorum <= nValidators.
* @param aliveDuringFault validators still alive during the fault window (<= nValidators).
* @param signature the Falcon-512 signature envelope, nonce(40) || esig.
* @return recordId the append-only id assigned to this record.
*/
function submitRecovery(
uint256 operatorId,
uint64 haltHeight,
uint64 resumeHeight,
uint8 faultKind,
uint16 nValidators,
uint16 quorum,
uint16 aliveDuringFault,
bytes calldata signature
) external returns (uint256 recordId) {
if (operatorId >= _operators.length) revert UnknownOperator(operatorId);
if (faultKind != FAULT_CRASH && faultKind != FAULT_NETSPLIT && faultKind != FAULT_BYZANTINE_PROPOSER) {
revert InvalidFaultKind(faultKind);
}
// Heights must describe a real, already-happened halt-then-resume window.
if (haltHeight == 0 || resumeHeight < haltHeight || resumeHeight > uint64(block.number)) {
revert InvalidHeights(haltHeight, resumeHeight);
}
// Validator-set context must be internally consistent.
if (
nValidators == 0 || quorum == 0 || quorum > nValidators || aliveDuringFault > nValidators
) {
revert InvalidValidatorSet(nValidators, quorum, aliveDuringFault);
}
Operator storage op = _operators[operatorId];
uint64 nonce = op.nonce;
bytes32 recordHash = recordChallenge(
operatorId, nonce, haltHeight, resumeHeight, faultKind, nValidators, quorum, aliveDuringFault
);
if (!_verifyFalcon(op.pubKey, recordHash, signature)) revert PQCVerificationFailed();
// Effects: consume the nonce and append the record (append-only, never rewritten).
op.nonce = nonce + 1;
recordId = _records.length;
_records.push(
RecoveryRecord({
haltHeight: haltHeight,
resumeHeight: resumeHeight,
faultKind: faultKind,
nValidators: nValidators,
quorum: quorum,
aliveDuringFault: aliveDuringFault,
operatorId: operatorId,
nonce: nonce,
recordedAt: uint64(block.number),
recordHash: recordHash
})
);
emit RecoveryAttested(
recordId,
operatorId,
faultKind,
haltHeight,
resumeHeight,
nValidators,
quorum,
aliveDuringFault,
nonce,
recordHash
);
}
/**
* @notice The exact 32-byte record hash that a signature for `operatorId` must sign at a
* given nonce. Off-chain watchtowers query this to know what to sign.
*/
function recordChallenge(
uint256 operatorId,
uint64 nonce,
uint64 haltHeight,
uint64 resumeHeight,
uint8 faultKind,
uint16 nValidators,
uint16 quorum,
uint16 aliveDuringFault
) public view returns (bytes32) {
return keccak256(
abi.encode(
RECORD_DOMAIN,
block.chainid,
address(this),
operatorId,
nonce,
haltHeight,
resumeHeight,
faultKind,
nValidators,
quorum,
aliveDuringFault
)
);
}
// ==========================================================================
// Internal verifier
// ==========================================================================
/**
* @dev Build the exact Falcon-512 precompile input with `message` as the signed 32-byte
* message, staticcall the live precompile at 0x0AE1, and return true iff it answers
* 0x..01. Any empty / short / zero return is invalid (FAIL-CLOSED).
*/
function _verifyFalcon(bytes memory pubKey, bytes32 message, bytes memory signature)
internal
view
returns (bool)
{
// signature = nonce(40) || esig
if (signature.length <= FALCON_NONCE_LEN) revert InvalidSignatureLength();
uint256 sigLen = signature.length - FALCON_NONCE_LEN; // esig length
if (sigLen < 2 || sigLen > type(uint16).max) revert InvalidSignatureLength();
bytes memory nonce = _slice(signature, 0, FALCON_NONCE_LEN);
bytes memory esig = _slice(signature, FALCON_NONCE_LEN, sigLen);
// sm = sigLen(2, big-endian) || nonce(40) || message(32) || esig
bytes memory sm = abi.encodePacked(uint16(sigLen), nonce, message, esig);
bytes memory input = abi.encodePacked(pubKey, sm);
(bool ok, bytes memory ret) = PRECOMPILE_FALCON512.staticcall(input);
return ok && ret.length >= 32 && ret[31] == 0x01;
}
function _slice(bytes memory data, uint256 start, uint256 len) internal pure returns (bytes memory out) {
out = new bytes(len);
for (uint256 i = 0; i < len; i++) {
out[i] = data[start + i];
}
}
// ==========================================================================
// Views
// ==========================================================================
/// @notice Number of registered operators (operatorIds run 0..operatorCount-1).
function operatorCount() external view returns (uint256) {
return _operators.length;
}
/// @notice Read a registered operator.
function getOperator(uint256 operatorId)
external
view
returns (address registrant, uint64 nonce, bytes memory pubKey)
{
if (operatorId >= _operators.length) revert UnknownOperator(operatorId);
Operator storage op = _operators[operatorId];
return (op.registrant, op.nonce, op.pubKey);
}
/// @notice The current (next unused) nonce for an operator.
function nonceOf(uint256 operatorId) external view returns (uint64) {
if (operatorId >= _operators.length) revert UnknownOperator(operatorId);
return _operators[operatorId].nonce;
}
/// @notice Total number of recovery records (recordIds run 0..recordCount-1).
function recordCount() external view returns (uint256) {
return _records.length;
}
/// @notice Read a recorded recovery event by its append-only id.
function getRecord(uint256 recordId)
external
view
returns (
uint64 haltHeight,
uint64 resumeHeight,
uint8 faultKind,
uint16 nValidators,
uint16 quorum,
uint16 aliveDuringFault,
uint256 operatorId,
uint64 nonce,
uint64 recordedAt,
bytes32 recordHash
)
{
if (recordId >= _records.length) revert UnknownOperator(recordId);
RecoveryRecord storage r = _records[recordId];
return (
r.haltHeight,
r.resumeHeight,
r.faultKind,
r.nValidators,
r.quorum,
r.aliveDuringFault,
r.operatorId,
r.nonce,
r.recordedAt,
r.recordHash
);
}
}