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

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);
}
}