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.
357 lines
15 KiB
Solidity
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;
|
|
}
|
|
}
|