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.
188 lines
9.2 KiB
Solidity
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);
|
|
}
|
|
}
|