tip: 2935
title: Serve historical block hashes from state
author: lily309@gmail.com
status: Draft
type: Standards Track
category: Core
created: 2025-01-20
Summary
As part of the Ethereum Pectra upgrade, EIP-2935:Serve historical block hashes from state is required to be implemented in TRON.
Abstract
Store last HISTORY_SERVE_WINDOW historical block hashes in the storage of a system contract as part of the block processing logic. Furthermore this TIP has no impact on BLOCKHASH resolution mechanism (and hence its range/costs etc).
Motivation
TVM implicitly assumes the client has the recent block (hashes) at hand. This assumption is not future-proof given the prospect of stateless clients. Including the block hashes in the state will allow bundling these hashes in the witness provided to a stateless client.
Extending the range of blocks which BLOCKHASH can serve (BLOCKHASH_SERVE_WINDOW) would have been a semantics change. Extending it via this contract storage instead allows a soft transition. Rollups can benefit from the longer history window through directly querying this contract.
A side benefit of this approach could be that it allows building/validating proofs related to last HISTORY_SERVE_WINDOW ancestors directly against the current state.
Specification
| Parameter |
Value |
BLOCKHASH_SERVE_WINDOW |
256 |
HISTORY_SERVE_WINDOW |
8191 |
SYSTEM_ADDRESS |
0xfffffffffffffffffffffffffffffffffffffffe (Base58Check: TZJozAg1ruapycCicgz31GxvYJ1Fmd2Pm8) |
HISTORY_STORAGE_ADDRESS |
0x0000F90827F1C53a10cb7A02335B175320002935 (Base58Check: T9yEAebSu94c1ndzJa1rpPtrjXBXp2FCMV) |
This TIP specifies for storing last HISTORY_SERVE_WINDOW block hashes in a ring buffer storage of HISTORY_SERVE_WINDOW length. Note that HISTORY_SERVE_WINDOW > BLOCKHASH_SERVE_WINDOW (which remains unchanged).
HISTORY_STORAGE_ADDRESS matches the canonical EIP-2935 address used by Ethereum mainnet, so cross-chain Solidity contracts hardcoding this constant work on TRON unchanged. The Base58Check forms are provided for use with TronWeb / TronGrid / TronLink and TRON block explorers.
Block processing
At the start of processing any block where this TIP is active (ie. before processing any transactions), call to HISTORY_STORAGE_ADDRESS as SYSTEM_ADDRESS with the 32-byte input of block.parent.hash, a gas limit of 30_000_000, and 0 value. This will trigger the set() routine of the history contract. This is a system operation and therefore:
- the call must execute to completion
- the call does not count against the block's gas limit
- if no code exists at
HISTORY_STORAGE_ADDRESS, the call must fail silently
Note: Alternatively clients can choose to directly write to the storage of the contract but TVM calling the contract remains preferred. Refer to the rationale for more info.
Note that, it will take HISTORY_SERVE_WINDOW blocks after the TIP's activation to completely fill up the ring buffer. The contract will only contain the parent hash of the fork block and no hashes prior to that.
TVM Changes
The BLOCKHASH opcode semantics remains the same as before.
Block hash history contract
The history contract has two operations: get and set. The set operation is invoked only when the caller is equal to the SYSTEM_ADDRESS.
get
It is used from the TVM for looking up block hashes.
- Callers provide the block number they are querying in a big-endian encoding.
- If calldata is not 32 bytes, revert.
- For any request outside the range of [block.number-
HISTORY_SERVE_WINDOW, block.number-1], revert.
set
- Caller provides
block.parent.hash as calldata to the contract.
- Set the storage value at
(block.number-1) % HISTORY_SERVE_WINDOW to be calldata[0:32].
Bytecode
Exact TVM assembly that can be used for the history contract:
// https://github.com/lightclient/sys-asm/blob/f1c13e285b6aeef2b19793995e00861bf0f32c9a/src/execution_hash/main.eas
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x46
jumpi
push1 0x20
calldatasize
sub
push1 0x42
jumpi
push0
calldataload
push1 0x01
number
sub
dup2
gt
push1 0x42
jumpi
push2 0x1fff
dup2
number
sub
gt
push1 0x42
jumpi
push2 0x1fff
swap1
mod
sload
push0
mstore
push1 0x20
push0
return
jumpdest
push0
push0
revert
jumpdest
push0
calldataload
push2 0x1fff
push1 0x01
number
sub
mod
sstore
stop
Deployment
The contract will be deployed as a system contract at the predefined address HISTORY_STORAGE_ADDRESS during the network upgrade that activates this TIP. Because a TRON contract-creation transaction includes owner_address, expiration, and ref_block_num, the pre-signed deployment transaction pattern used on Ethereum cannot produce a deterministic address on TRON; deployment is therefore handled directly by the protocol at activation time rather than through a regular contract creation transaction.
Activation is governed by committee proposal ALLOW_TVM_PRAGUE (proposal ID 95) with value 1, gated by hard-fork version VERSION_4_8_2. The proposal is one-shot and cannot be toggled off.
Gas costs
The system update at the beginning of the block, i.e. process_block_hash_history (or via system call to the contract with SYSTEM_ADDRESS caller), will not warm the HISTORY_STORAGE_ADDRESS account or its storage slots. As such the first call to the contract will pay for warming up the account and storage slots it accesses. To clarify further any contract call to the HISTORY_STORAGE_ADDRESS will follow normal TVM execution semantics.
Since BLOCKHASH semantics doesn't change, this TIP has no impact on BLOCKHASH mechanism and costs.
Bootstrap period
It takes up to HISTORY_SERVE_WINDOW blocks after activation for the ring buffer to fully populate. During this period, given activation block A and current block N:
get(K) where K >= N or K < N - HISTORY_SERVE_WINDOW: reverts (normal out-of-range behavior).
get(K) where A <= K <= N - 1: returns the stored hash.
get(K) where N - HISTORY_SERVE_WINDOW <= K < A (slot still unwritten): returns bytes32(0), not a revert.
Developers should therefore treat a bytes32(0) return as "unavailable" rather than a valid hash, and continue to use the BLOCKHASH opcode for the most recent 256 blocks where it is the cheapest and always-available option. Applications with strict correctness requirements (rollups, bridges) should gate their logic on block.number >= A + HISTORY_SERVE_WINDOW, or fall back to an off-chain oracle during the bootstrap window.
Rationale
Very similar ideas were proposed before. This TIP is a simplification, removing three sources of needless complexity:
- Having a tree-like structure with multiple layers as opposed to a single list
- Writing the TIP in TVM code
- Serial unbounded storage of hashes for a deep access to the history
However after weighing pros and cons, we decided to go with just a limited ring buffer to only serve the requisite HISTORY_SERVE_WINDOW.
Second concern was how to best transition the BLOCKHASH resolution logic post fork by:
- Either waiting for
HISTORY_SERVE_WINDOW blocks for the entire relevant history to persist
- Storing of all last
HISTORY_SERVE_WINDOW block hashes on the fork block.
We choose to go with the former. It simplifies the logic greatly. It will take roughly 7 hours at TRON's 3-second block interval to bootstrap the contract. Given that this is a new way of accessing history and no contract depends on it, it is deemed a favorable tradeoff.
Inserting the parent block hash
Clients have generally two options for inserting the parent block hash into state:
- Performing a system call to
HISTORY_STORAGE_ADDRESS and letting that handle the storing in state.
- Avoid TVM processing and directly write to the state trie.
The latter option is as follows:
def process_block_hash_history(block: Block, state: State):
if block.timestamp >= FORK_TIMESTAMP: // FORK_TIMESTAMP should be defined outside of the TIP
state.insert_slot(HISTORY_STORAGE_ADDRESS, (block.number-1) % HISTORY_SERVE_WINDOW , block.parent.hash)
The TRON reference implementation takes option 2 (direct state write). The effect on storage is byte-identical to what the system-call path produces, but avoids introducing synthetic-transaction / system-caller infrastructure solely for this one contract — TRON has no other Prague system contracts (no beacon chain, no withdrawal/consolidation queues) that would share such infrastructure.
Size of ring buffers
The ring buffer data structure is sized to hold 8191 hashes. In other system contracts a prime ring buffer size is chosen because using a prime as the modulus ensures that no value is overwritten until the entire ring buffer has been saturated, and thereafter each value will be updated once per iteration, regardless of whether some slots are missing or the slot time changes. However, in this TIP the block number is the value in the modulo operation and it only ever increases by 1 each iteration, which means we can be confident that the ring buffer will always remain saturated.
For consistency with other system contracts, we have decided to retain the buffer size of 8191. At TRON's 3-second block interval, 8191 hashes provides about 7 hours of coverage. This also gives users plenty of time to make a transaction with a verification against a specific hash and get the transaction included on-chain.
Backwards Compatibility
This TIP introduces backwards incompatible changes to the block validation rule set. But neither of these changes break anything related to current user activity and experience.
Test Cases
Reference implementation and tests: tronprotocol/java-tron#6686.
Security Considerations
Having contracts (system or otherwise) with hot update paths (branches) poses a risk of "branch" poisoning attacks where attacker could sprinkle trivial amounts of trx around these hot paths (branches). But it has been deemed that cost of attack would escalate significantly to cause any meaningful slow down of state root updates.
Copyright
Copyright and related rights waived via CC0.
Summary
As part of the Ethereum Pectra upgrade, EIP-2935:Serve historical block hashes from state is required to be implemented in TRON.
Abstract
Store last
HISTORY_SERVE_WINDOWhistorical block hashes in the storage of a system contract as part of the block processing logic. Furthermore this TIP has no impact onBLOCKHASHresolution mechanism (and hence its range/costs etc).Motivation
TVM implicitly assumes the client has the recent block (hashes) at hand. This assumption is not future-proof given the prospect of stateless clients. Including the block hashes in the state will allow bundling these hashes in the witness provided to a stateless client.
Extending the range of blocks which
BLOCKHASHcan serve (BLOCKHASH_SERVE_WINDOW) would have been a semantics change. Extending it via this contract storage instead allows a soft transition. Rollups can benefit from the longer history window through directly querying this contract.A side benefit of this approach could be that it allows building/validating proofs related to last
HISTORY_SERVE_WINDOWancestors directly against the current state.Specification
BLOCKHASH_SERVE_WINDOW256HISTORY_SERVE_WINDOW8191SYSTEM_ADDRESS0xfffffffffffffffffffffffffffffffffffffffe(Base58Check:TZJozAg1ruapycCicgz31GxvYJ1Fmd2Pm8)HISTORY_STORAGE_ADDRESS0x0000F90827F1C53a10cb7A02335B175320002935(Base58Check:T9yEAebSu94c1ndzJa1rpPtrjXBXp2FCMV)This TIP specifies for storing last
HISTORY_SERVE_WINDOWblock hashes in a ring buffer storage ofHISTORY_SERVE_WINDOWlength. Note thatHISTORY_SERVE_WINDOW>BLOCKHASH_SERVE_WINDOW(which remains unchanged).HISTORY_STORAGE_ADDRESSmatches the canonical EIP-2935 address used by Ethereum mainnet, so cross-chain Solidity contracts hardcoding this constant work on TRON unchanged. The Base58Check forms are provided for use with TronWeb / TronGrid / TronLink and TRON block explorers.Block processing
At the start of processing any block where this TIP is active (ie. before processing any transactions), call to
HISTORY_STORAGE_ADDRESSasSYSTEM_ADDRESSwith the 32-byte input ofblock.parent.hash, a gas limit of30_000_000, and0value. This will trigger theset()routine of the history contract. This is a system operation and therefore:HISTORY_STORAGE_ADDRESS, the call must fail silentlyNote: Alternatively clients can choose to directly write to the storage of the contract but TVM calling the contract remains preferred. Refer to the rationale for more info.
Note that, it will take
HISTORY_SERVE_WINDOWblocks after the TIP's activation to completely fill up the ring buffer. The contract will only contain the parent hash of the fork block and no hashes prior to that.TVM Changes
The
BLOCKHASHopcode semantics remains the same as before.Block hash history contract
The history contract has two operations:
getandset. Thesetoperation is invoked only when thecalleris equal to theSYSTEM_ADDRESS.getIt is used from the TVM for looking up block hashes.
HISTORY_SERVE_WINDOW, block.number-1], revert.setblock.parent.hashas calldata to the contract.(block.number-1) % HISTORY_SERVE_WINDOWto becalldata[0:32].Bytecode
Exact TVM assembly that can be used for the history contract:
Deployment
The contract will be deployed as a system contract at the predefined address
HISTORY_STORAGE_ADDRESSduring the network upgrade that activates this TIP. Because a TRON contract-creation transaction includesowner_address,expiration, andref_block_num, the pre-signed deployment transaction pattern used on Ethereum cannot produce a deterministic address on TRON; deployment is therefore handled directly by the protocol at activation time rather than through a regular contract creation transaction.Activation is governed by committee proposal
ALLOW_TVM_PRAGUE(proposal ID95) with value1, gated by hard-fork versionVERSION_4_8_2. The proposal is one-shot and cannot be toggled off.Gas costs
The system update at the beginning of the block, i.e.
process_block_hash_history(or via system call to the contract withSYSTEM_ADDRESScaller), will not warm theHISTORY_STORAGE_ADDRESSaccount or its storage slots. As such the first call to the contract will pay for warming up the account and storage slots it accesses. To clarify further any contract call to theHISTORY_STORAGE_ADDRESSwill follow normal TVM execution semantics.Since
BLOCKHASHsemantics doesn't change, this TIP has no impact onBLOCKHASHmechanism and costs.Bootstrap period
It takes up to
HISTORY_SERVE_WINDOWblocks after activation for the ring buffer to fully populate. During this period, given activation blockAand current blockN:get(K)whereK >= NorK < N - HISTORY_SERVE_WINDOW: reverts (normal out-of-range behavior).get(K)whereA <= K <= N - 1: returns the stored hash.get(K)whereN - HISTORY_SERVE_WINDOW <= K < A(slot still unwritten): returnsbytes32(0), not a revert.Developers should therefore treat a
bytes32(0)return as "unavailable" rather than a valid hash, and continue to use theBLOCKHASHopcode for the most recent 256 blocks where it is the cheapest and always-available option. Applications with strict correctness requirements (rollups, bridges) should gate their logic onblock.number >= A + HISTORY_SERVE_WINDOW, or fall back to an off-chain oracle during the bootstrap window.Rationale
Very similar ideas were proposed before. This TIP is a simplification, removing three sources of needless complexity:
However after weighing pros and cons, we decided to go with just a limited ring buffer to only serve the requisite
HISTORY_SERVE_WINDOW.Second concern was how to best transition the BLOCKHASH resolution logic post fork by:
HISTORY_SERVE_WINDOWblocks for the entire relevant history to persistHISTORY_SERVE_WINDOWblock hashes on the fork block.We choose to go with the former. It simplifies the logic greatly. It will take roughly 7 hours at TRON's 3-second block interval to bootstrap the contract. Given that this is a new way of accessing history and no contract depends on it, it is deemed a favorable tradeoff.
Inserting the parent block hash
Clients have generally two options for inserting the parent block hash into state:
HISTORY_STORAGE_ADDRESSand letting that handle the storing in state.The latter option is as follows:
The TRON reference implementation takes option 2 (direct state write). The effect on storage is byte-identical to what the system-call path produces, but avoids introducing synthetic-transaction / system-caller infrastructure solely for this one contract — TRON has no other Prague system contracts (no beacon chain, no withdrawal/consolidation queues) that would share such infrastructure.
Size of ring buffers
The ring buffer data structure is sized to hold 8191 hashes. In other system contracts a prime ring buffer size is chosen because using a prime as the modulus ensures that no value is overwritten until the entire ring buffer has been saturated, and thereafter each value will be updated once per iteration, regardless of whether some slots are missing or the slot time changes. However, in this TIP the block number is the value in the modulo operation and it only ever increases by 1 each iteration, which means we can be confident that the ring buffer will always remain saturated.
For consistency with other system contracts, we have decided to retain the buffer size of 8191. At TRON's 3-second block interval, 8191 hashes provides about 7 hours of coverage. This also gives users plenty of time to make a transaction with a verification against a specific hash and get the transaction included on-chain.
Backwards Compatibility
This TIP introduces backwards incompatible changes to the block validation rule set. But neither of these changes break anything related to current user activity and experience.
Test Cases
Reference implementation and tests:
tronprotocol/java-tron#6686.Security Considerations
Having contracts (system or otherwise) with hot update paths (branches) poses a risk of "branch" poisoning attacks where attacker could sprinkle trivial amounts of trx around these hot paths (branches). But it has been deemed that cost of attack would escalate significantly to cause any meaningful slow down of state root updates.
Copyright
Copyright and related rights waived via CC0.