// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /// @notice Minimal Succinct SP1 on-chain verifier interface. /// @dev The canonical `SP1VerifierGateway` / `SP1Verifier` exposes exactly /// this. `verifyProof` REVERTS if `proofBytes` is not a valid Groth16 /// proof for `(programVKey, publicValues)`; it returns nothing on success. interface ISP1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proofBytes ) external view; } /// @notice Destination-chain application handler. `AereOutboundVerifier` calls /// this once, and only once, per proven+unused message. interface IAereMessageHandler { /// @param srcChainId the AERE source chain id (2800). /// @param sender the original `msg.sender` on the AERE Outbox. /// @param payload the application bytes from the Outbox message. function handleAereMessage(uint256 srcChainId, address sender, bytes calldata payload) external; } /// @title AereOutboundVerifier /// @notice Destination-chain verifier that completes the trust-minimised OUTBOUND /// interop loop (AERE -> this chain). It accepts an SP1/Groth16 proof that /// an `AereOutboundOutbox` message log was included in a QBFT-final AERE /// block, and — if the proof verifies against the pinned AERE finality /// vkey — delivers the message to its target handler exactly once. /// /// @dev PUBLIC-VALUES ENCODING (must match the guest's committed struct): /// abi.encode( /// uint64 chainId, // must be AERE_CHAIN_ID (2800) /// uint64 blockNumber, // source block height (informational) /// bytes32 blockHash, // the QBFT-final block that carries the log /// bytes32 root, // receiptsRoot (informational; bound via proof) /// uint8 claimKind, // must be 1 (receipt/log claim) /// bytes32 commitment, // canonical message commitment (see below) /// bool finalized, // must be true /// bool falconVerified // must be false (mainnet consensus is ECDSA) /// ) /// All eight fields are static 32-byte words, so this is a plain 256-byte /// concatenation — identical to `alloy` `SolType::abi_encode` of the /// guest's `PublicValues` struct. /// /// COMMITMENT RE-DERIVATION (must match interop_core log_commitment(0)): /// commitment = keccak256( /// bytes32(AERE_CHAIN_ID) ‖ blockHash ‖ bytes32(outbox) ‖ /// EVENT_SIG ‖ messageId ‖ bytes32(destChainId) ‖ keccak256(data)) /// where data = abi.encode(nonce, sender, targetHandler, payload) and /// messageId = keccak256(abi.encode(AERE_CHAIN_ID, destChainId, nonce, /// sender, targetHandler, payload)). /// /// SECURITY MODEL — trust-minimised, NOT trustless. Read before quoting. /// * A valid proof attests only that a `>= quorum` of AERE's 5 QBFT validators /// sealed the source block. Delivery security == that validator set. It is a /// zk light-client bridge, not a magic-trustless one. /// * Those validator seals are classical ECDSA today and thus QUANTUM-VULNERABLE. /// A Falcon seal slot is reserved but inactive; `falconVerified` is required to /// be `false` so this contract can never launder an unbacked post-quantum claim. /// * The SP1 Groth16 wrapper is over BN254, which is ALSO quantum-vulnerable. /// * This is the honest replacement for the earlier flawed/inert multisig bridge: /// it removes the bridge signers, it does not remove trust in AERE consensus. contract AereOutboundVerifier { /// @notice AERE mainnet chain id the pinned vkey attests finality for. uint256 public constant AERE_CHAIN_ID = 2800; /// @notice topic0 preimage hash of `AereOutboundOutbox.AereCrossChainMessage`. bytes32 public constant EVENT_SIG = keccak256("AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)"); /// @notice The SP1 verifier (gateway) this contract calls. Immutable. ISP1Verifier public immutable sp1Verifier; /// @notice The pinned AERE QBFT-finality program vkey. A proof for any other /// program simply fails `sp1Verifier.verifyProof`. Immutable. bytes32 public immutable programVKey; /// @notice The trusted AERE-side Outbox address bound into every commitment. /// A log from any other contract yields a different commitment and is /// rejected. Immutable. address public immutable outbox; /// @notice Replay protection: messageId => delivered. mapping(bytes32 => bool) public delivered; event MessageDelivered( bytes32 indexed messageId, address indexed sender, address indexed targetHandler, uint64 sourceBlockNumber ); error WrongSourceChain(uint64 got); error NotReceiptClaim(uint8 got); error NotFinal(); error UnexpectedFalconFlag(); error WrongDestinationChain(uint256 got); error CommitmentMismatch(bytes32 expected, bytes32 got); error AlreadyDelivered(bytes32 messageId); /// @param _sp1Verifier the SP1 verifier / gateway on this destination chain. /// @param _programVKey the AERE QBFT-finality guest vkey to pin. /// @param _outbox the AERE-side `AereOutboundOutbox` address to trust. constructor(ISP1Verifier _sp1Verifier, bytes32 _programVKey, address _outbox) { require(address(_sp1Verifier) != address(0), "verifier: zero sp1"); require(_outbox != address(0), "verifier: zero outbox"); sp1Verifier = _sp1Verifier; programVKey = _programVKey; outbox = _outbox; } /// @notice Verify an AERE outbound-message proof and deliver it exactly once. /// @param publicValues the guest's ABI-encoded committed public values. /// @param proofBytes the SP1 Groth16 proof over `(programVKey, publicValues)`. /// @param destChainId the message's destination chain id (must be this chain). /// @param nonce the Outbox message nonce. /// @param sender the original Outbox `msg.sender`. /// @param targetHandler the destination handler to deliver to. /// @param payload the application payload. function deliver( bytes calldata publicValues, bytes calldata proofBytes, uint256 destChainId, uint256 nonce, address sender, address targetHandler, bytes calldata payload ) external { ( uint64 chainId, uint64 blockNumber, bytes32 blockHash, , // root (informational; bound into the proof, not re-checked here) uint8 claimKind, bytes32 commitment, bool finalized, bool falconVerified ) = abi.decode( publicValues, (uint64, uint64, bytes32, bytes32, uint8, bytes32, bool, bool) ); // ---- Cheap public-input gates (before the expensive proof check). ---- if (chainId != AERE_CHAIN_ID) revert WrongSourceChain(chainId); if (claimKind != 1) revert NotReceiptClaim(claimKind); if (!finalized) revert NotFinal(); // Honesty invariant: mainnet consensus is classical ECDSA. A `true` here // would be an unbacked post-quantum claim; refuse it. if (falconVerified) revert UnexpectedFalconFlag(); if (destChainId != block.chainid) revert WrongDestinationChain(destChainId); // ---- The zk check: proof valid for (pinned vkey, these publicValues). ---- // Reverts if the proof is not a genuine QBFT-finality+inclusion proof, or // if `publicValues` were tampered (the commitment would no longer match a // provable statement), or if it was produced for a different program vkey. sp1Verifier.verifyProof(programVKey, publicValues, proofBytes); // ---- Bind the proven commitment to the delivered message fields. ---- bytes32 messageId = keccak256( abi.encode(AERE_CHAIN_ID, destChainId, nonce, sender, targetHandler, payload) ); bytes32 dataHash = keccak256(abi.encode(nonce, sender, targetHandler, payload)); bytes32 expected = keccak256( abi.encodePacked( bytes32(AERE_CHAIN_ID), // cid, left-padded blockHash, // from the proof bytes32(uint256(uint160(outbox))), // trusted outbox, left-padded EVENT_SIG, // topic0 messageId, // topic1 (indexed) bytes32(destChainId), // topic2 (indexed) dataHash // keccak(log data) ) ); if (expected != commitment) revert CommitmentMismatch(expected, commitment); // ---- Replay protection (effects before interaction). ---- if (delivered[messageId]) revert AlreadyDelivered(messageId); delivered[messageId] = true; emit MessageDelivered(messageId, sender, targetHandler, blockNumber); // ---- Deliver to the application handler. ---- IAereMessageHandler(targetHandler).handleAereMessage(AERE_CHAIN_ID, sender, payload); } }