Skip to content

Repository files navigation

java-tron-oracle — java-tron internal-accounting oracle + universal divergence finder

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.


TL;DR — from "a divergence somewhere in X..Y" to "tx T, slot S: java=A ours=B"

# 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>

Components

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).

Setup

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

Getting a snapshot + a reference node

  • 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 set PRISTINE_HEAD accordingly. The snapshot dir must contain database/.
  • 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 — OracleFetch pulls [snapshot_head+1 .. target] from it, and jtdiff.py reads ground-truth receipts from its HTTP API. This can be any java-tron node you trust (e.g. your own, or a public endpoint).

Build the instrumented jar

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 OFFLINE replay — contention-proof, re-runnable

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.log

Key flags (./replay_offline.sh -h for all):

  • --to N apply blocks up to N (required). --from/--dump-to set the DUMP window.
  • --reset fast hard-link reset from pristine first (recommended; ~5s).
  • --addr HEX / --contract HEXJT_ADDR / JT_CONTRACT. --no-statedeltaJT_STATEDELTA=0.
  • --ref HOST:PORT reference gRPC (default $REF). --blocks FILE reuse a fetched file.
  • --stop-at N stop 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 .blocks file.
  • OracleReplay -c <conf> -d <dataDir> --blocks <file> [--stop-at N] — boots the same Spring context java-tron uses (so accounting is byte-identical) with p2pDisable=true, gets the Manager, starts consensus (no localwitness ⇒ no block production), and applies each block via Manager.pushBlock. Skips blocks ≤ the DB head; throws on a gap. Clean shutdown flushes RocksDB then halts so the LOCK releases.

Fast reset (re-run a range from a clean pre-state)

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 OracleReplay and refuse otherwise.


Your node's per-transaction trace (for the FEE diff)

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 logs N, a colon, then the first 4 bytes (8 hex chars) of a SHA-256 over the tx's logs. The default fingerprint scheme jtdiff.py reproduces 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 edit java_fp() / the FT_RX regex in jtdiff.py to 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.


Diff & READ the diverging field

FEE mode (energy/bandwidth fees + log fingerprint)

./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.

Deep-dive one tx (the universal readout)

./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.

STATE mode (balance / storage / dynprop vs your committed state)

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,balance

Classifies 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.


One-command: solve.sh

./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).


End-to-end

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)

Dump schema (every phase= line + its fields)

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=.

Notes / hazards

  • 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.sh uses 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 scanning trans). Normal, not a hang.
  • Heap: --xmx 10g/12g (default 10g). The DB survives an OOM (rolls back to the last checkpoint); just resume.

Worked examples

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.

License

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages