feat(vm): implement TIP-2935 serve historical block hashes from state#6686
Open
yanghang8612 wants to merge 3 commits intotronprotocol:developfrom
Open
feat(vm): implement TIP-2935 serve historical block hashes from state#6686yanghang8612 wants to merge 3 commits intotronprotocol:developfrom
yanghang8612 wants to merge 3 commits intotronprotocol:developfrom
Conversation
Port EIP-2935 to TRON: store recent block hashes in a system contract's storage so smart contracts can access up to 8191 blocks of history beyond the BLOCKHASH opcode's 256-block window. - New proposal ALLOW_TVM_PRAGUE(95), fork-gated by VERSION_4_8_2. - On activation, deploy the EIP-2935 runtime bytecode + ContractCapsule (version=0) + AccountCapsule(type=CONTRACT) at the canonical address 0x0000F90827F1C53a10cb7A02335B175320002935 via direct CodeStore / ContractStore / AccountStore writes. - On every block, before the transaction loop, write the parent block hash to StorageRowStore at slot (blockNum - 1) % 8191. The storage key layout replicates Storage.compose() for contractVersion=0 (addrHash[0:16] || slotKey[16:32]) so the written rows are readable via VM SLOAD. - BLOCKHASH opcode semantics unchanged; new storage is accessed by user contracts via STATICCALL to the deployed bytecode. Tests: round-trip equivalence (direct-write key = VM compose key), deploy idempotency, ring buffer modulo, end-to-end activation flow, and full VM repository read-back.
Proposal activation deploys the HistoryStorage contract via ProposalService, but a node started with committee.allowTvmPrague=1 would flip the flag on through the CommonParameter fallback without the contract ever being written to CodeStore/ContractStore/AccountStore. External STATICCALLs would then hit an empty account. - Add HistoryBlockHashUtil.deployIfMissing(Manager), called from Manager.init right after updateDynamicStoreByConfig so the bytecode is guaranteed present before any block is applied. - Guard write() against blockNum <= 0 so (0 - 1) % 8191 = -1 in Java can never corrupt a slot if the hook is ever reached for genesis. - Add integration tests covering the proposal-persisted, config-fallback, and already-deployed paths, plus block-1 writes genesis hash to slot 0 and the genesis no-op.
…hUtil Collapse the redundant getChainBaseManager() indirection — Manager already exposes getCodeStore/getContractStore/getAccountStore/getStorageRowStore/ getDynamicPropertiesStore pass-throughs.
alan-eth
reviewed
Apr 16, 2026
| * Called once from ProposalService when ALLOW_TVM_PRAGUE activates. | ||
| */ | ||
| public static void deploy(Manager manager) { | ||
| byte[] addr = HISTORY_STORAGE_ADDRESS; |
There was a problem hiding this comment.
The idempotent deploy() with per-store has() checks and the genesis guard in write() are really clean.
Minor: would it be helpful to add a log line in deploy() / deployIfMissing() for easier troubleshooting during upgrades?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements TIP-2935 (port of EIP-2935 "Serve Historical Block Hashes from State") for java-tron.
ALLOW_TVM_PRAGUE(ID95), fork-gated byVERSION_4_8_2.SmartContract(version = 0) +CONTRACT-type account at the 20-byte address0x0000F90827F1C53a10cb7A02335B175320002935(TRON:410000F90827F1C53a10cb7A02335B175320002935). The three writes go toCodeStore/ContractStore/AccountStorevia normal revoking-store puts — no synthetic transaction, no new actuator.StorageRowStoreat slot(block.num - 1) % 8191. The storage-key composition exactly replicatesStorage.compose()forcontractVersion=0(sha3(addr)[0:16] || slotKey[16:32]), so the direct-written rows are readable via a normal VMSLOADwhen user contractsSTATICCALLthe deployed bytecode.BLOCKHASHopcode semantics are unchanged (still the 256-block window).Why are these changes required?
The 256-block
BLOCKHASHwindow is too short for many workloads (rollups, stateless clients, multi-block fraud proofs, long-dated oracle signatures). EIP-2935 is the industry-standard solution on Ethereum (Prague, May 2025). Bringing it to TVM gives TRON DApps the same guarantees and — because we keep the same address and bytecode — lets cross-chain contracts hardcode0x0000F908…2935and work unchanged on both chains.This PR has been tested by:
HistoryBlockHashUtilTest(5 cases): storage-key round-trip equivalence withStorage.compose(), deploy populates Code/Contract/Account, deploy idempotency, slot-correctness, ring-buffer modulo wrap.HistoryBlockHashIntegrationTest(4 cases): activation flow, per-block write after activation, gated off before activation, and a full VM round-trip that reads the written hash back throughRepositoryImpl.getStorageValue— proving the read path composes the same key as our write path.ProposalUtilTest.validateCheck: newtestAllowTvmPragueProposalcovering pre-fork rejection, bad-value rejection, already-enabled rejection, and the valid path../gradlew :framework:compileJava :framework:compileTestJava— OK../gradlew lint— OK../gradlew :framework:test --tests "…HistoryBlockHash…" --tests "…ProposalUtilTest.validateCheck"— all pass.Follow up
410000F90827F1C53a10cb7A02335B175320002935has never held a contract / balance (deployment would collide otherwise).STATICCALLtoget(blockNum)from a Solidity contract on Nile.Extra details
Implementation notes on how this diverges from geth's Prague system-call pattern (direct-write instead of system call, proposal activation instead of genesis pre-alloc, one-block activation gap) are summarized in the TIP issue comment.