aere-contracts/contracts/anchor/AereStateRootAnchor.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

265 lines
12 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @dev Subset of the LIVE AereStorageProofVerifier
* (0xF9a1A183bEb3147D88dA5927301683fEbFb9362E) used by this anchor.
* `verify` is a stateless view that reverts on an invalid SP1 proof and
* otherwise returns the tuple committed in the 192-byte public values.
* `submitProofWithExpectedRoot` re-verifies and records the slot only if
* the proof's committed stateRoot equals `expectedStateRoot`.
*/
interface IAereStorageProofVerifier {
function verify(bytes calldata publicValues, bytes calldata proof)
external
view
returns (
uint256 blockNumber,
bytes32 stateRoot,
address account,
bytes32 slot,
bytes32 value
);
function submitProofWithExpectedRoot(
bytes calldata publicValues,
bytes calldata proof,
bytes32 expectedStateRoot
) external returns (bytes32 value);
}
/**
* @title AereStateRootAnchor
* @notice Binds an AERE state root to the CANONICAL chain, closing the trust
* gap left open by AereStorageProofVerifier. That verifier proves a
* storage value against a GIVEN state root but cannot, on its own, show
* the root is canonical for a block on chain 2800. This contract does
* exactly that: it recovers a canonical block hash from the EVM and
* checks a caller-supplied RLP header against it, then records the
* header's state root as an anchored, trust-minimized value.
*
* CANONICITY SOURCE ON CHAIN 2800 (probed on-chain, 2026-07-10).
* - BLOCKHASH opcode: LIVE. Serves the last 256 blocks. This is the
* ONLY canonicity source available on chain 2800 today.
* - EIP-2935 history-storage contract
* (0x0000F90827F1C53a10cb7A02335B175320002935 and the two earlier
* devnet addresses): NOT deployed (eth_getCode == 0x). So the
* extended ~8191-block window is NOT available here.
* Therefore this anchor uses BLOCKHASH and its window is 256 blocks.
* At AERE's 0.5s block time that is only ~128 seconds of history, so
* `anchorRecentHeader` MUST be called promptly after the target block
* is produced. Once anchored, the (blockNumber -> stateRoot) record is
* permanent and independent of the moving BLOCKHASH window.
*
* The block hash is recovered strictly from consensus (the BLOCKHASH
* opcode), never from a caller argument, so an attacker cannot anchor a
* fabricated root: keccak256(rlpHeader) must equal the opcode's answer.
*
* IMMUTABILITY: VERIFIER is fixed at deploy. No owner, no admin, no
* upgrade path. Anyone may anchor any in-window header and anyone may
* prove against an anchored root.
*/
contract AereStateRootAnchor {
/// @notice Live AereStorageProofVerifier this anchor drives for proofs.
IAereStorageProofVerifier public immutable VERIFIER;
/// @notice EVM BLOCKHASH window: the opcode addresses the last 256 blocks.
uint256 public constant BLOCKHASH_WINDOW = 256;
struct AnchoredRoot {
bool exists;
bytes32 stateRoot; // header field index 3
bytes32 blockHash; // canonical hash the header was checked against
uint64 blockNumber;
uint64 verifiedAt; // block.timestamp when anchored
}
/// @notice blockNumber => anchored canonical state root record.
mapping(uint256 => AnchoredRoot) private _anchored;
event HeaderAnchored(
uint256 indexed blockNumber,
bytes32 indexed blockHash,
bytes32 stateRoot,
uint256 verifiedAt
);
event ProvenAgainstAnchor(
uint256 indexed blockNumber,
bytes32 indexed stateRoot,
address indexed account,
bytes32 slot,
bytes32 value
);
error ZeroVerifier();
error OutOfWindow(uint256 blockNumber, uint256 currentBlock);
error HeaderHashMismatch(bytes32 got, bytes32 canonical);
error MalformedHeader(uint256 code);
error NotAnchored(uint256 blockNumber);
error RootNotCanonical(bytes32 proofRoot, bytes32 anchoredRoot);
constructor(address verifier) {
if (verifier == address(0)) revert ZeroVerifier();
VERIFIER = IAereStorageProofVerifier(verifier);
}
/**
* @dev Canonical block-hash source. Overridable so tests can inject a REAL
* AERE canonical hash for a block outside the live 256-block window
* (e.g. genesis) without weakening the production path, which always
* uses the BLOCKHASH opcode.
*/
function _blockHashOf(uint256 blockNumber) internal view virtual returns (bytes32) {
return blockhash(blockNumber);
}
/**
* @notice Anchor the state root of a recent canonical block.
* @param blockNumber The block whose header is being anchored. MUST be
* within the last 256 blocks (BLOCKHASH window) at call time.
* @param rlpHeader The exact RLP-encoded block header whose keccak256 is
* the canonical block hash. The state root is header field index 3.
* @return stateRoot The decoded, now-anchored canonical state root.
*
* Reverts OutOfWindow if BLOCKHASH cannot address the block (older than 256
* blocks, the current block, or a future block). Reverts HeaderHashMismatch
* if the supplied header does not hash to the canonical block hash.
*/
function anchorRecentHeader(uint256 blockNumber, bytes calldata rlpHeader)
external
returns (bytes32 stateRoot)
{
bytes32 canonical = _blockHashOf(blockNumber);
// BLOCKHASH returns 0 for any block it cannot address. A real block hash
// is never 0 (keccak of a nonempty header), so 0 == out of window.
if (canonical == bytes32(0)) revert OutOfWindow(blockNumber, block.number);
bytes32 got = keccak256(rlpHeader);
if (got != canonical) revert HeaderHashMismatch(got, canonical);
stateRoot = _decodeStateRoot(rlpHeader);
_anchored[blockNumber] = AnchoredRoot({
exists: true,
stateRoot: stateRoot,
blockHash: canonical,
blockNumber: uint64(blockNumber),
verifiedAt: uint64(block.timestamp)
});
emit HeaderAnchored(blockNumber, canonical, stateRoot, block.timestamp);
}
/**
* @notice Drive the live storage-proof verifier against an ANCHORED root.
* This is the trust-minimized end of the two-call pattern: rather
* than trusting a caller-supplied stateRoot, the proof's committed
* root is checked to equal the root anchored for the SAME block
* number (that block number is committed inside the proof, so the
* caller cannot mismatch them). Only then is the slot recorded in
* the verifier via {submitProofWithExpectedRoot}.
* @dev The verifier reverts the whole call on an invalid SP1 proof or a
* wrong chainId, so no unverified state can be recorded.
*/
function proveAgainstAnchor(bytes calldata publicValues, bytes calldata proof)
external
returns (bytes32 value)
{
(uint256 blockNumber, bytes32 stateRoot, address account, bytes32 slot, ) =
VERIFIER.verify(publicValues, proof);
AnchoredRoot memory a = _anchored[blockNumber];
if (!a.exists) revert NotAnchored(blockNumber);
if (a.stateRoot != stateRoot) revert RootNotCanonical(stateRoot, a.stateRoot);
value = VERIFIER.submitProofWithExpectedRoot(publicValues, proof, a.stateRoot);
emit ProvenAgainstAnchor(blockNumber, a.stateRoot, account, slot, value);
}
// ----------------------------------------------------------------------
// RLP header state-root decode.
//
// A block header is a long RLP list. Its first three fields are fixed size:
// [0] parentHash 32 bytes (prefix 0xa0)
// [1] ommersHash 32 bytes (prefix 0xa0)
// [2] beneficiary 20 bytes (prefix 0x94)
// [3] stateRoot 32 bytes (prefix 0xa0) <-- what we want
// so the state root sits at a fixed offset inside the list payload,
// regardless of any later fields (extraData, blob/beacon fields, or QBFT
// commit-seal handling), all of which come AFTER stateRoot.
// ----------------------------------------------------------------------
function _decodeStateRoot(bytes calldata rlpHeader) internal pure returns (bytes32 stateRoot) {
uint256 len = rlpHeader.length;
// Smallest possible header list prefix (1) + 4 leading fields.
if (len < 121) revert MalformedHeader(1);
uint8 first = uint8(rlpHeader[0]);
// Block headers are always long lists (payload > 55 bytes) -> 0xf8..0xff.
if (first < 0xf8) revert MalformedHeader(2);
uint256 numLenBytes = uint256(first) - 0xf7; // 1..8
uint256 payloadLen;
for (uint256 i = 0; i < numLenBytes; i++) {
payloadLen = (payloadLen << 8) | uint256(uint8(rlpHeader[1 + i]));
}
uint256 payloadStart = 1 + numLenBytes;
// The declared payload length must exactly cover the rest of the input.
if (payloadStart + payloadLen != len) revert MalformedHeader(3);
// Need room for the four leading fixed fields: 33 + 33 + 21 + 33 = 120.
if (payloadStart + 120 > len) revert MalformedHeader(4);
// Validate the fixed-size field prefixes so we know the layout is a
// standard Ethereum-style header and offset 88 truly is the stateRoot.
if (uint8(rlpHeader[payloadStart]) != 0xa0) revert MalformedHeader(5); // parentHash
if (uint8(rlpHeader[payloadStart + 33]) != 0xa0) revert MalformedHeader(6); // ommersHash
if (uint8(rlpHeader[payloadStart + 66]) != 0x94) revert MalformedHeader(7); // beneficiary
if (uint8(rlpHeader[payloadStart + 87]) != 0xa0) revert MalformedHeader(8); // stateRoot prefix
uint256 srContentOffset = payloadStart + 88;
assembly {
stateRoot := calldataload(add(rlpHeader.offset, srContentOffset))
}
}
// --------------------------- views -----------------------------------
function getAnchoredRoot(uint256 blockNumber)
external
view
returns (bool exists, bytes32 stateRoot, bytes32 blockHash, uint64 blockNum, uint64 verifiedAt)
{
AnchoredRoot memory a = _anchored[blockNumber];
return (a.exists, a.stateRoot, a.blockHash, a.blockNumber, a.verifiedAt);
}
/// @notice Anchored state root for `blockNumber`; reverts if not anchored.
function anchoredStateRoot(uint256 blockNumber) external view returns (bytes32) {
AnchoredRoot memory a = _anchored[blockNumber];
if (!a.exists) revert NotAnchored(blockNumber);
return a.stateRoot;
}
function isAnchored(uint256 blockNumber) external view returns (bool) {
return _anchored[blockNumber].exists;
}
/// @notice The canonical block hash the anchor would use right now (0 if the
/// block is outside the addressable BLOCKHASH window).
function canonicalBlockHash(uint256 blockNumber) external view returns (bytes32) {
return _blockHashOf(blockNumber);
}
/// @notice True iff `blockNumber` is currently anchorable (in the BLOCKHASH window).
function isWithinWindow(uint256 blockNumber) external view returns (bool) {
return _blockHashOf(blockNumber) != bytes32(0);
}
/// @notice Convenience preview of the state root a given header decodes to,
/// WITHOUT checking canonicity or recording anything. Pure helper
/// for off-chain callers assembling a header before anchoring.
function previewStateRoot(bytes calldata rlpHeader) external pure returns (bytes32) {
return _decodeStateRoot(rlpHeader);
}
}