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

293 lines
13 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
// Reuse the LIVE verifier interface and the AnchoredRoot semantics from the
// original anchor. Importing the V1 file also brings in IAereStorageProofVerifier
// so the two contracts share one interface definition and one ABI convention.
import "./AereHistoryStateRootAnchor.sol";
/**
* @title AereHistoryStateRootAnchorV2
* @notice Corrected redeploy of AereHistoryStateRootAnchor. The anchoring path
* (EIP-2935 ring-buffer block-hash recovery, keccak match, RLP state
* root decode, permanent record) is byte-for-byte identical to V1. The
* only change is in {proveAgainstAnchor}.
*
* WHAT V1 DID WRONG (audit FINDING 4, low).
* V1 called VERIFIER.verify() to recover the committed blockNumber and
* stateRoot, then called VERIFIER.submitProofWithExpectedRoot(), which
* re-runs the very same SP1 Groth16 pairing check on the very same
* publicValues and proof. The expensive proof verification therefore ran
* TWICE per call for no added safety, since submitProofWithExpectedRoot
* already reverts on an invalid proof and already enforces that the
* proof's committed root equals the expected root passed to it.
*
* WHAT V2 DOES.
* V2 decodes the block number, account, and slot LOCALLY from the fixed
* 192-byte public-values layout (no external call), looks up the root
* anchored for that block number, and calls ONLY
* submitProofWithExpectedRoot(publicValues, proof, anchoredRoot). The
* single verification inside submit still reverts on an invalid proof
* and still binds the committed root to the anchored root, so behaviour
* is unchanged while the redundant pairing check is removed.
*
* PUBLIC-VALUES LAYOUT (abi.encode, 6 words, 192 bytes):
* [0:32] chainId
* [32:64] blockNumber
* [64:96] stateRoot
* [96:128] account
* [128:160] slot
* [160:192] value
*
* 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 AereHistoryStateRootAnchorV2 {
/// @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;
/// @notice Fixed byte length of the SP1 public values this anchor accepts.
uint256 public constant PUBLIC_VALUES_LEN = 192;
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 BadPublicValuesLength(uint256 got);
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 the V1 contract
* note for the full rationale).
*
* 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. Logic is identical to V1.
* @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,
* running the SP1 proof verification EXACTLY ONCE.
* @dev The block number, account, and slot are decoded locally from the
* fixed 192-byte public-values layout, so no call to VERIFIER.verify
* is made. The anchored root for that block number is passed as the
* expected root to submitProofWithExpectedRoot, which performs the
* single verification, reverts on an invalid proof, and reverts if
* the proof's committed root does not equal the anchored root. This
* is the same guarantee V1 provided, without the redundant second
* verification.
*/
function proveAgainstAnchor(bytes calldata publicValues, bytes calldata proof)
external
returns (bytes32 value)
{
if (publicValues.length != PUBLIC_VALUES_LEN) {
revert BadPublicValuesLength(publicValues.length);
}
(uint256 blockNumber, address account, bytes32 slot) =
_decodePublicValues(publicValues);
AnchoredRoot memory a = _anchored[blockNumber];
if (!a.exists) revert NotAnchored(blockNumber);
// Single verification. submitProofWithExpectedRoot reverts on an invalid
// SP1 proof and reverts unless the proof's committed root == a.stateRoot,
// so no unverified or off-root state can be recorded.
value = VERIFIER.submitProofWithExpectedRoot(publicValues, proof, a.stateRoot);
emit ProvenAgainstAnchor(blockNumber, a.stateRoot, account, slot, value);
}
/**
* @dev Decode blockNumber (word 1), account (word 3), and slot (word 4) from
* the fixed 192-byte public-values layout. chainId, stateRoot, and value
* are not needed here: the committed stateRoot is bound to the anchored
* root inside submitProofWithExpectedRoot, and chainId is enforced by the
* verifier itself.
*/
function _decodePublicValues(bytes calldata publicValues)
internal
pure
returns (uint256 blockNumber, address account, bytes32 slot)
{
uint256 accountWord;
assembly {
blockNumber := calldataload(add(publicValues.offset, 32))
accountWord := calldataload(add(publicValues.offset, 96))
slot := calldataload(add(publicValues.offset, 128))
}
// Low 20 bytes of word 3, matching abi.decode of an address.
account = address(uint160(accountWord));
}
// ----------------------------------------------------------------------
// RLP header state-root decode. Identical layout logic to
// AereHistoryStateRootAnchor: 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);
}
}