aere-contracts/contracts/pqfinality/AereFinalityCertificateVerifier.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

445 lines
22 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {ISP1Verifier} from "../zkverify/ISP1Verifier.sol";
/// @dev Minimal view surface of AerePQAttestationKeyRegistry this verifier reads.
interface IAerePQAttestationKeyRegistry {
function validatorSetRoot() external view returns (bytes32);
function validatorCount() external view returns (uint256);
}
/**
* @title AereFinalityCertificateVerifier, the on-chain half of Aere Network's ADDITIVE
* Post-Quantum Finality Certificate (chain 2800).
*
* @notice A Post-Quantum Finality Certificate is ONE succinct proof that a quorum of Aere
* validators attested to a finalized block with their HASH-BASED (post-quantum)
* attestation keys. Anyone, a light client, a bridge, an L2, or an AI agent, can
* verify with a SINGLE `verifyProof` call, at a cost INDEPENDENT of the validator
* count N, that an Aere block reached PQ finality, WITHOUT trusting the classical
* ECDSA seals and WITHOUT changing consensus.
*
* The off-chain aggregation circuit (the [MEASURE] research component) proves,
* inside a zkVM, that at least `ceil(2N/3)` DISTINCT validators, each committed
* under the registry's `validatorSetRoot`, produced a valid hash-based signature
* over the block. This contract does the cheap on-chain part: it binds the proof's
* public values to the live attestation-key registry, enforces the quorum
* threshold, and verifies the one proof through the live SP1 gateway.
*
* @dev THE HONEST BOUNDARY, read this before trusting or describing anything.
*
* 1. ADDITIVE, NOT A CONSENSUS FLIP. Aere consensus is STILL classical ECDSA QBFT.
* Validators keep signing ECDSA committed seals for block production and ALSO
* sign a hash-based attestation over each finalized block. This certificate
* proves the SECOND, additive quorum. It does not replace, gate, or flip
* consensus, it needs no re-genesis, and PQ finality is NOT live. Never imply
* Aere consensus is post-quantum: it is not.
*
* 2. THE OFF-CHAIN AGGREGATION zkVM CIRCUIT IS NOT IMPLEMENTED HERE. What is built
* and tested is the ON-CHAIN side: this verifier, the registry, the public-input
* binding scheme, the fail-closed checks, and a mock-gateway test. The zkVM guest
* that verifies N hash-based (XMSS / leanSig / SLH-DSA) signatures against the
* validator-set root and emits the public values is REAL, NOVEL cryptographic
* engineering that is NOT in this repository and is marked [MEASURE] in
* docs/AERE-PQ-FINALITY-CERTIFICATE.md. Verifying a stateful hash-based scheme
* (XMSS) or a leanSig scheme inside a zkVM, or an ML-DSA-in-zkVM alternative, is
* the research target. Do NOT describe the certificate as working end-to-end: the
* on-chain verifier is proven against a MOCK SP1 gateway that asserts the bound
* public values, the same test double the aggregate verifier and the light clients
* use.
*
* 3. WHY THIS IS QUANTUM-SAFE WHERE THE ECDSA PATH IS NOT. The attestation legs are
* hash-based signatures (post-quantum from hash collision / preimage resistance
* alone), and the aggregation proof is a hash-based STARK (SP1's core proof is a
* hash-based FRI STARK, Shor-resistant). This differs from AereZkQbftLightClient,
* which proves the CLASSICAL ECDSA committed-seal quorum and, when wrapped to
* Groth16 over BN254, is quantum-vulnerable. A quantum adversary that can forge
* ECDSA still cannot forge the hash-based attestations, so a PQ certificate stays
* sound. HONESTY: this quantum-safety property is a property of the DESIGN and of
* the [MEASURE] circuit once it is real; the on-chain contract alone does not
* establish it, and a Groth16-wrapped SP1 proof reintroduces a BN254 leg (see the
* doc for the STARK-verifier note).
*
* 4. THE PINNED CIRCUIT. PROGRAM_VKEY is the SP1 verification key of the ONE audited
* aggregation circuit this verifier accepts, fixed at construction. No caller can
* substitute a different circuit: every certificate is proven by the same pinned
* program through the same immutable gateway.
*
* 5. FAIL-CLOSED. A certificate is accepted ONLY when ALL hold, and rejected
* otherwise:
* - the registry validator set is non-empty (else EmptyValidatorSet),
* - the proof's validatorSetRoot equals the registry's CURRENT root
* (else ValidatorSetRootMismatch) -- so a rotation invalidates old certs,
* - the proof's validatorSetSize equals the registry's current N (else SizeMismatch),
* - the proof's attestationDomain equals this verifier's bound domain
* (else DomainMismatch) -- binds chainId + registry, blocks cross-context replay,
* - the proof's quorumCount is >= ceil(2N/3) (else BelowQuorum),
* - the proof's blockHash is non-zero (else ZeroBlockHash),
* - the SP1 proof verifies against the pinned vkey through the gateway
* (verifyProof REVERTS on an invalid proof).
*
* @dev PUBLIC-VALUES LAYOUT. The aggregation guest MUST output its public values as the
* fixed 192-byte ABI encoding of six words:
* abi.encode(
* bytes32 blockHash, // the finalized Aere block hash
* uint256 blockHeight, // its height
* bytes32 validatorSetRoot, // recomputed IN the circuit from the witnessed keys
* uint256 validatorSetSize, // N committed under the root
* uint256 quorumCount, // number of DISTINCT valid hash-based attestations, >= ceil(2N/3)
* bytes32 attestationDomain // domain tag; each key signed keccak256(domain, blockHash, blockHeight)
* )
* The circuit is responsible for: recomputing validatorSetRoot from the witnessed
* keys, verifying each counted hash-based signature against a DISTINCT committed key
* over the domain-bound block message, and never double-counting a validator. This
* contract trusts the proof for those facts and binds the public values to the live
* registry and to the quorum threshold. See docs/AERE-PQ-FINALITY-CERTIFICATE.md.
*/
contract AereFinalityCertificateVerifier {
/// @notice Exact byte length of the SP1 public values (six 32-byte words).
uint256 internal constant PUBLIC_VALUES_LEN = 192;
/// @notice Domain-separation prefix for the attestation message binding.
bytes32 public constant DOMAIN_PREFIX = keccak256("AerePQFinalityCertificate.v1");
// ==========================================================================
// Immutable wiring
// ==========================================================================
/// @notice The deployed SP1 verifier gateway that checks certificate proofs on-chain.
/// On Aere Network mainnet (chain 2800) this is the canonical Succinct gateway
/// at 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628. verifyProof REVERTS on an
/// invalid proof, which is the fail-closed backbone of this verifier.
ISP1Verifier public immutable SP1_GATEWAY;
/// @notice The SP1 verification key of the ONE audited aggregation circuit this verifier
/// accepts. Fixed at construction; no caller can substitute another circuit.
bytes32 public immutable PROGRAM_VKEY;
/// @notice The post-quantum attestation-key registry this verifier binds certificates to.
IAerePQAttestationKeyRegistry public immutable REGISTRY;
/// @notice The domain tag the circuit must bind: keccak256(DOMAIN_PREFIX, chainId, registry).
/// The message each validator's hash-based key signed is
/// keccak256(abi.encode(attestationDomain, blockHash, blockHeight)); pinning the
/// domain blocks replay of a certificate to a different chain or registry.
bytes32 public immutable attestationDomain;
// ==========================================================================
// Types
// ==========================================================================
/// @notice A recorded PQ finality certificate, keyed by block hash.
struct Certificate {
bool exists;
uint64 blockHeight;
uint32 quorumCount;
uint32 validatorSetSize;
bytes32 validatorSetRoot;
uint64 recordedBlock;
address prover;
}
// ==========================================================================
// Storage
// ==========================================================================
// finalized Aere block hash => recorded certificate
mapping(bytes32 => Certificate) private _certificates;
/// @notice The highest Aere block height recorded PQ-final so far (a convenience head).
uint64 public highestFinalizedHeight;
/// @notice The block hash at `highestFinalizedHeight`.
bytes32 public highestFinalizedBlockHash;
/// @notice Number of certificates recorded.
uint64 public certificateCount;
// ==========================================================================
// Events
// ==========================================================================
event CertificateRecorded(
bytes32 indexed blockHash,
uint64 indexed blockHeight,
uint32 quorumCount,
uint32 validatorSetSize,
bytes32 validatorSetRoot,
address prover
);
// ==========================================================================
// Errors
// ==========================================================================
error ZeroGateway();
error ZeroVKey();
error ZeroRegistry();
error VerifierHasNoCode();
error BadPublicValues(); // wrong length, not the fixed 192-byte layout
error EmptyValidatorSet();
error ValidatorSetRootMismatch(bytes32 provenRoot, bytes32 registeredRoot);
error SizeMismatch(uint256 provenSize, uint256 registeredSize);
error DomainMismatch(bytes32 provenDomain, bytes32 expectedDomain);
error BelowQuorum(uint256 provenQuorum, uint256 requiredQuorum);
error ZeroBlockHash();
error BlockHashMismatch(bytes32 provenBlockHash, bytes32 claimedBlockHash);
error BlockHeightMismatch(uint256 provenHeight, uint256 claimedHeight);
// ==========================================================================
// Construction
// ==========================================================================
/// @param gateway the deployed SP1 verifier gateway (chain 2800: 0x9ca4…0628).
/// @param programVKey the SP1 vkey of the one aggregation circuit this verifier accepts.
/// @param registry the AerePQAttestationKeyRegistry whose root certificates bind to.
constructor(address gateway, bytes32 programVKey, address registry) {
if (gateway == address(0)) revert ZeroGateway();
// A code-less gateway would make verifyProof (which returns no data) succeed silently,
// accepting every "proof". The deployed value must be the live SP1 gateway.
if (gateway.code.length == 0) revert VerifierHasNoCode();
if (programVKey == bytes32(0)) revert ZeroVKey();
if (registry == address(0)) revert ZeroRegistry();
SP1_GATEWAY = ISP1Verifier(gateway);
PROGRAM_VKEY = programVKey;
REGISTRY = IAerePQAttestationKeyRegistry(registry);
attestationDomain = keccak256(abi.encode(DOMAIN_PREFIX, block.chainid, registry));
}
// ==========================================================================
// Certificate verification
// ==========================================================================
/**
* @notice STRICT verification: reverts (fail-closed) unless the certificate proves a
* >= ceil(2N/3) hash-based attestation quorum over its block against the live
* registry. Verification is a SINGLE gateway verifyProof call; the gas does NOT
* grow with N (no per-validator work happens on-chain). Callable as a view by
* any relying contract. Returns the finalized (blockHash, blockHeight) on success.
*
* @param publicValues the SP1 public values, the fixed 192-byte layout documented above.
* @param proof the SP1 proof; its first 4 bytes select the gateway route.
*/
function verifyCertificate(bytes calldata publicValues, bytes calldata proof)
public
view
returns (bytes32 blockHash, uint64 blockHeight)
{
(
bytes32 pvBlockHash,
uint256 pvBlockHeight,
bytes32 pvRoot,
uint256 pvSize,
uint256 pvQuorum,
bytes32 pvDomain
) = _decode(publicValues);
// Bind to the LIVE registry state: the current root and size are the source of truth.
uint256 n = REGISTRY.validatorCount();
if (n == 0) revert EmptyValidatorSet();
bytes32 registeredRoot = REGISTRY.validatorSetRoot();
if (pvRoot != registeredRoot) revert ValidatorSetRootMismatch(pvRoot, registeredRoot);
if (pvSize != n) revert SizeMismatch(pvSize, n);
if (pvDomain != attestationDomain) revert DomainMismatch(pvDomain, attestationDomain);
uint256 required = _quorumThreshold(n);
if (pvQuorum < required) revert BelowQuorum(pvQuorum, required);
if (pvBlockHash == bytes32(0)) revert ZeroBlockHash();
// Single on-chain proof check. verifyProof reverts on an invalid proof, so a bad
// proof reverts the whole call and finalizes nothing. This is the only cryptographic
// gate and its cost is fixed by the SP1 verifier, independent of N.
SP1_GATEWAY.verifyProof(PROGRAM_VKEY, publicValues, proof);
return (pvBlockHash, uint64(pvBlockHeight));
}
/**
* @notice NON-REVERTING verification: returns true iff `verifyCertificate` would succeed,
* and false on ANY failure (malformed public values, a binding mismatch, below
* quorum, or an invalid proof). For consumers that prefer a boolean gate.
*/
function tryVerifyCertificate(bytes calldata publicValues, bytes calldata proof) external view returns (bool) {
try this.verifyCertificate(publicValues, proof) returns (bytes32, uint64) {
return true;
} catch {
return false;
}
}
/**
* @notice STRICT verification bound to a CLAIMED block: like `verifyCertificate` but
* additionally requires the proof to finalize EXACTLY (claimedBlockHash,
* claimedHeight). A consumer that wants to confirm a SPECIFIC block is PQ-final
* (a bridge releasing funds for block X) uses this so a certificate for a
* different block cannot be substituted. Reverts BlockHashMismatch /
* BlockHeightMismatch on a mismatch.
*/
function assertFinalizes(
bytes32 claimedBlockHash,
uint64 claimedHeight,
bytes calldata publicValues,
bytes calldata proof
) public view returns (bool) {
(bytes32 blockHash, uint64 blockHeight) = verifyCertificate(publicValues, proof);
if (blockHash != claimedBlockHash) revert BlockHashMismatch(blockHash, claimedBlockHash);
if (blockHeight != claimedHeight) revert BlockHeightMismatch(blockHeight, claimedHeight);
return true;
}
/// @notice Non-reverting variant of `assertFinalizes`.
function tryAssertFinalizes(
bytes32 claimedBlockHash,
uint64 claimedHeight,
bytes calldata publicValues,
bytes calldata proof
) external view returns (bool) {
try this.assertFinalizes(claimedBlockHash, claimedHeight, publicValues, proof) returns (bool ok) {
return ok;
} catch {
return false;
}
}
// ==========================================================================
// Certificate recording
// ==========================================================================
/**
* @notice STATE-CHANGING: verify the certificate (strict, fail-closed) and record it,
* keyed by the finalized block hash, so a consumer contract can later gate on
* PQ finality via `isFinalPQ`. Reverts unless the proof verifies, so a recorded
* certificate is always backed by a valid proof over a quorum. Permissionless.
*
* @dev Re-recording the same block hash refreshes the record's provenance; it grants
* no new authority. The head (highestFinalizedHeight / BlockHash) advances only
* on a strictly greater height.
*/
function recordCertificate(bytes calldata publicValues, bytes calldata proof)
external
returns (bytes32 blockHash, uint64 blockHeight)
{
(blockHash, blockHeight) = verifyCertificate(publicValues, proof);
(, , bytes32 pvRoot, uint256 pvSize, uint256 pvQuorum, ) = _decode(publicValues);
bool isNew = !_certificates[blockHash].exists;
_certificates[blockHash] = Certificate({
exists: true,
blockHeight: blockHeight,
quorumCount: uint32(pvQuorum),
validatorSetSize: uint32(pvSize),
validatorSetRoot: pvRoot,
recordedBlock: uint64(block.number),
prover: msg.sender
});
if (isNew) certificateCount += 1;
if (blockHeight > highestFinalizedHeight) {
highestFinalizedHeight = blockHeight;
highestFinalizedBlockHash = blockHash;
}
emit CertificateRecorded(
blockHash, blockHeight, uint32(pvQuorum), uint32(pvSize), pvRoot, msg.sender
);
}
// ==========================================================================
// Consumer gating / views
// ==========================================================================
/// @notice True iff a PQ finality certificate has been recorded for `blockHash`. This is
/// the gate a bridge / light client / L2 / AI agent checks before acting on Aere
/// finality quantum-safely.
function isFinalPQ(bytes32 blockHash) external view returns (bool) {
return _certificates[blockHash].exists;
}
/// @notice True iff `blockHash` is recorded PQ-final AND at exactly `blockHeight`.
function isFinalPQAtHeight(bytes32 blockHash, uint64 blockHeight) external view returns (bool) {
Certificate storage c = _certificates[blockHash];
return c.exists && c.blockHeight == blockHeight;
}
/// @notice Read a recorded certificate. `exists` is false for an unknown block hash.
function certificateOf(bytes32 blockHash)
external
view
returns (
bool exists,
uint64 blockHeight,
uint32 quorumCount,
uint32 validatorSetSize,
bytes32 validatorSetRoot,
uint64 recordedBlock,
address prover
)
{
Certificate storage c = _certificates[blockHash];
return (
c.exists, c.blockHeight, c.quorumCount, c.validatorSetSize, c.validatorSetRoot, c.recordedBlock, c.prover
);
}
/// @notice The quorum threshold ceil(2N/3) for the current registry validator set.
function currentQuorumThreshold() external view returns (uint256) {
return _quorumThreshold(REGISTRY.validatorCount());
}
/**
* @notice Helper for off-chain tooling and the circuit harness: the exact 192-byte public
* values a certificate finalizing (blockHash, blockHeight) with `quorumCount`
* distinct attestations must emit, bound to the CURRENT registry root and size.
*/
function expectedPublicValues(bytes32 blockHash, uint64 blockHeight, uint256 quorumCount)
external
view
returns (bytes memory)
{
return abi.encode(
blockHash,
uint256(blockHeight),
REGISTRY.validatorSetRoot(),
REGISTRY.validatorCount(),
quorumCount,
attestationDomain
);
}
/// @notice The message an attestation key signs for (blockHash, blockHeight) under this
/// verifier's domain. The circuit checks each hash-based signature over THIS value.
function attestationMessage(bytes32 blockHash, uint64 blockHeight) external view returns (bytes32) {
return keccak256(abi.encode(attestationDomain, blockHash, blockHeight));
}
// ==========================================================================
// Internal
// ==========================================================================
/// @dev ceil(2N/3): the QBFT quorum. N=7 -> 5, N=9 -> 6, N=4 -> 3.
function _quorumThreshold(uint256 n) internal pure returns (uint256) {
return (2 * n + 2) / 3;
}
/// @dev Decode the fixed 192-byte public-values layout. Reverts BadPublicValues on any
/// other length; a correct-length blob always decodes without reverting.
function _decode(bytes calldata publicValues)
internal
pure
returns (
bytes32 blockHash,
uint256 blockHeight,
bytes32 validatorSetRoot,
uint256 validatorSetSize,
uint256 quorumCount,
bytes32 domain
)
{
if (publicValues.length != PUBLIC_VALUES_LEN) revert BadPublicValues();
(blockHash, blockHeight, validatorSetRoot, validatorSetSize, quorumCount, domain) =
abi.decode(publicValues, (bytes32, uint256, bytes32, uint256, uint256, bytes32));
}
}