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.
224 lines
9.6 KiB
Solidity
224 lines
9.6 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 AereZKMLVerifier uses it.
|
|
interface ISp1Verifier {
|
|
function verifyProof(
|
|
bytes32 programVKey,
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
) external view;
|
|
}
|
|
|
|
/**
|
|
* @title AereRollupValidity - on-chain anchor for ZERO-KNOWLEDGE VALIDITY PROOFS
|
|
* of AERE's rollup executor.
|
|
*
|
|
* @notice Verifies SP1 zkVM Groth16 proofs that AERE's deterministic rollup
|
|
* executor (the `aere-block-stm` sequential oracle) transitioned a prior
|
|
* state root `prevRoot` to a new state root `postRoot` by applying a
|
|
* specific batch of transactions committed as `batchHash`. On a valid
|
|
* proof that continues the canonical chain (prevRoot == the last accepted
|
|
* postRoot) it records a validated rollup epoch and advances `latestRoot`.
|
|
*
|
|
* This is a VALIDITY (zk) rollup settlement anchor: unlike the optimistic
|
|
* AereRollupSettlement (propose + challenge window), an epoch recorded
|
|
* here is final the instant the proof verifies. No fraud-proof window is
|
|
* required because the zk proof already establishes correctness.
|
|
*
|
|
* PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact
|
|
* guest ELF (program/src/main.rs) that ports AERE's transaction semantics
|
|
* (execute_txn), keccak256 and state-root folding VERBATIM from
|
|
* parallel-executor/src/{vm,keccak}.rs. Binding the vkey binds the exact
|
|
* executor whose transition is being proven.
|
|
*
|
|
* PUBLIC-VALUES CONVENTION (exactly 160 bytes):
|
|
* abi.encode(
|
|
* uint256 chainId, // must equal block.chainid (2800)
|
|
* bytes32 prevRoot, // keccak256 state root before the batch
|
|
* bytes32 postRoot, // keccak256 state root after the batch
|
|
* bytes32 batchHash, // keccak256 of the canonical batch encoding
|
|
* uint256 numTxns // number of transactions in the batch
|
|
* )
|
|
*
|
|
* BATCH HASH ENCODING (matches the guest's batch_hash, so an operator can
|
|
* recompute it off-chain to identify the batch): num_txns (u32 BE), then
|
|
* per tx a 1-byte tag and big-endian fields, then gas (u32 BE):
|
|
* 0x00 Transfer : from u64 | to u64 | amount u128
|
|
* 0x01 Sweep : from u64 | to u64
|
|
* 0x02 Increment : contract u64 | slot u64
|
|
* 0x03 AmmSwap : contract u64 | x_slot u64 | y_slot u64 | dx u128
|
|
*
|
|
* IMMUTABILITY / TRUST: SP1_VERIFIER, PROGRAM_VKEY and GENESIS_ROOT are
|
|
* fixed at deploy. There is NO owner and NO admin: epoch submission is
|
|
* permissionless. Soundness rests entirely on the SP1 Groth16 proof plus
|
|
* the prevRoot == latestRoot continuity check, so any party running the
|
|
* prover can advance the canonical chain, and none can forge or fork it.
|
|
*
|
|
* ===========================================================================
|
|
* HONEST SCOPE - READ THIS.
|
|
* ===========================================================================
|
|
* This proves the state transition of AERE's CURRENT rollup executor, whose VM
|
|
* implements a BOUNDED set of transaction kinds (Transfer, Sweep, Increment,
|
|
* AmmSwap) over a balance/storage map. It is NOT arbitrary EVM bytecode.
|
|
*
|
|
* It is a REAL validity proof of THAT executor: the proven computation is
|
|
* byte-identical to what the production `aere-block-stm` binary self-checks and
|
|
* what BlockSTMExecutor commits via AereRollupSettlement.proposeStateRoot.
|
|
*
|
|
* The honest next step toward a FULL validity rollup is replacing the
|
|
* executor's VM with a real EVM (revm) inside the zkVM, which is a separate
|
|
* multi-quarter effort. This contract does NOT prove a full EVM, and no code,
|
|
* UI or claim built on it should imply that it does.
|
|
* ===========================================================================
|
|
*/
|
|
contract AereRollupValidity {
|
|
|
|
/// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector).
|
|
address public immutable SP1_VERIFIER;
|
|
|
|
/// @notice SP1 vkey binding the exact rollup-executor guest ELF.
|
|
bytes32 public immutable PROGRAM_VKEY;
|
|
|
|
/// @notice Pinned starting state root; epoch 0 must prove prevRoot == this.
|
|
bytes32 public immutable GENESIS_ROOT;
|
|
|
|
/// @notice The most recently accepted postRoot (starts at GENESIS_ROOT).
|
|
bytes32 public latestRoot;
|
|
|
|
/// @notice Number of validated epochs recorded.
|
|
uint256 public epochCount;
|
|
|
|
struct Epoch {
|
|
bytes32 prevRoot;
|
|
bytes32 postRoot;
|
|
bytes32 batchHash;
|
|
uint64 numTxns;
|
|
uint64 at; // block timestamp when the proof verified
|
|
}
|
|
|
|
/// @notice epochId (0-based) => validated epoch record.
|
|
mapping(uint256 => Epoch) public epochs;
|
|
|
|
/// @notice batchHash => 1-based epochId that recorded it (0 == not seen).
|
|
mapping(bytes32 => uint256) public batchToEpoch;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event EpochValidated(
|
|
uint256 indexed epochId,
|
|
bytes32 indexed prevRoot,
|
|
bytes32 indexed postRoot,
|
|
bytes32 batchHash,
|
|
uint256 numTxns,
|
|
address prover,
|
|
uint256 at
|
|
);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error InvalidProof();
|
|
error BadPublicValuesLength(uint256 got);
|
|
error WrongChain(uint256 inProof, uint256 onChain);
|
|
error RootDiscontinuity(bytes32 prevInProof, bytes32 expected);
|
|
error ZeroAddress();
|
|
error ZeroVKey();
|
|
|
|
/* ----------------------------- constructor ------------------------------ */
|
|
|
|
constructor(address sp1Verifier, bytes32 programVKey, bytes32 genesisRoot) {
|
|
if (sp1Verifier == address(0)) revert ZeroAddress();
|
|
if (programVKey == bytes32(0)) revert ZeroVKey();
|
|
SP1_VERIFIER = sp1Verifier;
|
|
PROGRAM_VKEY = programVKey;
|
|
GENESIS_ROOT = genesisRoot;
|
|
latestRoot = genesisRoot;
|
|
}
|
|
|
|
/* ------------------------------ submit epoch ---------------------------- */
|
|
|
|
/// @notice Verify a validity proof for the next rollup epoch and, on success,
|
|
/// record it and advance `latestRoot`. Permissionless.
|
|
/// @param publicValues abi.encode(chainId, prevRoot, postRoot, batchHash, numTxns), 160 bytes.
|
|
/// @param proof SP1 Groth16 proof bytes (leading selector 0x4388a21c).
|
|
/// @return epochId the 0-based id of the recorded epoch.
|
|
function submitEpoch(
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
) external returns (uint256 epochId) {
|
|
// 5 x 32 = 160 bytes for (uint256, bytes32, bytes32, bytes32, uint256).
|
|
if (publicValues.length != 160) 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,
|
|
bytes32 prevRoot,
|
|
bytes32 postRoot,
|
|
bytes32 batchHash,
|
|
uint256 numTxns
|
|
) = abi.decode(publicValues, (uint256, bytes32, bytes32, bytes32, uint256));
|
|
|
|
// 2) bind to this chain.
|
|
if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
|
|
|
|
// 3) canonical-chain continuity: the batch must start from the current tip.
|
|
if (prevRoot != latestRoot) revert RootDiscontinuity(prevRoot, latestRoot);
|
|
|
|
// 4) record the validated epoch and advance the tip.
|
|
epochId = epochCount;
|
|
epochs[epochId] = Epoch({
|
|
prevRoot: prevRoot,
|
|
postRoot: postRoot,
|
|
batchHash: batchHash,
|
|
numTxns: uint64(numTxns),
|
|
at: uint64(block.timestamp)
|
|
});
|
|
batchToEpoch[batchHash] = epochId + 1; // 1-based so 0 means "unseen"
|
|
latestRoot = postRoot;
|
|
epochCount = epochId + 1;
|
|
|
|
emit EpochValidated(
|
|
epochId, prevRoot, postRoot, batchHash, numTxns, msg.sender, block.timestamp
|
|
);
|
|
}
|
|
|
|
/* ----------------------------------- views ------------------------------ */
|
|
|
|
/// @notice Stateless verification: reverts if the proof is invalid for
|
|
/// PROGRAM_VKEY, otherwise returns the decoded (prevRoot, postRoot,
|
|
/// batchHash, numTxns). Records nothing and ignores chain continuity.
|
|
function verify(
|
|
bytes calldata publicValues,
|
|
bytes calldata proof
|
|
)
|
|
external
|
|
view
|
|
returns (bytes32 prevRoot, bytes32 postRoot, bytes32 batchHash, uint256 numTxns)
|
|
{
|
|
if (publicValues.length != 160) revert BadPublicValuesLength(publicValues.length);
|
|
ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
|
|
(, prevRoot, postRoot, batchHash, numTxns) =
|
|
abi.decode(publicValues, (uint256, bytes32, bytes32, bytes32, uint256));
|
|
}
|
|
|
|
/// @notice Convenience accessor for a recorded epoch.
|
|
function getEpoch(uint256 epochId)
|
|
external
|
|
view
|
|
returns (bytes32 prevRoot, bytes32 postRoot, bytes32 batchHash, uint64 numTxns, uint64 at)
|
|
{
|
|
Epoch memory e = epochs[epochId];
|
|
return (e.prevRoot, e.postRoot, e.batchHash, e.numTxns, e.at);
|
|
}
|
|
}
|