Skip to content

feat(vm): implement TIP-2935 serve historical block hashes from state#6686

Open
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
yanghang8612:feat/tip-2935-historical-blockhash
Open

feat(vm): implement TIP-2935 serve historical block hashes from state#6686
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
yanghang8612:feat/tip-2935-historical-blockhash

Conversation

@yanghang8612
Copy link
Copy Markdown
Collaborator

@yanghang8612 yanghang8612 commented Apr 16, 2026

What does this PR do?

Implements TIP-2935 (port of EIP-2935 "Serve Historical Block Hashes from State") for java-tron.

  • Adds proposal ALLOW_TVM_PRAGUE (ID 95), fork-gated by VERSION_4_8_2.
  • On activation, deploys the canonical EIP-2935 runtime bytecode + a minimal SmartContract (version = 0) + CONTRACT-type account at the 20-byte address 0x0000F90827F1C53a10cb7A02335B175320002935 (TRON: 410000F90827F1C53a10cb7A02335B175320002935). The three writes go to CodeStore / ContractStore / AccountStore via normal revoking-store puts — no synthetic transaction, no new actuator.
  • On every block, before the transaction loop, writes the parent block hash to StorageRowStore at slot (block.num - 1) % 8191. The storage-key composition exactly replicates Storage.compose() for contractVersion=0 (sha3(addr)[0:16] || slotKey[16:32]), so the direct-written rows are readable via a normal VM SLOAD when user contracts STATICCALL the deployed bytecode.
  • BLOCKHASH opcode semantics are unchanged (still the 256-block window).

Why are these changes required?

The 256-block BLOCKHASH window 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 hardcode 0x0000F908…2935 and work unchanged on both chains.

This PR has been tested by:

  • Unit Tests
    • HistoryBlockHashUtilTest (5 cases): storage-key round-trip equivalence with Storage.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 through RepositoryImpl.getStorageValue — proving the read path composes the same key as our write path.
    • ProposalUtilTest.validateCheck: new testAllowTvmPragueProposal covering pre-fork rejection, bad-value rejection, already-enabled rejection, and the valid path.
  • Manual Testing
    • ./gradlew :framework:compileJava :framework:compileTestJava — OK.
    • ./gradlew lint — OK.
    • ./gradlew :framework:test --tests "…HistoryBlockHash…" --tests "…ProposalUtilTest.validateCheck" — all pass.

Follow up

  • Verify on mainnet that the address 410000F90827F1C53a10cb7A02335B175320002935 has never held a contract / balance (deployment would collide otherwise).
  • Testnet DApp check: measure energy cost of a STATICCALL to get(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.

alan-eth

This comment was marked as duplicate.

* Called once from ProposalService when ALLOW_TVM_PRAGUE activates.
*/
public static void deploy(Manager manager) {
byte[] addr = HISTORY_STORAGE_ADDRESS;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

@yanghang8612 yanghang8612 Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the nudge — good point, one info log is cheap and pays off the first time someone has to trace when TIP-2935 actually landed on a given node.

Just pushed it in c4dde34: a single logger.info inside the CodeStore write branch of deploy(), guarded by !has(addr) so it only fires the instant fresh bytecode first lands. Steady-state restarts stay silent.

Format:

TIP-2935: wrote HistoryStorage bytecode at <addr-hex>

Topic is DB, matching the convention in org.tron.core.db.backup.* etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this in — the !has(addr) guard plus @Slf4j(topic = "DB") is spot-on, and having steady-state restarts stay silent is exactly the right trade-off. 🎯

Nice bonus I noticed while reading the commit: you also took the chance to rename the constant to BlockHashHistory (clearer domain naming; the log string carries the new name too) and add validateExistingOrThrow() so deploy() now validates-first-then-writes. That pattern cleanly handles both the "canonical address already occupied by foreign state" case and the "crashed between the three store writes" partial-install case — a solid defensive improvement that goes well beyond the original log-line ask. 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — and good eye on the rename. BlockHashHistory reads better everywhere it surfaces (proposal name, log topic, store comments), and validateExistingOrThrow() is the right shape for the three-store triplet: a mid-deploy() crash now surfaces as a loud mismatch on restart rather than a silently partial install across CodeStore / ContractStore / AccountStore. Appreciated the careful read across the whole commit, not just the spot I asked about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anytime! The simplified deployIfMissing (gate on the flag, delegate to deploy() so the validate-first path runs regardless of which store crashed mid-write) is a particularly tidy way to fold partial-install recovery into the normal deploy path — no separate code path to maintain. 👍 Looking forward to seeing TIP-2935 land.

case ALLOW_TVM_PRAGUE: {
manager.getDynamicPropertiesStore().saveAllowTvmPrague(entry.getValue());
if (entry.getValue() == 1) {
HistoryBlockHashUtil.deploy(manager);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if deploy() throws an exception here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — pushed three commits in response: 344f418 (initial defensive wrap), f60c100 (simplification once we knew this runs at most once), and 34aa2fb after rethinking the exception strategy.

Final shape: ProposalService no longer wraps deploy() at all:

case ALLOW_TVM_PRAGUE: {
  manager.getDynamicPropertiesStore().saveAllowTvmPrague(entry.getValue());
  if (entry.getValue() == 1) {
    HistoryBlockHashUtil.deploy(manager);
  }
  break;
}

deploy() separates the failure shapes by control flow, not exception type:

  1. Foreign code/contract metadata at the canonical address (deterministic — same pre-state on every node, requires a SHA-3 pre-image to ever fire) → logger.warn(...) + return. Flag commits, contract install skipped, STATICCALLs return empty at the user level on every node ⇒ consensus intact.

  2. Pre-existing EOA at the address (someone TRX-transfers to the canonical address before the proposal fires) → upgrade type to Contract in place, preserving balance.

  3. Anything else (RocksDB / IO / OOM / …) → propagate naturally. The maintenance block aborts; the shared revoking session rolls back saveAllowTvmPrague(1) together with any partial store writes. Bad node fails loudly — correct outcome for non-deterministic failure modes.

The intermediate rev had catch(Throwable) which conflated (1) and (3); briefly considered narrowing to catch(IllegalStateException) but it's also raised by SnapshotManager and TxCacheDB, so a narrow catch couldn't cleanly separate the cases. Putting the deterministic skip inside deploy() as if/return separates the cases by position in source rather than by exception type — more robust.

@bladehan1 bladehan1 assigned bladehan1 and unassigned bladehan1 Apr 28, 2026
@yanghang8612 yanghang8612 force-pushed the feat/tip-2935-historical-blockhash branch 2 times, most recently from 62fbbee to 27e76b3 Compare April 29, 2026 04:02
Activates via ALLOW_TVM_PRAGUE proposal: deploy() installs the
BlockHashHistory bytecode + metadata at the canonical address;
write() propagates parent hashes to a 8191-slot ring buffer.
ProposalUtilTest: validateCheck for ALLOW_TVM_PRAGUE pre/post-fork
and re-activation. HistoryBlockHashUtilTest + IntegrationTest:
deploy/write paths, install marker, foreign-state collision.
@yanghang8612 yanghang8612 force-pushed the feat/tip-2935-historical-blockhash branch from 27e76b3 to 5806e22 Compare April 29, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:vm VM, smart contract

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants