aere-contracts/contracts/zkverify/AereStorageProofVerifier.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

225 lines
9.0 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.
interface ISp1Verifier {
function verifyProof(
bytes32 programVKey,
bytes calldata publicValues,
bytes calldata proof
) external view;
}
/**
* @title AereStorageProofVerifier - zk storage-proof coprocessor primitive
* @notice On-chain anchor for SP1 zkVM proofs that, at a GIVEN state root R, an
* account A's storage slot S held value V. The proof is generated by the
* `aere-storage-proof-program` guest, which RLP-decodes and hash-chains
* the `eth_getProof` account-proof and storage-proof nodes of AERE's REAL
* Merkle-Patricia state trie (the same trie committed in each Besu block
* header's stateRoot). No Foundation-seeded side tree is involved: the
* guest walks account trie stateRoot -> storageRoot, then storage trie
* storageRoot -> value, panicking on any node whose keccak256 does not
* match its parent reference.
*
* PUBLIC-VALUES CONVENTION (exactly 192 bytes):
* abi.encode(
* uint256 chainId, // must equal this chain
* uint256 blockNumber, // METADATA only, see canonicity note
* bytes32 stateRoot, // the root the value was proven against
* address account, // A
* bytes32 slot, // S
* bytes32 value // V (left-padded 32-byte slot value)
* )
*
* --------------------------------------------------------------------
* HONEST SCOPE / TRUST BOUNDARY (read this).
* This contract proves "value V is the content of slot S of account A in
* the trie whose root is stateRoot". It does NOT and CANNOT, from the
* proof alone, establish that `stateRoot` is the CANONICAL state root of
* `blockNumber` on this chain. `blockNumber` is committed as metadata for
* the consumer's convenience only.
*
* To be trustless, the CONSUMER must independently establish that
* `stateRoot` is canonical. Two protocol-native coprocessor anchors do
* this without any keeper or oracle:
* - AereStateRootAnchor: recovers the canonical block hash from the
* BLOCKHASH opcode (~256 blocks) and checks the RLP header; or
* - AereHistoryStateRootAnchor: recovers it from the EIP-2935
* history-storage system contract
* (0x0000F90827F1C53a10cb7A02335B175320002935, ring buffer written
* by consensus each block on the AerePQC / futureEips fork), giving
* a ~8191-block reach far beyond BLOCKHASH.
* Both call {submitProofWithExpectedRoot} to force the committed
* stateRoot to equal a root established canonical on-chain.
* Use {submitProofWithExpectedRoot} to force the committed stateRoot to
* equal a root the caller already trusts, moving the canonicity check to
* the call site. This contract is the coprocessor storage-proof building
* block, NOT yet a full historical-state oracle.
* --------------------------------------------------------------------
*
* IMMUTABILITY: SP1_VERIFIER and PROGRAM_VKEY are fixed at deploy. There
* is no owner and no admin: anyone may submit a valid proof.
*/
contract AereStorageProofVerifier {
/// @notice SP1VerifierGateway (routes by proof selector to the versioned verifier).
address public immutable SP1_VERIFIER;
/// @notice Verification key binding the exact storage-proof guest ELF.
bytes32 public immutable PROGRAM_VKEY;
struct ProvenSlot {
bool exists;
bytes32 stateRoot;
bytes32 value;
uint64 blockNumber;
uint64 at; // block timestamp when recorded
}
/// @notice keccak256(blockNumber, account, slot) => proven slot record.
mapping(bytes32 => ProvenSlot) public proven;
event SlotProven(
address indexed account,
bytes32 indexed slot,
uint256 indexed blockNumber,
bytes32 stateRoot,
bytes32 value
);
error InvalidProof();
error WrongChain(uint256 inProof, uint256 onChain);
error BadPublicValuesLength(uint256 got);
error ZeroAddress();
error ZeroVKey();
error StateRootMismatch(bytes32 inProof, bytes32 expected);
constructor(address sp1Verifier, bytes32 programVKey) {
if (sp1Verifier == address(0)) revert ZeroAddress();
if (programVKey == bytes32(0)) revert ZeroVKey();
SP1_VERIFIER = sp1Verifier;
PROGRAM_VKEY = programVKey;
}
function _key(uint256 blockNumber, address account, bytes32 slot) internal pure returns (bytes32) {
return keccak256(abi.encode(blockNumber, account, slot));
}
/// @dev Verify the proof through the gateway (reverts on failure) and decode.
function _verifyAndDecode(bytes calldata publicValues, bytes calldata proof)
internal
view
returns (
uint256 chainId,
uint256 blockNumber,
bytes32 stateRoot,
address account,
bytes32 slot,
bytes32 value
)
{
// 6 x 32 = 192 bytes for (uint256, uint256, bytes32, address, bytes32, bytes32).
if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length);
try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
// verified - did not revert
} catch {
revert InvalidProof();
}
(chainId, blockNumber, stateRoot, account, slot, value) =
abi.decode(publicValues, (uint256, uint256, bytes32, address, bytes32, bytes32));
if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
}
/// @notice Verify an SP1 storage proof and record (blockNumber, account, slot) => value.
/// Reverts if the proof is invalid or the committed chainId is not this chain.
/// Does NOT check stateRoot canonicity: see the contract-level note; the
/// emitted stateRoot is the root the value was proven against.
function submitProof(bytes calldata publicValues, bytes calldata proof)
external
returns (bytes32 value)
{
(
,
uint256 blockNumber,
bytes32 stateRoot,
address account,
bytes32 slot,
bytes32 v
) = _verifyAndDecode(publicValues, proof);
proven[_key(blockNumber, account, slot)] = ProvenSlot({
exists: true,
stateRoot: stateRoot,
value: v,
blockNumber: uint64(blockNumber),
at: uint64(block.timestamp)
});
emit SlotProven(account, slot, blockNumber, stateRoot, v);
return v;
}
/// @notice Same as {submitProof} but reverts unless the proof's committed
/// stateRoot equals `expectedStateRoot`. The caller is responsible for
/// obtaining `expectedStateRoot` from a source it trusts to be canonical
/// (recent BLOCKHASH, EIP-2935 history, or a block-hash-anchor oracle),
/// which moves the canonicity trust to the call site.
function submitProofWithExpectedRoot(
bytes calldata publicValues,
bytes calldata proof,
bytes32 expectedStateRoot
) external returns (bytes32 value) {
(
,
uint256 blockNumber,
bytes32 stateRoot,
address account,
bytes32 slot,
bytes32 v
) = _verifyAndDecode(publicValues, proof);
if (stateRoot != expectedStateRoot) revert StateRootMismatch(stateRoot, expectedStateRoot);
proven[_key(blockNumber, account, slot)] = ProvenSlot({
exists: true,
stateRoot: stateRoot,
value: v,
blockNumber: uint64(blockNumber),
at: uint64(block.timestamp)
});
emit SlotProven(account, slot, blockNumber, stateRoot, v);
return v;
}
/// @notice Stateless verification: reverts if the proof is invalid or the
/// chainId is wrong, otherwise returns the decoded tuple. Records nothing.
function verify(bytes calldata publicValues, bytes calldata proof)
external
view
returns (
uint256 blockNumber,
bytes32 stateRoot,
address account,
bytes32 slot,
bytes32 value
)
{
(, blockNumber, stateRoot, account, slot, value) = _verifyAndDecode(publicValues, proof);
}
/// @notice Read a previously proven slot record.
function getProven(uint256 blockNumber, address account, bytes32 slot)
external
view
returns (bool exists, bytes32 stateRoot, bytes32 value, uint64 at)
{
ProvenSlot memory p = proven[_key(blockNumber, account, slot)];
return (p.exists, p.stateRoot, p.value, p.at);
}
}