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.
445 lines
22 KiB
Solidity
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));
|
|
}
|
|
}
|