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.
225 lines
9.0 KiB
Solidity
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);
|
|
}
|
|
}
|