TVM.md — Interpreter Divergences, Precompiles, Energy & Address Derivation¶
Living reference for internal/tvm and internal/state, and the designated
reference for the S6 differential arbiter. Tier-1 authority remains
docs/ARCHITECTURE.md §5.3–§5.6 (with the erratum recorded there for §5.5);
this file documents what is implemented and where every fact was grounded.
Grounding pin. All opcode numbers, energy costs, precompile transforms and
call semantics were transcribed from java-tron source at commit
f8ff7c76f45ab41d9bb76922329657506819701b (master, 2026-07-15) — classes
Op, OperationRegistry, OperationActions, EnergyCost, Program, VM,
Memory, PrecompiledContracts, TransactionUtil, WalletUtil and the
Stake 2.0 nativecontract processors. Nothing was written from memory
(CLAUDE.md §2). Final on-chain confirmation is S6's differential suite
(±0 metering, CREATE2 dual-path, precompile conformance).
1. EVM/TVM divergences implemented¶
| Surface | TVM behavior (as shipped by java-tron) |
|---|---|
GASPRICE (0x3a) |
0 for standard (version-0) contracts; the chain energy price only for EVM-compatible version-1 contracts. The sprint prose "GASPRICE → energyPrice" holds only in that mode — the version-0 zero is asserted by the conformance suite with the EVM expectation required to fail. |
BASEFEE (0x48) |
The injected chain energy price (getEnergyFee), read via chainparams — never a constant. |
DIFFICULTY (0x44), GASLIMIT (0x45) |
Constant 0. |
| Call depth | 64 (MAX_DEPTH), not the EVM's 1024. Conformance pins a 65-frame recursion counter. |
| Call energy forwarding | min(requested, available) — the 63/64 rule applies ONLY to version-1 contracts (Program.getCallEnergy). CREATE forwards the full remainder. |
| SSTORE pricing | Keyed on ROW EXISTENCE, not value: an absent row set non-zero costs SET 20000; every other transition — including rewriting a row that holds zero — costs 5000. A row written to zero EXISTS, so zero→non-zero within a transaction is RESET (5000) where original-EVM charges SET. Asserted with a failing EVM twin. |
SELFDESTRUCT (0xff) |
TIP-6780: full destruction only when the contract was created in the same transaction; otherwise holdings move and the account/code survive. Ineligible contracts (outstanding delegations or pending unfreezes) revert the frame. Base cost 5000 + 25000 for a dead inheritor. |
| Address words | ADDRESS/CALLER/ORIGIN push the 20-byte form; COINBASE and the CREATE/CREATE2 result words carry the 21-byte form with 0x41 visible at byte 11. ecrecover output likewise (see §2). |
| Memory | Word-aligned visible growth, reads extend like writes, quadratic cost 3·w + w²/512, hard 3 MB cap (memoryOverflow exception, not OOG). Under the active higher-limit adjustment MLOAD/MSTORE/MSTORE8 cost +1. |
| Internal-transaction nonce | One transaction-wide counter shared by CALLs, CREATEs, SELFDESTRUCT and the staking ops. CREATE addresses read it pre-increment — a preceding CALL therefore shifts subsequent CREATE addresses. Pinned by the conformance suite. |
TRON opcodes 0xd0–0xdf |
CALLTOKEN family, ISCONTRACT, Stake 1.0 trio (V2-superseded: FREEZE pushes 0), VOTEWITNESS/WITHDRAWREWARD, Stake 2.0 family — all against StateDB, mutating simulated freeze/delegation/TRC-10 state. Fixed costs from EnergyCost (V2 family 10000, VOTEWITNESS 30000+mem, WITHDRAWREWARD 20000). |
Simulation notes (memState): network-wide usage weighting (delegate/unfreeze "available minus usage" checks) collapses to zero usage; nothing expires inside one transaction (WITHDRAWEXPIREUNFREEZE answers 0); Stake 1.0 legacy records don't exist in fresh state. The S6 fork backend supplies real account data for all of these.
2. Precompiles — exact transforms¶
Dispatch is java-tron's: the full 32-byte call-target word is compared before any code lookup.
| Addr | v1 status | Transform / note |
|---|---|---|
| 0x01 ecrecover | implemented | 3000 energy. Output word left-pads the 21-byte address — 0x41 visible at byte 11 (EVM form asserted to fail). Failure returns empty, not a zero word. |
| 0x02 sha256 | implemented | 60 + 12/word. |
| 0x03 "ripemd160" | implemented | SHA256(SHA256(input)[0:20]) — see the erratum below. 600 + 120/word. |
| 0x04 identity | implemented | 15 + 3/word. |
| 0x05 modexp | implemented | EIP-198 with divisor 20; output sized to modLen. |
| 0x06–0x08 bn128 | explicit error | tvm: bn128 precompile 0xNN not supported in v1 — aborts the frame, self-explaining at S6 (user decision). |
| 0x09 BatchValidateSign (TIP-43) | implemented | (bytes32, bytes[], address[]) → 32-byte result, byte i = 1 on match; ≤16 signatures; malformed input yields the zero word. 1500/signature. |
| 0x0a ValidateMultiSign (TIP-60) | implemented | verified message = SHA256(account21 ‖ int32(permID) ‖ word32); recovered weights vs the account permission threshold (StateDB permission surface); ≤5 signatures. 1500/signature. |
| 0x100 p256verify, 0x20003/0x20009 EVM-compat, 0x1000001–4 shielded, 0x1000005–15 vote/resource queries | explicit error | Same distinguishable failure — never a silent success. |
ERRATUM — 0x03 (ARCHITECTURE §5.5)¶
The Tier-1 prose specifies leftPad32(RIPEMD160(SHA256(SHA256(input)))).
java-tron's shipped implementation — byte-identical from at least
GreatVoyage-v4.7.4 through the pinned master, and already
commented-out-RIPEMD in Odyssey-v3.0.0 — computes
RIPEMD-160 never runs and there is no padding. Per the Tier-3 rule (node
beats prose) the interpreter implements the node's transform;
docs/ARCHITECTURE.md carries the erratum entry, and the conformance suite
asserts that BOTH the Ethereum-ported expectation and the §5.5 prose formula
fail (TestPrecompile03_EthereumSemanticsMustFail). S6 confirms on-chain.
3. Basic energy model (layer 1 only)¶
E_basic is the deterministic per-opcode sum from the schedule transcribed
out of EnergyCost (tiers 0/2/3/5/8/10/20/1; SLOAD 50; SSTORE 20000/5000;
SHA3 30+6/word; EXP 10+10/exponent-byte; LOG 375+375/topic+8/byte; CALL 40
(+9000 value, +25000 dead account, 2300 stipend); CREATE 32000 + 200/byte
code deposit; memory quadratic). Costs are charged BEFORE execution
(java-tron VM.play order); any VM exception consumes the frame's whole
energy; REVERT keeps the unused remainder.
The Schedule struct versions the active fork set; the default enables
everything through Cancun plus the memory/energy/selfdestruct adjustments
(current mainnet/Nile), with Osaka and blob support off. Pre-TIP-6780
SELFDESTRUCT semantics are not modeled.
3b. Dynamic Energy Model (layer 2, S6)¶
The energy_factor (10⁴ precision, constant within each 6-hour maintenance
cycle) is a per-contract parameter read from the chain — via
GetContractInfo at the fork pin, frozen for the fork's lifetime — and never
simulated or hardcoded. The pure composition lives in
tvm.ApplyEnergyFactor; the layer activates only when Config.EnergyFactor
is set (fork mode): memState runs pay zero accounting cost.
Attribution is per contract (TIP-491 semantics): the VM tracks each contract's OWN consumed energy (children excluded, keyed by the context address — under DELEGATECALL the usage stays with the proxy) and reports
ExecResult.EnergyPenalty = Σ_c ⌊ usage_c × factor_c / 10000 ⌋
ExecResult.EnergyTotal = EnergyUsed + EnergyPenalty
EnergyUsed remains pure E_basic — the ±0 layer is never touched by the
DEM. For a single-contract call the per-contract sum reduces exactly to the
§5.4 formula (⌊b×(10000+f)/10000⌋ = b + ⌊b×f/10000⌋); across contracts with
different factors each is charged its own — node-faithful where a whole-call
single factor would over- or under-count.
3c. Differential validation results (S6 keyless; S7 FULL run with a funded key)¶
Full suite run 2026-07-19 against Nile with NILE_PRIVATE_KEY — every
battery green after the E4 correction:
- Metering battery ±0 (Metering.sol deployed at block 69329340): all
seven scenarios — SSTORE fresh-set (20203), reset (5224), clear (5203),
loop-64 (16623), nested call (1273), keccak-calldata (1091), precompile
0x02 (1445) — matched
energy_used − energy_penaltyexactly, with byte-identical return data. - Fork metering, real contract: USDT
balanceOfover the pinned fork: return data byte-identical,E_basic935 == 935 − 0 (S6 block 69328028, re-confirmed S7 at 69329318). - CREATE2 duality RESOLVED (Erratum E3): real deploys via BOTH syntactic
paths at block 69329318 —
new C{salt}AND inline-assemblycreate2observed on-chain at the 0x41 formula addresses (the 0xff formula matched neither). The interpreter's single-preimage model is confirmed;sonar create2 --mode evmis a cross-chain parity helper only. - Precompile 0x03 (Erratum E1 CONFIRMED ON-CHAIN): node output for
inputs of length 0/3/32/100 byte-identical to
SHA256(SHA256(input)[0:20])(block 69329380) — and distinct from the Ethereum and pre-erratum formulas. - E_total composition ±0 + EstimateEnergy semantics (Erratum E4):
ApplyEnergyFactor(E_basic_local, factor)==TriggerConstantContracttotal exactly (16623 == 16623, block 69329527). The node'sEstimateEnergy.energy_requiredis NOTE_totalbut a fee-limit binary-search bound (1-TRX bracket) — reproduced locally ±0 (24935 == 24935). See ARCHITECTURE Erratum E4. sun scriptplan vs receipts ±0 (S7 battery, §5.7): deploy + interaction broadcast undersolidified; receipts matched the plan exactly — energy 441901/20203, bandwidth 2453/280 — and the node'sContractAddressequaled the txid-derived planned address.- Storage-tag probe (Erratum E2): Nile remains latest-only, surfaced via
StoragePinned() == false.
4. Address derivation¶
CREATE (opcode): 0x41 ‖ keccak256(txid32 ‖ nonce8_BE)[12:] TransactionUtil.generateContractAddress
CREATE2 (opcode): 0x41 ‖ keccak256(0x41‖sender21‖salt32‖keccak256(initCode))[12:]
Root deploy: 0x41 ‖ keccak256(txid32 ‖ owner21)[12:] WalletUtil.generateContractAddress
Local txids: SHA-256(seed ‖ counter8_BE) deterministic, never wall-clock
The interpreter implements ONLY the 0x41 CREATE2 preimage; the byte 0xff
never appears in internal/tvm — the EVM-emulated derivation lives solely in
sun sonar create2 --mode evm (ARCHITECTURE §5.3). Vectors are cross-computed
with an independent pure-Python Keccak, and the conformance suite pins that
the native and 0xff derivations differ.
5. Testing and performance¶
Everything is hermetic on internal/state's journaled mem backend (nested
snapshots, absent-vs-written-zero storage, transient storage, resource
buckets). The conformance suite annotates the expected E_basic per fixture
— exact sums, a 1-energy drift fails. The compiled fixture is the scaffold
Counter.sol built once through the real S2 pipeline (solc-tron 0.8.23,
checksum-verified) and committed as hex testdata with pinned golden energies.
internal/tvm/testdata/bench_baseline.txt is the committed interpreter
performance baseline (regenerate with
go test -bench BenchmarkInterpreter -run XXX -benchtime=2s ./internal/tvm).