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.
230 lines
12 KiB
Solidity
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();
|
|
}
|
|
}
|