// 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 AereRollupValidity / AereEVMValidity use it. interface ISp1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view; } /** * @title AereZkEthLightClient — succinct (zk) Ethereum beacon light client on AERE. * * @notice Advances a trust-minimized view of Ethereum finality on AERE (chain 2800) * by verifying an SP1 zkVM **Groth16 proof** that ONE Ethereum beacon * `LightClientUpdate` is valid against the currently-trusted sync committee. * The proof (guest `eth-lightclient-guest`) checks, INSIDE the zkVM: * 1. the supplied 512 committee pubkeys hash (SSZ) to the trusted root; * 2. participation >= 2/3 of 512, and aggregates the participants (G1); * 3. the aggregate BLS12-381 signature over the SSZ signing root of the * attested header verifies (hash-to-G2 + pairing); * 4. the finalized header is committed in the attested state (Electra * finality Merkle branch, gindex 169); * 5. the delivered next sync committee is committed in the attested state * (next-committee branch, gindex 87); * 6. the attested slot is not behind the finalized slot. * * This contract then 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 state (committee lineage + strict slot advance + genesis * validators root), and on success advances the finalized header + rotates the * trusted committee to the delivered next committee. * * @dev WHY THIS EXISTS. The sibling `AereEthLightClient` verifies the SAME update * DIRECTLY on chain against AERE's EIP-2537 BLS12-381 precompiles. That path * is correct but its 512-pubkey committee-root recomputation + BLS aggregation * costs ~13.2M+ gas and the full update OOGs at AERE's EIP-7825 2^24 * (16,777,216) per-tx gas cap. Moving the heavy verification into a succinct * proof makes the on-chain step a single Groth16 verify — well under the cap. * The off-chain verification math is byte-identical to `AereEthLightClient` * (SYNC_COMMITTEE_SIZE=512, MIN_PARTICIPANTS=342, DOMAIN 0x07000000, Electra * gindices) and was proven against REAL live mainnet data before proving. * * @dev SECURITY MODEL — read before quoting. * * What is proven ON CHAIN here is the Groth16 proof + the public-value * binding (committee lineage, slot advance, gvr). What is proven OFF CHAIN * (inside the zkVM) is the sync-committee signature + SSZ branches. * * Security reduces to the honesty of >= 2/3 of Ethereum's 512-member sync * committee, exactly like every Altair light client. NOT full Ethereum * consensus, NOT a fraud/zk execution bridge. * * TRUST-MINIMIZED, NOT TRUSTLESS and NOT QUANTUM-SAFE: the inbound signature * scheme is BLS12-381 (classical, Ethereum's inherited crypto) and the proof * wrap is an SP1 Groth16 SNARK over BN254 (classical). Both are * quantum-vulnerable. This says nothing about AERE's own PQC work. * * Bootstrap is weakly subjective: the constructor pins a trusted committee * root + finalized slot/header (a checkpoint). From there it self-advances, * one committee per accepted proof. * * PROGRAM BINDING: PROGRAM_VKEY is the SP1 vkey of the exact guest ELF that * runs the update verification. Binding the vkey binds the exact program. * * PUBLIC-VALUES CONVENTION (exactly 192 bytes = 6 x 32): * abi.encode( * bytes32 gvr, // genesis validators root used for domain * bytes32 prevCommitteeRoot, // trusted signing-committee SSZ root * uint64 finalizedSlot, // new finalized slot * bytes32 finalizedHeaderRoot, // new finalized beacon header root * bytes32 nextCommitteeRoot, // delivered next sync-committee root * uint64 participation // participants (guest enforces >= 342) * ) * * IMMUTABILITY / TRUST: SP1_VERIFIER, PROGRAM_VKEY and genesisValidatorsRoot * are fixed at deploy. There is NO owner and NO admin: advancing is * permissionless. Soundness rests entirely on the SP1 Groth16 proof plus the * on-chain lineage/monotonicity checks. */ contract AereZkEthLightClient { /// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector). address public immutable SP1_VERIFIER; /// @notice SP1 vkey binding the exact eth-lightclient guest ELF. bytes32 public immutable PROGRAM_VKEY; /// @notice Ethereum genesis validators root (mainnet), bound in every proof's /// domain. A proof for a different network's gvr is rejected. bytes32 public immutable genesisValidatorsRoot; /// @notice The sync-committee root a valid next proof MUST have signed with. bytes32 public trustedCommitteeRoot; /// @notice The highest finalized beacon slot advanced so far. uint64 public finalizedSlot; /// @notice The finalized beacon block header root at `finalizedSlot`. bytes32 public finalizedHeaderRoot; /// @notice Number of proofs accepted (finality advances). uint64 public updateCount; event Bootstrapped(bytes32 committeeRoot, uint64 finalizedSlot, bytes32 finalizedHeaderRoot, bytes32 gvr); event FinalityAdvanced( uint64 indexed finalizedSlot, bytes32 finalizedHeaderRoot, bytes32 rotatedFromCommittee, bytes32 rotatedToCommittee, uint64 participation, address prover ); error InvalidProof(); error BadPublicValuesLength(uint256 got); error WrongGenesisValidatorsRoot(bytes32 inProof, bytes32 expected); error CommitteeMismatch(bytes32 inProof, bytes32 trusted); error StaleFinality(uint64 inProof, uint64 current); error ZeroNextCommittee(); error ZeroAddress(); error ZeroValue(); constructor( address sp1Verifier, bytes32 programVKey, bytes32 gvr, bytes32 bootstrapCommitteeRoot, uint64 bootstrapFinalizedSlot, bytes32 bootstrapFinalizedHeaderRoot ) { if (sp1Verifier == address(0)) revert ZeroAddress(); if (programVKey == bytes32(0) || gvr == bytes32(0) || bootstrapCommitteeRoot == bytes32(0)) { revert ZeroValue(); } SP1_VERIFIER = sp1Verifier; PROGRAM_VKEY = programVKey; genesisValidatorsRoot = gvr; trustedCommitteeRoot = bootstrapCommitteeRoot; finalizedSlot = bootstrapFinalizedSlot; finalizedHeaderRoot = bootstrapFinalizedHeaderRoot; emit Bootstrapped(bootstrapCommitteeRoot, bootstrapFinalizedSlot, bootstrapFinalizedHeaderRoot, gvr); } /// @notice Verify a zk proof of one Ethereum LightClientUpdate and, on success, /// advance finality + rotate the trusted committee. Permissionless. /// @param publicValues abi.encode(gvr, prevCommitteeRoot, finalizedSlot, /// finalizedHeaderRoot, nextCommitteeRoot, participation), 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 gvr, bytes32 prevCommitteeRoot, uint64 newFinalizedSlot, bytes32 newFinalizedHeaderRoot, bytes32 nextCommitteeRoot, uint64 participation ) = abi.decode(publicValues, (bytes32, bytes32, uint64, bytes32, bytes32, uint64)); // 2) bind the proof to THIS client's trusted state. if (gvr != genesisValidatorsRoot) revert WrongGenesisValidatorsRoot(gvr, genesisValidatorsRoot); if (prevCommitteeRoot != trustedCommitteeRoot) { revert CommitteeMismatch(prevCommitteeRoot, trustedCommitteeRoot); } if (newFinalizedSlot <= finalizedSlot) revert StaleFinality(newFinalizedSlot, finalizedSlot); if (nextCommitteeRoot == bytes32(0)) revert ZeroNextCommittee(); // 3) advance finality + rotate the trusted committee to the delivered next. bytes32 from = trustedCommitteeRoot; finalizedSlot = newFinalizedSlot; finalizedHeaderRoot = newFinalizedHeaderRoot; trustedCommitteeRoot = nextCommitteeRoot; unchecked { updateCount += 1; } emit FinalityAdvanced( newFinalizedSlot, newFinalizedHeaderRoot, from, nextCommitteeRoot, participation, msg.sender ); } /// @notice Stateless verification: reverts if the proof is invalid for /// PROGRAM_VKEY, otherwise returns the decoded fields. Records nothing. function verify(bytes calldata publicValues, bytes calldata proof) external view returns ( bytes32 gvr, bytes32 prevCommitteeRoot, uint64 newFinalizedSlot, bytes32 newFinalizedHeaderRoot, bytes32 nextCommitteeRoot, uint64 participation ) { if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof); (gvr, prevCommitteeRoot, newFinalizedSlot, newFinalizedHeaderRoot, nextCommitteeRoot, participation) = abi.decode(publicValues, (bytes32, bytes32, uint64, bytes32, bytes32, uint64)); } }