A developer tool for making an alternative TRON node implementation byte-exact with the canonical java-tron client (GreatVoyage-v4.8.1.1). It turns "reverse-engineer java's hidden accounting from receipts + source" into "read java's exact internal value — and the full per-tx state delta — at any historical block."
A mainnet node only exposes receipts (energy_fee, net_fee, logs) and
tip state. Java's internal accounting — the bandwidth byte breakdown, the
energy split inputs, which charge branch was taken, every account field it
changed, every contract SSTORE, every dynamic-property delta, the contractRet +
revert reason — is invisible. This tool exposes all of it by replaying a
pristine mainnet snapshot through an instrumented "oracle" build of
java-tron, offline (no peers, contention-proof, re-runnable), and then aligns
java's per-tx dump 1:1 with your node's per-transaction trace to classify the
first divergence.
The instrumented jar is inert by default — every dump is gated behind
JT_DUMP=1, so it has no effect on the hot path when off.
Why this exists. This oracle was built to assist development of tron-goblin-node, an alternative TRON node implementation, by making it possible to read java-tron's exact internal accounting at any historical block instead of guessing at it from receipts. The tool is node-agnostic — it works with any alternative TRON implementation that can emit a per-transaction trace — but if you are chasing byte-exactness against java-tron, tron-goblin-node is the reference consumer it was designed alongside.
# 1) produce java's full internal dump for a range, OFFLINE (no p2p):
./replay_offline.sh --reset --to 83317800 --from 83317790 --dump-to 83317800 \
--log /tmp/jt_dump.log
# --reset = fast (~5s) hard-link reset of the replay DB from the pristine snapshot
# fetches blocks once over gRPC, then applies them via Manager.pushBlock (no peers)
# 2) find the FIRST diverging tx (vs your node's trace + java receipts), classified:
./jtdiff.py 83317790 83317800 /path/to/your_trace.log /tmp/jt_dump.log
# 3) deep-dive one tx -- java's EVERY phase + the full state-delta:
./jtdiff.py 83317790 83317800 /dev/null /tmp/jt_dump.log --no-rpc --tx <TXID>
# one-command wrapper that does 1+2 (or 1+deep-dive):
./solve.sh 83317790 83317800 --feetrace /path/to/your_trace.log --reset
./solve.sh 83327780 83327795 --tx <TXID> --contract 41<contract-hex>| file | what |
|---|---|
instrumentation/ |
the 3 new java files + a patch that adds env-gated dumps to 8 java-tron files + INSTRUMENTATION.md (every hook + build instructions). Build the instrumented FullNode.jar from these. It also bundles two extra mains: OracleFetch (block pre-fetch over gRPC) + OracleReplay (offline apply, no peers). |
replay_offline.sh |
the contention-proof replay. Optionally fast-resets the replay DB, pre-fetches blocks once from a reference node's gRPC into a local file, then applies them OFFLINE (no peers) with JT_DUMP gating → dump log. |
reset_replay.sh |
fast (~5s) local reset of the writable replay DB from the pristine snapshot via RocksDB SST hard-links (vs a multi-minute cp -a). |
jtdiff.py |
aligns java's JT dump with your node's per-tx trace (+ java receipts as ground truth) and prints java's internal breakdown for every diverging tx. Also: full state-delta deep-dive (--tx) and a STATE diff vs your committed state (--diag-data-dir). |
solve.sh |
one-command: ensure the java dump for a range (replay if needed) → diff → print the first divergence with full context (or deep-dive one tx). |
validation/ |
neutral worked examples of the oracle's output for arbitrary public mainnet transactions. |
replay.sh |
superseded live-p2p replay variant (kept for reference; the offline flow above is the one to use). |
This tool needs four external things you must provide; the scripts read them from environment variables (or matching flags) so no path is hard-coded.
| env var | what to point it at |
|---|---|
JT_REPO |
your instrumented java-tron checkout (where you built FullNode.jar — see instrumentation/INSTRUMENTATION.md). The scripts default JAR to $JT_REPO/framework/build/libs/FullNode.jar. |
JDK8 |
a JDK 8 home (java-tron 4.8.1.1 builds/runs on JDK 8). |
ORACLE_ROOT |
a scratch dir (plenty of disk) holding the snapshot + replay DB + fetched block files. Defaults to ./oracle-work. |
PRISTINE |
the pristine java LiteFullNode RocksDB snapshot dir (contains database/), READ-ONLY. Defaults to $ORACLE_ROOT/LiteFullNode_output-directory. |
REPLAY_DIR |
the writable replay DB dir (reset from PRISTINE). Defaults to $ORACLE_ROOT/replay. |
CONF |
a java-tron conf for the replay node. Defaults to $ORACLE_ROOT/replay.conf. |
PRISTINE_HEAD |
the block number at the head of your snapshot (so the fetch starts at head+1). |
REF |
reference node gRPC host:port for block fetch (replay_offline.sh). Defaults to 127.0.0.1:50051. |
JT_REF_HTTP |
reference node HTTP base for receipts (jtdiff.py/solve.sh). Defaults to http://127.0.0.1:8090. |
JTDIFF_OURS |
default path to your node's per-tx trace log. |
JTDIFF_JAVA |
default path to the java oracle dump (defaults to /tmp/jt_dump.log). |
JTDIFF_DIAG_BIN |
(optional) your node's read-only "diag" binary for STATE diffs. |
Example:
export JT_REPO=$HOME/src/java-tron
export JDK8=$HOME/jdk8
export ORACLE_ROOT=/data/oracle
export PRISTINE=$ORACLE_ROOT/LiteFullNode_output-directory
export PRISTINE_HEAD=83316752 # the head block of YOUR snapshot
export REF=10.0.0.5:50051 # a reference java-tron node, PAST your target block
export JT_REF_HTTP=http://10.0.0.5:8090- Snapshot. The pristine snapshot is large and external — it is not
included. Obtain a java-tron LiteFullNode RocksDB snapshot (the conf for
the replay must use
storage.db.engine = "ROCKSDB"). Note its head block and setPRISTINE_HEADaccordingly. The snapshot dir must containdatabase/. - Reference node. You need a java-tron full node reachable over gRPC (block
fetch) and HTTP (receipts) whose head is past the highest target block you
want to replay —
OracleFetchpulls[snapshot_head+1 .. target]from it, andjtdiff.pyreads ground-truth receipts from its HTTP API. This can be any java-tron node you trust (e.g. your own, or a public endpoint).
See instrumentation/INSTRUMENTATION.md. In short: clone java-tron at tag
GreatVoyage-v4.8.1.1, copy in the 3 new files, apply
instrumentation-hooks.patch, then:
cd "$JT_REPO"
JAVA_HOME="$JDK8" PATH="$JDK8/bin:$PATH" \
./gradlew :framework:buildFullNodeJar -x test -x check --no-daemon \
-Dorg.gradle.java.home="$JDK8"
# output: framework/build/libs/FullNode.jar
unzip -l framework/build/libs/FullNode.jar | grep -E 'JtDump|OracleReplay|OracleFetch'The replay pre-fetches the target blocks ONCE into a local file and applies them
with no peers (Manager.pushBlock). This is deterministic and re-runnable.
(A live-p2p variant, replay.sh, is kept for reference but stalls under CPU
contention when a peer drops the node on a keepalive timeout — prefer offline.)
# fast-reset + fetch [snapshot_head+1, 83317800] + apply OFFLINE, dump [83317790,83317800]:
./replay_offline.sh --reset --to 83317800 --from 83317790 --dump-to 83317800 \
--log /tmp/jt_dump.logKey flags (./replay_offline.sh -h for all):
--to Napply blocks up to N (required).--from/--dump-toset the DUMP window.--resetfast hard-link reset from pristine first (recommended; ~5s).--addr HEX/--contract HEX→JT_ADDR/JT_CONTRACT.--no-statedelta→JT_STATEDELTA=0.--ref HOST:PORTreference gRPC (default$REF).--blocks FILEreuse a fetched file.--stop-at Nstop the apply at N (default--to).
How it works (two extra mains in the jar):
OracleFetch <host> <port> <from> <to> <outFile>— pulls blocks over gRPC byte-exact (Block.toByteArray()) into a length-prefixed.blocksfile.OracleReplay -c <conf> -d <dataDir> --blocks <file> [--stop-at N]— boots the same Spring context java-tron uses (so accounting is byte-identical) withp2pDisable=true, gets theManager, starts consensus (nolocalwitness⇒ no block production), and applies each block viaManager.pushBlock. Skips blocks ≤ the DB head; throws on a gap. Clean shutdown flushes RocksDB then halts so the LOCK releases.
The replay DB "poisons forward" each run. To re-run cleanly, reset it:
./reset_replay.sh # FAST: hard-link the immutable .sst files (~5s)
./reset_replay.sh --full # plain cp -a (slow, maximally conservative)The fast reset hard-links the (content-immutable) RocksDB .sst files and copies
only the small mutable metadata. RocksDB only ever creates NEW SSTs and rewrites
its OWN copied metadata, so the pristine snapshot is never modified (this is
exactly what a RocksDB checkpoint/backup does).
A replay JVM must be fully dead (LOCK released) before a reset — the scripts check for a live
OracleReplayand refuse otherwise.
To diff fees, jtdiff.py needs a per-tx trace from your node, one line per
transaction, in this format (the parser keys off the FEE_TRACE prefix — the
name is just a label):
FEE_TRACE blk=<num> tx=<txid> fee=<sun> energy_total=<n> energy_fee=<sun> \
net_usage=<bytes> net_fee=<sun> penalty=<n> logs=<N>:<fp>
blk/tx— block number and lowercase hex tx id (to align with the oracle).energy_fee/net_fee— the energy and bandwidth fees your node charged (sun).logs=N:fp— a log fingerprint: the count of logsN, a colon, then the first 4 bytes (8 hex chars) of a SHA-256 over the tx's logs. The default fingerprint schemejtdiff.pyreproduces for java receipts is: for each log, append the address bytes, a 1-byte topic count, each topic, a 4-byte big-endian data length, then the data; SHA-256 the buffer. Emit the same scheme from your node (or editjava_fp()/ theFT_RXregex injtdiff.pyto match whatever your node emits). The other fields (fee,energy_total,net_usage,penalty) are printed for context.
Collect these into a log file (e.g. your_trace.log). For STATE diffs you don't
need this — see below.
./jtdiff.py <LO> <HI> <your_trace.log> <java_dump.log>For every tx where energy_fee / net_fee / logfp differ from java (ground
truth = receipts from your reference node's HTTP API, or --no-rpc to use the
oracle's own derived fees), it prints java's internal phase breakdown and the
full state-delta, and reports the first divergence classified
ENERGY_FEE / NET_FEE / LOGFP.
./jtdiff.py <LO> <HI> /dev/null <java_dump.log> --no-rpc --tx <TXID>Prints java's EVERY phase for the tx + the state-delta: every account field that changed (before→after), every SSTORE (contract/slot/old→new), the dynprop deltas, and the receipt (contractRet + revert reason). This is how you pin the mechanism once the tx is found.
With your node stopped at a data-dir, diff java's per-tx final state against your committed state read READ-ONLY via an external "diag" command:
./jtdiff.py <LO> <HI> /dev/null <java_dump.log> \
--diag-data-dir /path/to/your/node/data --class storage,balanceClassifies STORAGE / BALANCE / ENERGY_USAGE / NET_USAGE and prints the
first state divergence with java's full breakdown. This mode assumes your node
exposes a read-only inspector, invoked as:
<diag-bin> diag account <hexaddr> --data-dir DIR # prints balance/usage fields
<diag-bin> diag storage <contract> 0x<slot> --data-dir DIR # prints the 32-byte slot value
If your node has no such command, skip this mode — the FEE diff and --tx
deep-dive only need the oracle dump (+ optionally your trace / receipts). Adapt
the small diag()/our_account_field()/our_storage() helpers in jtdiff.py
to your node's CLI if it differs.
Options: --no-rpc, --rpc URL, --all, --max N, --diag-bin PATH.
Log-path defaults come from $JTDIFF_OURS / $JTDIFF_JAVA.
./solve.sh <LO> <HI> [--feetrace F] [--reset] [--tx TXID] [--contract HEX] \
[--diag-data-dir DIR --class storage,balance] [--no-rpc] [--no-replay]It ensures the java dump covers [LO,HI] (runs the OFFLINE replay if not), then
runs jtdiff.py (FEE diff, or deep-dive --tx, or STATE diff).
build instrumented jar (inert until JT_DUMP=1; see instrumentation/)
└─ ./replay_offline.sh --reset --to HI --from LO --dump-to HI → /tmp/jt_dump.log (java internals + state-delta)
└─ run YOUR node with a per-tx trace over the same range → your_trace.log
└─ ./jtdiff.py LO HI your_trace.log jt_dump.log → first diverging tx, classified, java internals
└─ ./jtdiff.py LO HI … --tx TXID → ONE tx, full state-delta (pin the mechanism)
└─ ./jtdiff.py LO HI … --diag-data-dir DIR → STATE diff vs your committed DB
(or just ./solve.sh LO HI … to orchestrate it)
See instrumentation/INSTRUMENTATION.md for the full table of phases and the
fields each hook emits. All lines start JT blk=<n> tx=<txid> phase=<p> so they
align with your trace's blk=/tx=.
- The instrumented jar is inert unless
JT_DUMP=1; every dump is a couple of cheap reads behind one boolean, so the hot path is unaffected when off. - The offline harness opens no listener; the live
replay.shuses the conf's HTTP port — keep it clear of any other node. - A replay JVM must be fully dead + the RocksDB LOCK released before a reset.
- Block files are reusable across runs (same
--blocks FILE); fetch once, replay many. The reference node must be past your target block. - First boot after a reset/crash can take a couple of minutes (no
txCache.properties⇒ the tx bloom filter is rebuilt by scanningtrans). Normal, not a hang. - Heap:
--xmx 10g/12g(default 10g). The DB survives an OOM (rolls back to the last checkpoint); just resume.
See validation/ for neutral demonstrations of the oracle's readout for
arbitrary public mainnet transactions (a bandwidth burn, a TRX transfer's
per-account balance delta, and an origin/caller energy split). Each is the raw
JT … dump the tool produced, reproducible with replay_offline.sh + jtdiff.py.
LGPL-3.0 — see LICENSE and NOTICE. The instrumentation is derived from
java-tron (LGPL-3.0, tag GreatVoyage-v4.8.1.1); this repo uses the same license
to stay compatible. java-tron source is not bundled — you provide it.