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.
276 lines
12 KiB
Solidity
276 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/**
|
|
* @dev Subset of the LIVE AereStorageProofVerifier 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 AereHistoryStateRootAnchor
|
|
* @notice EIP-2935 sibling of AereStateRootAnchor. It binds an AERE state root
|
|
* to the CANONICAL chain for blocks FAR OLDER than the 256-block
|
|
* BLOCKHASH window, closing the same trust gap AereStorageProofVerifier
|
|
* leaves open (that verifier proves a value against a GIVEN root but
|
|
* cannot show the root is canonical).
|
|
*
|
|
* WHERE THE BLOCK HASH COMES FROM (this is the whole point).
|
|
* The canonical block hash is read, at call time, from the EIP-2935
|
|
* history-storage system contract at
|
|
* 0x0000F90827F1C53a10cb7A02335B175320002935 via STATICCALL. That
|
|
* contract is a protocol-native ring buffer: on the AerePQC fork
|
|
* (futureEips milestone, same futureEipsTime activation as the native
|
|
* PQC precompiles) Besu's PraguePreExecutionProcessor writes the parent
|
|
* block hash into slot (number-1) % 8191 at the start of EVERY block,
|
|
* as a system call from address 0xffff...fffe. No externally-owned
|
|
* account, relayer, keeper, or Foundation key ever writes it. The value
|
|
* this anchor trusts is therefore produced by consensus itself, exactly
|
|
* like the BLOCKHASH opcode but with an 8191-block reach instead of 256.
|
|
*
|
|
* At AERE's 0.5s block time, 8191 blocks is roughly 68 minutes of
|
|
* reach, versus the ~128 seconds BLOCKHASH gives. Once a header is
|
|
* anchored here, the (blockNumber -> stateRoot) record is permanent and
|
|
* independent of the moving 8191-block ring buffer.
|
|
*
|
|
* The block hash is recovered strictly from the system contract, never
|
|
* from a caller argument, so an attacker cannot anchor a fabricated
|
|
* root: keccak256(rlpHeader) must equal the ring buffer'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 AereHistoryStateRootAnchor {
|
|
/// @notice Live AereStorageProofVerifier this anchor drives for proofs.
|
|
IAereStorageProofVerifier public immutable VERIFIER;
|
|
|
|
/// @notice EIP-2935 history-storage system contract (protocol-written ring buffer).
|
|
address public constant HISTORY_STORAGE_ADDRESS =
|
|
0x0000F90827F1C53a10cb7A02335B175320002935;
|
|
|
|
/// @notice EIP-2935 serve window: the ring buffer addresses the last 8191 blocks.
|
|
uint256 public constant HISTORY_SERVE_WINDOW = 8191;
|
|
|
|
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 OutOfHistoryWindow(uint256 blockNumber, uint256 currentBlock);
|
|
error HistoryContractMissing();
|
|
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: the EIP-2935 history-storage contract.
|
|
* Overridable so tests can inject a REAL AERE canonical hash without a
|
|
* live fork; the production path always STATICCALLs the system
|
|
* contract, whose storage is written by consensus (see contract note).
|
|
*
|
|
* Read ABI (per EIP-2935 runtime code): input is the 32-byte big-endian
|
|
* block number; output is the 32-byte canonical hash. The contract
|
|
* REVERTS for any block outside [number-8191, number-1], so a revert or
|
|
* a non-32-byte return is treated as out-of-window.
|
|
*/
|
|
function _historyBlockHash(uint256 blockNumber) internal view virtual returns (bytes32 h) {
|
|
if (HISTORY_STORAGE_ADDRESS.code.length == 0) revert HistoryContractMissing();
|
|
(bool ok, bytes memory ret) =
|
|
HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber));
|
|
if (!ok || ret.length != 32) revert OutOfHistoryWindow(blockNumber, block.number);
|
|
h = abi.decode(ret, (bytes32));
|
|
// A real block hash is never zero; zero means the slot was never written
|
|
// for this block number (out of the served window).
|
|
if (h == bytes32(0)) revert OutOfHistoryWindow(blockNumber, block.number);
|
|
}
|
|
|
|
/**
|
|
* @notice Anchor the state root of a historical canonical block using the
|
|
* EIP-2935 ring buffer. Works for blocks up to 8191 back, far beyond
|
|
* the 256-block BLOCKHASH horizon.
|
|
* @param blockNumber The block whose header is being anchored. MUST be
|
|
* within the last 8191 blocks (EIP-2935 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.
|
|
*/
|
|
function anchorHistoricalHeader(uint256 blockNumber, bytes calldata rlpHeader)
|
|
external
|
|
returns (bytes32 stateRoot)
|
|
{
|
|
bytes32 canonical = _historyBlockHash(blockNumber);
|
|
|
|
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.
|
|
* 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. Identical layout logic to
|
|
// AereStateRootAnchor: the first four header fields are fixed size, so the
|
|
// stateRoot sits at a fixed offset in the list payload.
|
|
// [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
|
|
// ----------------------------------------------------------------------
|
|
function _decodeStateRoot(bytes calldata rlpHeader) internal pure returns (bytes32 stateRoot) {
|
|
uint256 len = rlpHeader.length;
|
|
if (len < 121) revert MalformedHeader(1);
|
|
|
|
uint8 first = uint8(rlpHeader[0]);
|
|
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;
|
|
if (payloadStart + payloadLen != len) revert MalformedHeader(3);
|
|
if (payloadStart + 120 > len) revert MalformedHeader(4);
|
|
|
|
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 EIP-2935 ring buffer serves right now
|
|
/// for `blockNumber`. Reverts OutOfHistoryWindow if not served.
|
|
function historyBlockHash(uint256 blockNumber) external view returns (bytes32) {
|
|
return _historyBlockHash(blockNumber);
|
|
}
|
|
|
|
/// @notice True iff `blockNumber` is currently served by the EIP-2935 buffer.
|
|
function isWithinHistoryWindow(uint256 blockNumber) external view returns (bool) {
|
|
if (HISTORY_STORAGE_ADDRESS.code.length == 0) return false;
|
|
(bool ok, bytes memory ret) =
|
|
HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber));
|
|
if (!ok || ret.length != 32) return false;
|
|
return abi.decode(ret, (bytes32)) != bytes32(0);
|
|
}
|
|
|
|
/// @notice True iff the EIP-2935 system contract has code (fork active here).
|
|
function historyContractDeployed() external view returns (bool) {
|
|
return HISTORY_STORAGE_ADDRESS.code.length != 0;
|
|
}
|
|
|
|
/// @notice Preview the state root a header decodes to, WITHOUT canonicity
|
|
/// checks or recording. Pure helper for off-chain header assembly.
|
|
function previewStateRoot(bytes calldata rlpHeader) external pure returns (bytes32) {
|
|
return _decodeStateRoot(rlpHeader);
|
|
}
|
|
}
|