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.
331 lines
14 KiB
Solidity
331 lines
14 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 AereEVMValidity uses it.
|
|
interface ISp1Verifier {
|
|
function verifyProof(
|
|
bytes32 programVKey,
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
) external view;
|
|
}
|
|
|
|
/**
|
|
* @title AereEVMValidityV2 - CANONICAL-BOUND on-chain anchor for REAL FULL-EVM
|
|
* validity proofs of live chain-2800 blocks.
|
|
*
|
|
* @dev STAGED / DO-NOT-DEPLOY-UNTIL-FORK: this contract, its sibling
|
|
* AereEVMValidityBatchV2, and the re-proved SP1 guest are STAGED and MUST NOT
|
|
* be deployed until (1) the 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 binding. See aerenew/rollup-evm-validity/NOTE.md.
|
|
*
|
|
* @notice Successor to AereEVMValidity. It fixes the canonical-binding gap in V1:
|
|
* V1 recorded an SP1 postStateRoot as block N with NO blockHash/parentHash
|
|
* in the public values and NO on-chain blockhash() check, so a genuine SP1
|
|
* proof of a FABRICATED execution could be anchored as block N and an
|
|
* attacker could squat slot N via first-write-wins. V2 binds every record
|
|
* to the real canonical chain.
|
|
*
|
|
* The guest (bin/aere-client) re-executes a real chain-2800 (AERE mainnet,
|
|
* QBFT / Besu) block with revm INSIDE the SP1 zkVM, re-roots the sparse
|
|
* trie, and only produces a valid proof when the recomputed post-state root
|
|
* equals the block's header.stateRoot. The V2 guest ALSO commits the block's
|
|
* own blockHash and its parentHash and enforces, inside the circuit, that
|
|
* prevStateRoot == parent.stateRoot.
|
|
*
|
|
* PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact
|
|
* V2 guest ELF. Its vkey differs from the V1 guest's, so a V1 proof cannot
|
|
* be recorded here and vice-versa.
|
|
*
|
|
* PUBLIC-VALUES CONVENTION (exactly 288 bytes = 9 x 32):
|
|
* abi.encode(
|
|
* uint256 chainId, // must equal block.chainid (2800)
|
|
* uint256 blockNumber, // the proven chain-2800 block height
|
|
* bytes32 parentHash, // header.parentHash of the proven block
|
|
* bytes32 blockHash, // the proven block's own canonical hash
|
|
* bytes32 prevStateRoot, // parent block state root (pre-state anchor)
|
|
* bytes32 postStateRoot, // proven post-execution state root
|
|
* bytes32 txRoot, // header.transactionsRoot (binds the txs)
|
|
* uint256 gasUsed, // total gas used by the block
|
|
* uint256 numTxns // number of transactions executed
|
|
* )
|
|
*
|
|
* CANONICAL BINDING: recordBlock resolves the on-chain canonical hash of
|
|
* blockNumber and requires it to equal the committed blockHash. Recent
|
|
* blocks (within the 256-block EVM window) use blockhash(); older blocks
|
|
* consult the EIP-2935 history-storage ring at
|
|
* 0x0000F90827F1C53a10cb7A02335B175320002935 (once that fork is live). If
|
|
* the canonical hash is unavailable, or does not match, the call reverts,
|
|
* so a fabricated execution can never be anchored as a real block. When the
|
|
* parent's canonical hash is also resolvable, the committed parentHash is
|
|
* cross-checked against it too.
|
|
*
|
|
* 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 AND the on-chain canonical-hash binding.
|
|
*
|
|
* ===========================================================================
|
|
* HONEST SCOPE - READ THIS.
|
|
* ===========================================================================
|
|
* This proves REAL EVM execution of a SMALL live chain-2800 block (a handful
|
|
* of transactions) and binds it to the canonical chain via blockhash. It is a
|
|
* legitimate real-EVM validity PROOF-OF-APPROACH. It is NOT yet a full-throughput
|
|
* production rollup: it does not prove a large batch, and it records isolated
|
|
* bound blocks rather than a continuous sequencer chain.
|
|
* ===========================================================================
|
|
*/
|
|
contract AereEVMValidityV2 {
|
|
|
|
/// @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 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, canonically-bound blocks recorded.
|
|
uint256 public provenCount;
|
|
|
|
struct ProvenBlock {
|
|
uint64 blockNumber;
|
|
bytes32 parentHash;
|
|
bytes32 blockHash;
|
|
bytes32 prevStateRoot;
|
|
bytes32 postStateRoot;
|
|
bytes32 txRoot;
|
|
uint64 gasUsed;
|
|
uint32 numTxns;
|
|
uint64 at; // block timestamp when the proof verified
|
|
address prover; // who submitted it
|
|
}
|
|
|
|
/// @notice recordId (0-based) => proven block record.
|
|
mapping(uint256 => ProvenBlock) public records;
|
|
|
|
/// @notice chain-2800 blockNumber => 1-based recordId (0 == not proven).
|
|
mapping(uint256 => uint256) public blockToRecord;
|
|
|
|
/// @notice The most recently proven post-state root (informational tip).
|
|
bytes32 public latestPostRoot;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event BlockProven(
|
|
uint256 indexed recordId,
|
|
uint256 indexed blockNumber,
|
|
bytes32 parentHash,
|
|
bytes32 blockHash,
|
|
bytes32 prevStateRoot,
|
|
bytes32 postStateRoot,
|
|
bytes32 txRoot,
|
|
uint256 gasUsed,
|
|
uint256 numTxns,
|
|
address prover,
|
|
uint256 at
|
|
);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error InvalidProof();
|
|
error BadPublicValuesLength(uint256 got);
|
|
error WrongChain(uint256 inProof, uint256 onChain);
|
|
error AlreadyProven(uint256 blockNumber, uint256 recordId);
|
|
error BlockHashUnavailable(uint256 blockNumber);
|
|
error BlockHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
|
|
error ParentHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
|
|
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;
|
|
}
|
|
|
|
/* --------------------------- 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) {
|
|
// No hash exists for the current or a future block.
|
|
if (blockNumber >= block.number) return bytes32(0);
|
|
|
|
// Recent window: the EVM exposes the last 256 ancestors directly.
|
|
if (block.number - blockNumber <= 256) {
|
|
return blockhash(blockNumber);
|
|
}
|
|
|
|
// Older block: ask the EIP-2935 history-storage ring. Before that fork is
|
|
// live the address has no code; a staticcall then succeeds with empty
|
|
// returndata, so ret.length != 32 and we report "unavailable".
|
|
(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 block ---------------------------- */
|
|
|
|
/// @notice Verify a full-EVM validity proof for a chain-2800 block, bind it to
|
|
/// the canonical chain, and record it. Permissionless. Reverts if the
|
|
/// proof is not valid for PROGRAM_VKEY, if it is for a different chain,
|
|
/// if the block's canonical hash is unavailable or does not match the
|
|
/// committed blockHash, or if that block number was already recorded.
|
|
/// @param publicValues abi.encode(chainId, blockNumber, parentHash, blockHash,
|
|
/// prevStateRoot, postStateRoot, txRoot, gasUsed, numTxns),
|
|
/// 288 bytes.
|
|
/// @param proof SP1 Groth16 proof bytes (leading selector 0x4388a21c).
|
|
/// @return recordId the 0-based id of the recorded proven block.
|
|
function recordBlock(
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
) external returns (uint256 recordId) {
|
|
// 9 x 32 = 288 bytes.
|
|
if (publicValues.length != 288) 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 blockNumber,
|
|
bytes32 parentHash,
|
|
bytes32 blockHash,
|
|
bytes32 prevStateRoot,
|
|
bytes32 postStateRoot,
|
|
bytes32 txRoot,
|
|
uint256 gasUsed,
|
|
uint256 numTxns
|
|
) = abi.decode(
|
|
publicValues,
|
|
(uint256, uint256, bytes32, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
|
|
);
|
|
|
|
// 2) bind to this chain.
|
|
if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
|
|
|
|
// 3) CANONICAL BINDING: the committed blockHash must equal the on-chain
|
|
// canonical hash of blockNumber. This is what makes the recorded root
|
|
// canonical rather than merely self-consistent.
|
|
bytes32 onChainHash = canonicalHash(blockNumber);
|
|
if (onChainHash == bytes32(0)) revert BlockHashUnavailable(blockNumber);
|
|
if (onChainHash != blockHash) {
|
|
revert BlockHashMismatch(blockNumber, onChainHash, blockHash);
|
|
}
|
|
|
|
// 3b) When the parent's canonical hash is also resolvable, cross-check the
|
|
// committed parentHash against it. This closes fabricated-parent tricks
|
|
// without ever blocking a legitimate in-window record.
|
|
if (blockNumber > 0) {
|
|
bytes32 onChainParent = canonicalHash(blockNumber - 1);
|
|
if (onChainParent != bytes32(0) && onChainParent != parentHash) {
|
|
revert ParentHashMismatch(blockNumber, onChainParent, parentHash);
|
|
}
|
|
}
|
|
|
|
// 4) one record per block number.
|
|
uint256 existing = blockToRecord[blockNumber];
|
|
if (existing != 0) revert AlreadyProven(blockNumber, existing - 1);
|
|
|
|
// 5) record the proven, canonically-bound block.
|
|
recordId = provenCount;
|
|
records[recordId] = ProvenBlock({
|
|
blockNumber: uint64(blockNumber),
|
|
parentHash: parentHash,
|
|
blockHash: blockHash,
|
|
prevStateRoot: prevStateRoot,
|
|
postStateRoot: postStateRoot,
|
|
txRoot: txRoot,
|
|
gasUsed: uint64(gasUsed),
|
|
numTxns: uint32(numTxns),
|
|
at: uint64(block.timestamp),
|
|
prover: msg.sender
|
|
});
|
|
blockToRecord[blockNumber] = recordId + 1; // 1-based so 0 means "unseen"
|
|
latestPostRoot = postStateRoot;
|
|
provenCount = recordId + 1;
|
|
|
|
emit BlockProven(
|
|
recordId, blockNumber, parentHash, blockHash, prevStateRoot,
|
|
postStateRoot, txRoot, gasUsed, 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 and records nothing; recordBlock is the
|
|
/// canonical-bound path.
|
|
function verify(
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
)
|
|
external
|
|
view
|
|
returns (
|
|
uint256 blockNumber,
|
|
bytes32 parentHash,
|
|
bytes32 blockHash,
|
|
bytes32 prevStateRoot,
|
|
bytes32 postStateRoot,
|
|
bytes32 txRoot,
|
|
uint256 gasUsed,
|
|
uint256 numTxns
|
|
)
|
|
{
|
|
if (publicValues.length != 288) revert BadPublicValuesLength(publicValues.length);
|
|
ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
|
|
(
|
|
, blockNumber, parentHash, blockHash, prevStateRoot,
|
|
postStateRoot, txRoot, gasUsed, numTxns
|
|
) = abi.decode(
|
|
publicValues,
|
|
(uint256, uint256, bytes32, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
|
|
);
|
|
}
|
|
|
|
/// @notice Convenience accessor for a recorded proven block.
|
|
function getRecord(uint256 recordId)
|
|
external
|
|
view
|
|
returns (
|
|
uint64 blockNumber,
|
|
bytes32 parentHash,
|
|
bytes32 blockHash,
|
|
bytes32 prevStateRoot,
|
|
bytes32 postStateRoot,
|
|
bytes32 txRoot,
|
|
uint64 gasUsed,
|
|
uint32 numTxns,
|
|
uint64 at,
|
|
address prover
|
|
)
|
|
{
|
|
ProvenBlock memory r = records[recordId];
|
|
return (
|
|
r.blockNumber, r.parentHash, r.blockHash, r.prevStateRoot,
|
|
r.postStateRoot, r.txRoot, r.gasUsed, r.numTxns, r.at, r.prover
|
|
);
|
|
}
|
|
}
|