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

230 lines
12 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
/// NOTHING and REVERTS on an invalid proof. On chain 2800 the deployed
/// SP1VerifierGateway is 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 and the
/// SP1 Groth16 route is selected by the proof's leading 4-byte selector,
/// exactly as AereZkEthLightClient / AereRollupValidity / AereEVMValidity use it.
interface ISp1Verifier {
function verifyProof(
bytes32 programVKey,
bytes calldata publicValues,
bytes calldata proof
) external view;
}
/**
* @title AereZkQbftLightClient — succinct (zk) light client of AERE's OWN QBFT finality.
*
* @notice The OUTBOUND direction of AERE's zk light-client stack: it lets ANY chain (or
* any verifier) advance a trust-minimized view of AERE (chain 2800) finality by
* checking an SP1 zkVM **Groth16 proof** that ONE AERE block header is FINAL
* under Hyperledger Besu QBFT. The proof (guest `qbft-lightclient-guest`, sharing
* `qbft_finality::verify_finality` verbatim with the real-header test) checks,
* INSIDE the zkVM:
* 1. decode the header's QBFT `extraData` exactly as Besu `QbftExtraDataCodec`;
* 2. the header's stored validator set equals the known (trusted) set;
* 3. recompute the on-chain block hash (`EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER`)
* and the committed-seal digest (`EXCLUDE_COMMIT_SEALS`);
* 4. ecrecover every 65-byte committed seal over the digest and keep those that
* map to a DISTINCT known validator;
* 5. assert the distinct count reaches the QBFT quorum `ceil(2N/3)`.
*
* This contract does the CHEAP part on chain: it verifies the ~300k-gas Groth16
* proof through the live SP1 gateway, binds the proof's public values to its own
* trusted validator-set anchor, enforces a STRICTLY ADVANCING block number, and on
* success records the newly-final AERE block (number + on-chain hash). Permissionless.
*
* @dev WHY THIS EXISTS. The sibling `AereZkEthLightClient` is the INBOUND direction
* (AERE tracking Ethereum's sync-committee finality). This is its mirror: it makes
* AERE's QBFT finality independently, succinctly checkable by an external verifier,
* so a bridge OUT of AERE rests on a proof of the committed-seal quorum instead of a
* multisig attestation. The hard verification (RLP layout, dual Besu hash
* pre-images, secp256k1 committed-seal recovery, distinct-quorum counting) is proven
* off-chain in the zkVM and against REAL chain-2800 headers before proving; the
* on-chain step is one Groth16 verify plus the lineage/monotonicity binding below.
*
* @dev SECURITY MODEL — read before quoting.
* * On chain here we verify the Groth16 proof + bind its public values (validator-set
* anchor, strict block-number advance, finalized flag). Off chain (in the zkVM) we
* prove the committed-seal quorum. Security reduces to the honesty of `>= 2f+1` of
* the trusted QBFT validator set — exactly what QBFT finality means, no more.
* * TRUST-MINIMIZED, NOT TRUSTLESS, NOT QUANTUM-SAFE. AERE mainnet QBFT commits on
* classical secp256k1 ECDSA, and the proof wrap is an SP1 Groth16 SNARK over BN254
* (classical). Both are quantum-vulnerable. `falconVerified` is committed `false` by
* the guest today and is recorded, never required — it becomes meaningful only once
* mainnet consensus VERIFIABLY blocks on a Falcon-512 quorum certificate.
* * With AERE's live N=7 validator set the quorum is 5 (2f+1, f=2). All 7 are
* Foundation-operated, so the honest-quorum premise is an OPERATOR assumption, not
* an economic one; the light client is only as decentralized as that set.
* * VALIDATOR-SET ANCHOR IS IMMUTABLE. `trustedValidatorSetRoot` is pinned at deploy
* (= `keccak256(addr_0 ‖ addr_1 ‖ …)` over the set in stored order, matching the
* guest's `validator_set_root`). The guest proves finality against a FIXED known
* set, so this contract binds to that set and does NOT track validator-set changes.
* A validator rotation therefore requires a fresh anchor (new deployment /
* checkpoint). Stated honestly; rotation is a documented follow-up, not a silent gap.
* THIS IS NOT THEORETICAL: the first deployment (0xc9A2DCae…B1c2) anchored the then-N=3
* set, the live set grew to N=7 at block 9,312,565, and that client was thereby frozen
* out of verifying current finality — it was replaced by 0xCaDA54FA…6488 on 2026-07-16
* (see zk-light-client/QBFT-LIGHTCLIENT-7SET-2026-07-16.md). Treat the anchor as an
* expiring credential: a consumer MUST check that trustedValidatorSetRoot still equals
* keccak256 over the live qbft_getValidatorsByBlockNumber set before trusting this
* client as a finality oracle.
* * WEAKLY-SUBJECTIVE BOOTSTRAP: the constructor pins the validator-set anchor and an
* initial finalized block number; from there the client self-advances by proof.
* * PROGRAM BINDING: `PROGRAM_VKEY` is the SP1 vkey of the exact guest ELF. Binding
* the vkey binds the exact verification program.
*
* PUBLIC-VALUES CONVENTION (exactly 192 bytes = 6 x 32, matching the guest's
* `PublicValues` `abi.encode`):
* abi.encode(
* bytes32 blockHash, // recomputed on-chain block hash
* uint64 blockNumber, // the block's height
* bytes32 committedSealHash, // digest the seals were recovered over
* bytes32 validatorSetRoot, // commitment to the validator set used as anchor
* bool finalized, // guest asserts true; re-checked here
* bool falconVerified // reserved PQC slot; false on mainnet today
* )
*
* IMMUTABILITY / TRUST: SP1_VERIFIER, PROGRAM_VKEY and trustedValidatorSetRoot are
* fixed at deploy. There is NO owner and NO admin: advancing is permissionless.
* Soundness rests on the SP1 Groth16 proof plus the on-chain anchor/monotonicity checks.
*/
contract AereZkQbftLightClient {
/// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector).
address public immutable SP1_VERIFIER;
/// @notice SP1 vkey binding the exact qbft-lightclient guest ELF.
bytes32 public immutable PROGRAM_VKEY;
/// @notice Commitment to the trusted AERE validator set: keccak256 over the
/// addresses in stored order. Every accepted proof must be about THIS set.
bytes32 public immutable trustedValidatorSetRoot;
/// @notice The highest QBFT-final AERE block number advanced so far.
uint64 public finalizedNumber;
/// @notice The on-chain block hash of the block at `finalizedNumber`.
bytes32 public finalizedBlockHash;
/// @notice The committed-seal digest recorded for the latest advance.
bytes32 public lastCommittedSealHash;
/// @notice Whether the latest accepted proof carried a verified Falcon certificate.
/// False on mainnet today (classical ECDSA); recorded for transparency.
bool public lastFalconVerified;
/// @notice Number of proofs accepted (finality advances).
uint64 public updateCount;
event Bootstrapped(bytes32 validatorSetRoot, uint64 finalizedNumber, bytes32 finalizedBlockHash);
event FinalityAdvanced(
uint64 indexed blockNumber,
bytes32 blockHash,
bytes32 committedSealHash,
bool falconVerified,
address prover
);
error InvalidProof();
error BadPublicValuesLength(uint256 got);
error ValidatorSetMismatch(bytes32 inProof, bytes32 trusted);
error NotFinalized();
error StaleFinality(uint64 inProof, uint64 current);
error ZeroBlockHash();
error ZeroAddress();
error ZeroValue();
error VerifierHasNoCode();
constructor(
address sp1Verifier,
bytes32 programVKey,
bytes32 validatorSetRoot,
uint64 bootstrapFinalizedNumber,
bytes32 bootstrapFinalizedBlockHash
) {
if (sp1Verifier == address(0)) revert ZeroAddress();
// A code-less verifier would make verifyProof (which returns no data) succeed silently,
// accepting every "proof". Attacker cannot reach this (no control of constructor args), but
// it removes a deploy-time footgun; the deployed value must be the live SP1 gateway.
if (sp1Verifier.code.length == 0) revert VerifierHasNoCode();
if (programVKey == bytes32(0) || validatorSetRoot == bytes32(0)) revert ZeroValue();
SP1_VERIFIER = sp1Verifier;
PROGRAM_VKEY = programVKey;
trustedValidatorSetRoot = validatorSetRoot;
finalizedNumber = bootstrapFinalizedNumber;
finalizedBlockHash = bootstrapFinalizedBlockHash;
emit Bootstrapped(validatorSetRoot, bootstrapFinalizedNumber, bootstrapFinalizedBlockHash);
}
/// @notice Verify a zk proof that one AERE block is QBFT-final and, on success,
/// advance the tracked finalized head. Permissionless.
/// @param publicValues abi.encode(blockHash, blockNumber, committedSealHash,
/// validatorSetRoot, finalized, falconVerified), 192 bytes.
/// @param proof SP1 Groth16 proof bytes.
function processProof(bytes calldata publicValues, bytes calldata proof) external {
if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length);
// 1) zk proof: reverts if the proof is not valid for PROGRAM_VKEY.
try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
// verified — did not revert
} catch {
revert InvalidProof();
}
(
bytes32 blockHash,
uint64 blockNumber,
bytes32 committedSealHash,
bytes32 validatorSetRoot,
bool finalized,
bool falconVerified
) = abi.decode(publicValues, (bytes32, uint64, bytes32, bytes32, bool, bool));
// 2) bind the proof to THIS client's trusted anchor + monotonic head.
if (validatorSetRoot != trustedValidatorSetRoot) {
revert ValidatorSetMismatch(validatorSetRoot, trustedValidatorSetRoot);
}
if (!finalized) revert NotFinalized();
if (blockNumber <= finalizedNumber) revert StaleFinality(blockNumber, finalizedNumber);
if (blockHash == bytes32(0)) revert ZeroBlockHash();
// 3) advance the tracked finalized head.
finalizedNumber = blockNumber;
finalizedBlockHash = blockHash;
lastCommittedSealHash = committedSealHash;
lastFalconVerified = falconVerified;
unchecked {
updateCount += 1;
}
emit FinalityAdvanced(blockNumber, blockHash, committedSealHash, falconVerified, msg.sender);
}
/// @notice Stateless verification: reverts if the proof is invalid for PROGRAM_VKEY
/// or the public values fail the anchor/finalized binding; otherwise returns
/// the decoded fields. Records nothing (no monotonicity constraint).
function verify(bytes calldata publicValues, bytes calldata proof)
external
view
returns (
bytes32 blockHash,
uint64 blockNumber,
bytes32 committedSealHash,
bytes32 validatorSetRoot,
bool finalized,
bool falconVerified
)
{
if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length);
ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
(blockHash, blockNumber, committedSealHash, validatorSetRoot, finalized, falconVerified) =
abi.decode(publicValues, (bytes32, uint64, bytes32, bytes32, bool, bool));
if (validatorSetRoot != trustedValidatorSetRoot) {
revert ValidatorSetMismatch(validatorSetRoot, trustedValidatorSetRoot);
}
if (!finalized) revert NotFinalized();
}
}