aere-contracts/contracts/parallel/AereEVMValidityBatchV2.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

357 lines
15 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
/// NOTHING and REVERTS on an invalid proof. On chain 2800 the deployed
/// SP1VerifierGateway is 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 and the
/// SP1 Groth16 route is selected by the proof's leading 4-byte selector
/// 0x4388a21c (SP1 v6 groth16), exactly as AereEVMValidityBatch uses it.
interface ISp1Verifier {
function verifyProof(
bytes32 programVKey,
bytes calldata publicValues,
bytes calldata proof
) external view;
}
/**
* @title AereEVMValidityBatchV2 - CANONICAL-BOUND, CONTINUITY-ENFORCED anchor for
* REAL FULL-EVM validity proofs of a CONTIGUOUS BATCH of chain-2800 blocks.
*
* @dev STAGED / DO-NOT-DEPLOY-UNTIL-FORK: this contract, its sibling
* AereEVMValidityV2, and the re-proved SP1 batch guest are STAGED and MUST NOT
* be deployed until (1) the batch guest is re-proved and its new vkey wired
* into PROGRAM_VKEY, and (2) the AerePQC futureEips SUPERVISED MAINNET FORK is
* live on chain 2800 so EIP-2935 history storage backs canonicalHash() for
* older-than-256-block tip binding. See aerenew/rollup-evm-validity/NOTE.md.
*
* @notice Successor to AereEVMValidityBatch. It fixes two canonical-binding gaps in
* V1: (a) the recorded batch tip was never bound to the real chain (no
* blockHash, no blockhash() check), and (b) there was no continuity between
* records, so an attacker could anchor overlapping or fabricated ranges.
*
* The V2 batch guest re-executes every block in [firstBlock, lastBlock] with
* revm INSIDE the SP1 zkVM, threading each block's recomputed post-state root
* into the next block's pre-state, enforces prevStateRoot == parent.stateRoot
* at the range boundary inside the circuit, and commits the batch tip's
* blockHash plus firstBlock's parentHash. A valid proof exists only when the
* final recomputed state root equals the canonical header.stateRoot of the
* last block.
*
* PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact V2
* BATCH guest ELF. It differs from the V1 batch guest and from the
* single-block guest, so proofs are not interchangeable.
*
* PUBLIC-VALUES CONVENTION (exactly 320 bytes = 10 x 32):
* abi.encode(
* uint256 chainId, // must equal block.chainid (2800)
* uint256 firstBlock, // first proven chain-2800 block height
* uint256 lastBlock, // last proven chain-2800 block height
* uint256 numBlocks, // count of blocks in the batch
* bytes32 firstParentHash, // header.parentHash of firstBlock
* bytes32 lastBlockHash, // canonical hash of lastBlock (the tip)
* bytes32 prevStateRoot, // parent state root of firstBlock
* bytes32 postStateRoot, // proven post-state root of lastBlock
* uint256 totalGas, // total gas used across the batch
* uint256 numTxns // total transactions executed across the batch
* )
*
* CANONICAL BINDING: recordBatch resolves the on-chain canonical hash of
* lastBlock and requires it to equal the committed lastBlockHash. Recent
* blocks use the 256-deep EVM blockhash window; older blocks consult the
* EIP-2935 history ring at 0x0000F90827F1C53a10cb7A02335B175320002935 (once
* that fork is live). If the tip hash is unavailable or mismatched, the call
* reverts, so a fabricated range can never be anchored.
*
* CONTINUITY: once at least one batch is recorded, every subsequent batch
* must satisfy firstBlock == priorBatch.lastBlock + 1 AND prevStateRoot ==
* priorBatch.postStateRoot. This forms a single, gap-free, fork-free chain
* of proven ranges (the first recorded batch anchors the chain).
*
* IMMUTABILITY / TRUST: SP1_VERIFIER and PROGRAM_VKEY are fixed at deploy.
* There is NO owner and NO admin: recording is permissionless. Soundness
* rests on the SP1 Groth16 proof, the canonical tip binding, and continuity.
*
* ===========================================================================
* HONEST SCOPE - READ THIS.
* ===========================================================================
* This proves REAL EVM execution of a SMALL contiguous batch of live chain-2800
* blocks and binds the tip to the canonical chain. It is a legitimate real-EVM
* validity PROOF-OF-APPROACH for batched blocks. It is NOT yet a full-throughput
* production rollup: batch size is small.
* ===========================================================================
*/
contract AereEVMValidityBatchV2 {
/// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector).
address public immutable SP1_VERIFIER;
/// @notice SP1 vkey binding the exact V2 revm-in-zkVM BATCH guest ELF.
bytes32 public immutable PROGRAM_VKEY;
/// @notice EIP-2935 history-storage ring (serves historical block hashes once
/// the fork is live; before then this address has no code).
address internal constant HISTORY_STORAGE_ADDRESS =
0x0000F90827F1C53a10cb7A02335B175320002935;
/// @notice Number of proven batches recorded.
uint256 public provenCount;
struct ProvenBatch {
uint64 firstBlock;
uint64 lastBlock;
uint32 numBlocks;
bytes32 firstParentHash;
bytes32 lastBlockHash;
bytes32 prevStateRoot;
bytes32 postStateRoot;
uint64 totalGas;
uint32 numTxns;
uint64 at; // block timestamp when the proof verified
address prover; // who submitted it
}
/// @notice recordId (0-based) => proven batch record.
mapping(uint256 => ProvenBatch) public records;
/// @notice keccak256(firstBlock, lastBlock) => 1-based recordId (0 == unseen).
mapping(bytes32 => uint256) public batchToRecord;
/// @notice The most recently proven post-state root (continuity anchor / tip).
bytes32 public latestPostRoot;
/// @notice The last block of the most recently proven batch (continuity anchor).
uint64 public latestLastBlock;
/* --------------------------------- events ------------------------------- */
event BatchProven(
uint256 indexed recordId,
uint256 indexed firstBlock,
uint256 indexed lastBlock,
uint256 numBlocks,
bytes32 firstParentHash,
bytes32 lastBlockHash,
bytes32 prevStateRoot,
bytes32 postStateRoot,
uint256 totalGas,
uint256 numTxns,
address prover,
uint256 at
);
/* --------------------------------- errors ------------------------------- */
error InvalidProof();
error BadPublicValuesLength(uint256 got);
error WrongChain(uint256 inProof, uint256 onChain);
error BadRange(uint256 firstBlock, uint256 lastBlock, uint256 numBlocks);
error AlreadyProven(uint256 firstBlock, uint256 lastBlock, uint256 recordId);
error BlockHashUnavailable(uint256 blockNumber);
error BlockHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
error Discontinuous(uint256 gotFirstBlock, uint256 wantFirstBlock, bytes32 gotPrevRoot, bytes32 wantPrevRoot);
error ZeroAddress();
error ZeroVKey();
/* ----------------------------- constructor ------------------------------ */
constructor(address sp1Verifier, bytes32 programVKey) {
if (sp1Verifier == address(0)) revert ZeroAddress();
if (programVKey == bytes32(0)) revert ZeroVKey();
SP1_VERIFIER = sp1Verifier;
PROGRAM_VKEY = programVKey;
}
/* ------------------------------ batch key ------------------------------- */
/// @notice Deterministic dedupe key for a contiguous batch range.
function batchKey(uint256 firstBlock, uint256 lastBlock) public pure returns (bytes32) {
return keccak256(abi.encodePacked(uint64(firstBlock), uint64(lastBlock)));
}
/* --------------------------- canonical hash ----------------------------- */
/// @notice Resolve the canonical hash of a past block, or bytes32(0) if it is
/// not available. Recent blocks use the 256-deep EVM blockhash window;
/// older blocks consult the EIP-2935 history ring (empty before the
/// fork is live, which yields bytes32(0) and a safe revert upstream).
function canonicalHash(uint256 blockNumber) public view returns (bytes32) {
if (blockNumber >= block.number) return bytes32(0);
if (block.number - blockNumber <= 256) {
return blockhash(blockNumber);
}
(bool ok, bytes memory ret) =
HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber));
if (!ok || ret.length != 32) return bytes32(0);
return abi.decode(ret, (bytes32));
}
/* ----------------------------- record batch ----------------------------- */
/// @notice Verify a full-EVM validity proof for a contiguous batch of chain-2800
/// blocks, bind the tip to the canonical chain, enforce continuity with
/// the previously recorded batch, and record it. Permissionless.
/// @param publicValues abi.encode(chainId, firstBlock, lastBlock, numBlocks,
/// firstParentHash, lastBlockHash, prevStateRoot,
/// postStateRoot, totalGas, numTxns), 320 bytes.
/// @param proof SP1 Groth16 proof bytes (leading selector 0x4388a21c).
/// @return recordId the 0-based id of the recorded proven batch.
function recordBatch(
bytes calldata publicValues,
bytes calldata proof
) external returns (uint256 recordId) {
// 10 x 32 = 320 bytes.
if (publicValues.length != 320) revert BadPublicValuesLength(publicValues.length);
// 1) zk proof: reverts if the proof is not valid for PROGRAM_VKEY.
try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
// verified - did not revert
} catch {
revert InvalidProof();
}
(
uint256 chainId,
uint256 firstBlock,
uint256 lastBlock,
uint256 numBlocks,
bytes32 firstParentHash,
bytes32 lastBlockHash,
bytes32 prevStateRoot,
bytes32 postStateRoot,
uint256 totalGas,
uint256 numTxns
) = abi.decode(
publicValues,
(uint256, uint256, uint256, uint256, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
);
// 2) bind to this chain.
if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
// 3) contiguity invariant guaranteed by the batch guest.
if (lastBlock < firstBlock || numBlocks != lastBlock - firstBlock + 1) {
revert BadRange(firstBlock, lastBlock, numBlocks);
}
// 4) CANONICAL TIP BINDING: committed lastBlockHash must equal the on-chain
// canonical hash of lastBlock.
bytes32 tipHash = canonicalHash(lastBlock);
if (tipHash == bytes32(0)) revert BlockHashUnavailable(lastBlock);
if (tipHash != lastBlockHash) {
revert BlockHashMismatch(lastBlock, tipHash, lastBlockHash);
}
// 5) CONTINUITY: after the first record, ranges must chain gap-free and the
// pre-state must equal the prior batch's proven post-state.
if (provenCount != 0) {
uint256 wantFirst = uint256(latestLastBlock) + 1;
if (firstBlock != wantFirst || prevStateRoot != latestPostRoot) {
revert Discontinuous(firstBlock, wantFirst, prevStateRoot, latestPostRoot);
}
}
// 6) one record per exact range.
bytes32 key = batchKey(firstBlock, lastBlock);
uint256 existing = batchToRecord[key];
if (existing != 0) revert AlreadyProven(firstBlock, lastBlock, existing - 1);
// 7) record the proven, canonically-bound batch.
recordId = provenCount;
records[recordId] = ProvenBatch({
firstBlock: uint64(firstBlock),
lastBlock: uint64(lastBlock),
numBlocks: uint32(numBlocks),
firstParentHash: firstParentHash,
lastBlockHash: lastBlockHash,
prevStateRoot: prevStateRoot,
postStateRoot: postStateRoot,
totalGas: uint64(totalGas),
numTxns: uint32(numTxns),
at: uint64(block.timestamp),
prover: msg.sender
});
batchToRecord[key] = recordId + 1; // 1-based so 0 means "unseen"
latestPostRoot = postStateRoot;
latestLastBlock = uint64(lastBlock);
provenCount = recordId + 1;
emit BatchProven(
recordId, firstBlock, lastBlock, numBlocks, firstParentHash,
lastBlockHash, prevStateRoot, postStateRoot, totalGas, numTxns,
msg.sender, block.timestamp
);
}
/* ----------------------------------- views ------------------------------ */
/// @notice Stateless verification: reverts if the proof is invalid for
/// PROGRAM_VKEY, otherwise returns the decoded fields. Does NOT check
/// the canonical blockhash or continuity and records nothing.
function verify(
bytes calldata publicValues,
bytes calldata proof
)
external
view
returns (
uint256 firstBlock,
uint256 lastBlock,
uint256 numBlocks,
bytes32 firstParentHash,
bytes32 lastBlockHash,
bytes32 prevStateRoot,
bytes32 postStateRoot,
uint256 totalGas,
uint256 numTxns
)
{
if (publicValues.length != 320) revert BadPublicValuesLength(publicValues.length);
ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
(
, firstBlock, lastBlock, numBlocks, firstParentHash, lastBlockHash,
prevStateRoot, postStateRoot, totalGas, numTxns
) = abi.decode(
publicValues,
(uint256, uint256, uint256, uint256, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
);
}
/// @notice Convenience accessor for a recorded proven batch.
function getRecord(uint256 recordId)
external
view
returns (
uint64 firstBlock,
uint64 lastBlock,
uint32 numBlocks,
bytes32 firstParentHash,
bytes32 lastBlockHash,
bytes32 prevStateRoot,
bytes32 postStateRoot,
uint64 totalGas,
uint32 numTxns,
uint64 at,
address prover
)
{
ProvenBatch memory r = records[recordId];
return (
r.firstBlock, r.lastBlock, r.numBlocks, r.firstParentHash,
r.lastBlockHash, r.prevStateRoot, r.postStateRoot, r.totalGas,
r.numTxns, r.at, r.prover
);
}
/// @notice recordId for an exact range, or reverts if that range is unseen.
function recordIdForRange(uint256 firstBlock, uint256 lastBlock)
external
view
returns (uint256 recordId)
{
uint256 existing = batchToRecord[batchKey(firstBlock, lastBlock)];
require(existing != 0, "range not proven");
return existing - 1;
}
}