aere-contracts/contracts/zkverify/AerePQStarkVerifier.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

128 lines
6.8 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ISP1Verifier, ISP1VerifierWithHash} from "./ISP1Verifier.sol";
/// @title AerePQStarkVerifier - ISP1Verifier-compatible adapter for the native POST-QUANTUM
/// STARK-verification precompile at 0x0AE8 (direct SP1/Plonky3 inner FRI/STARK verify, no
/// BN254 Groth16 wrap).
///
/// @notice This adapter lets every existing SP1 consumer on AERE (AereEVMValidityV2, the compliance
/// privacy pool, AereProofRegistry, AereOutboundVerifierV2, the zkML verifier, the zk light
/// clients, ...) switch from the Shor-breakable BN254 Groth16 gateway
/// (0x9ca479C8c52C0EbB4599319a36a5a017BCC70628, route 0x4388a21c) to the hash-based PQ
/// precompile by a ONE-LINE address swap: point the consumer's `SP1_VERIFIER` immutable at
/// an instance of this contract instead of at the gateway. The `verifyProof` ABI is
/// byte-identical to Succinct's ISP1Verifier (reverts on an invalid proof, returns nothing
/// on success), so no consumer code changes - only the constructor argument.
///
/// @dev WIRE. It re-frames the SP1 call `(programVKey, publicValues, proofBytes)` into the
/// precompile's self-describing v1 envelope (see
/// docs/PQ-STARK-VERIFIER-PRECOMPILE-2026-07-18.md section 3):
///
/// magic("AS1\0") || version(1) || configId(1) || reserved(2)
/// || vkeyDigest(32) || publicValuesLen(4 BE) || publicValues
/// || proofLen(4 BE) || proofBytes
///
/// `vkeyDigest` here is passed through as the 32-byte `programVKey` (the precompile treats
/// it as the Poseidon2 digest of the recursion vk / program vk; the exact digest domain is
/// pinned in the spec). `configId` selects the frozen FRI config (1 = SP1 inner/shrink,
/// 2 = SP1 wrap). The precompile returns the 32-byte word `0x..01` iff the inner STARK
/// verifies, and EMPTY otherwise; this adapter reverts on anything but that success word.
///
/// @dev ============================ HONEST SCOPE - READ THIS ============================
/// The precompile at 0x0AE8 is a REFERENCE SKELETON as of 2026-07-18 (NOT AUDITED, NOT a
/// working verifier, and NOT activated on any AERE network - mainnet chain 2800 has exactly
/// the FIVE PQC precompiles 0x0AE1..0x0AE5). Its BabyBear field layer, wire parser, and
/// verifier structure are real and testable, but the Poseidon2 permutation, FRI folding
/// arithmetic, and SP1 recursion-AIR constraints are DELEGATED and not yet ported, so the
/// precompile FAIL-CLOSES: it returns EMPTY for every input and this adapter therefore
/// REVERTS on every proof. That is the safe direction: a consumer swapped to this adapter
/// before the core is completed + externally audited + founder-activated simply DENIES all
/// proofs; it can never accept a forged one. Do NOT deploy a real consumer against this
/// adapter until 0x0AE8 is activated per
/// docs/PQ-STARK-VERIFIER-ACTIVATION-2026-07-18.md. Nothing here changes AERE consensus,
/// which remains classical secp256k1 ECDSA QBFT.
contract AerePQStarkVerifier is ISP1VerifierWithHash {
/// @notice The native PQ STARK-verify precompile.
address internal constant PQ_STARK_PRECOMPILE = address(0x0AE8);
/// @notice v1 envelope constants.
bytes4 internal constant MAGIC = 0x41533100; // "AS1\0"
uint8 internal constant WIRE_VERSION = 1;
/// @notice Which frozen FRI config the proofs use. 1 = SP1 inner/shrink, 2 = SP1 wrap.
/// Immutable so an instance is bound to exactly one proof family.
uint8 public immutable CONFIG_ID;
error WrongVerifierSelector(bytes4 received, bytes4 expected);
error PQProofInvalid();
/// @param configId 1 for the SP1 inner/shrink STARK, 2 for the SP1 wrap STARK.
constructor(uint8 configId) {
require(configId == 1 || configId == 2, "AerePQStarkVerifier: bad configId");
CONFIG_ID = configId;
}
/// @inheritdoc ISP1VerifierWithHash
/// @dev A domain-separated tag so a consumer can tell a PQ verifier apart from the BN254 one.
/// This is NOT the SP1 Groth16 VERIFIER_HASH; PQ proofs carry no BN254 selector.
function VERIFIER_HASH() external pure returns (bytes32) {
return keccak256("AerePQStarkVerifier.v1");
}
/// @notice The circuit/proof family this instance verifies.
function VERSION() external pure returns (string memory) {
return "pq-stark-v1(reference-skeleton)";
}
/// @notice Verify an SP1 inner FRI/STARK proof directly against the PQ precompile.
/// @dev Reverts (PQProofInvalid) unless the precompile returns the 32-byte success word.
/// Signature and semantics match Succinct's ISP1Verifier exactly, so this is a drop-in.
/// @param programVKey 32-byte program/recursion vkey digest (Poseidon2 digest domain per spec).
/// @param publicValues The public values the SP1 program committed, ABI-encoded as usual.
/// @param proofBytes The serialized Plonky3 shard (FRI/STARK) proof for this config.
function verifyProof(
bytes32 programVKey,
bytes calldata publicValues,
bytes calldata proofBytes
) external view {
bytes memory envelope = abi.encodePacked(
MAGIC,
WIRE_VERSION,
CONFIG_ID,
bytes2(0), // reserved
programVKey, // vkeyDigest(32)
uint32(publicValues.length), // publicValuesLen (BE)
publicValues,
uint32(proofBytes.length), // proofLen (BE)
proofBytes
);
(bool ok, bytes memory ret) = PQ_STARK_PRECOMPILE.staticcall(envelope);
// Success == precompile returned exactly the 32-byte word 0x..01. EMPTY (unactivated
// precompile, malformed input, or the skeleton's fail-closed path) -> revert. Fail-closed.
if (!ok || ret.length != 32 || bytes32(ret) != bytes32(uint256(1))) {
revert PQProofInvalid();
}
}
/// @notice Non-reverting variant for callers that already staticcall-wrap (e.g. the compliance
/// pool adapter and AereProofRegistry.verifySP1Only). Returns false instead of reverting.
function isValidProof(
bytes32 programVKey,
bytes calldata publicValues,
bytes calldata proofBytes
) external view returns (bool) {
bytes memory envelope = abi.encodePacked(
MAGIC, WIRE_VERSION, CONFIG_ID, bytes2(0),
programVKey,
uint32(publicValues.length), publicValues,
uint32(proofBytes.length), proofBytes
);
(bool ok, bytes memory ret) = PQ_STARK_PRECOMPILE.staticcall(envelope);
return ok && ret.length == 32 && bytes32(ret) == bytes32(uint256(1));
}
}