feat(vm): implement TIP-2935 serve historical block hashes from state#6686
feat(vm): implement TIP-2935 serve historical block hashes from state#6686yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
Conversation
| * 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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 👍
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
What happens if deploy() throws an exception here?
There was a problem hiding this comment.
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:
-
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. -
Pre-existing EOA at the address (someone TRX-transfers to the canonical address before the proposal fires) → upgrade type to
Contractin place, preserving balance. -
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.
62fbbee to
27e76b3
Compare
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.
27e76b3 to
5806e22
Compare
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.