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

188 lines
9.2 KiB
Solidity

// 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);
}
}