Skip to content

Gingerbreadfork/tron-goblin-node

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

111 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tron Goblin Node

A Rust implementation of the TRON full-node protocol — the same role java-tron plays, written from scratch in Rust with byte-exact database and wire compatibility as a stated goal.

Status: pre-release, experimental. Pointed at public mainnet from a java-tron RocksDB snapshot, it catches up a multi-day backlog, holds the live tip, and applies real blocks into RocksDB chainbase state — and the block hashes it computes from its own executed state match the canonical chain block-for-block. Transactions run through an optional Block-STM parallel executor that stays byte-identical to the serial path, and it rebuilds head state on a fork-switch (reorg-driven rollback). Still open: full state-exactness across every edge case (block headers carry no state root, so that's verified separately by RPC reads — see below), long-running mainnet soak, and assorted polish. See Status for specifics. This is not a drop-in production replacement for java-tron today — though that is the goal.

What this is

tron-goblin-node is a workspace of small, focused crates that reproduce java-tron's behaviour piece by piece. The goal is one binary you can point at a peer and have it stay in lockstep with the java-tron reference implementation — same hashes, same state, same RPC responses.

Concretely, this means:

  • Byte-exact RocksDB compatibility. A java-tron snapshot can be planted under data_dir/db/ with tron-node import-snapshot, and the daemon picks up where the java node left off.
  • Wire-compatible P2P, both directions. The TRON adv-broadcast protocol (HelloMessage, BlockInventory, Inventory, FetchInvData, Block, Trx) is implemented at the byte level. A tron-goblin-node instance hand-shakes with java-tron mainnet peers and pulls real blocks — and, as a full peer, it also listens for inbound connections and serves the sync protocol, so other nodes (java-tron included) can sync from it.
  • java-tron API surface. JSON-RPC (eth_* + wallet/*) and gRPC (Wallet / WalletSolidity / Database / Monitor / Network) are both served. TronWeb, the Java SDK, and TronGrid clients can point at this node without modification.

See it for yourself

A block hash commits to the full header and the transaction Merkle root — so if a node's block hash matches the network's, every transaction in that block executed to the same result. Sync this node against mainnet, then compare its own independently-computed block hashes against the official trongrid.io API for the most recent blocks:

CT='-H Content-Type:application/json'
H=$(curl -s $CT -X POST http://127.0.0.1:8090/wallet/getnowblock -d '{}' | jq .block_header.raw_data.number)
for N in $(seq $((H-6)) $((H-2))); do
  O=$(curl -s $CT -X POST http://127.0.0.1:8090/wallet/getblockbynum -d "{\"num\":$N}" | jq -r .blockID)
  T=$(curl -s $CT -X POST https://api.trongrid.io/wallet/getblockbynum  -d "{\"num\":$N}" | jq -r .blockID)
  [ "$O" = "$T" ] && echo "✔ #$N  $O" || echo "✘ #$N  MISMATCH"
done
✔ #83425584  0000000004f8f930899faeaa52baa520eded6c93d36fb9a384d77f978817664f
✔ #83425585  0000000004f8f93133171bdd150fab96f1fffc78894c4c21d25b1d39d491e158
✔ #83425586  0000000004f8f932d13f30de9fc5856ca13ca21d34ef7080ac5195fdce0c9c5d
✔ #83425587  0000000004f8f9339d1953474fac8c5fb3ce6fd7c9d31dba40ce05eb7830792a
✔ #83425588  0000000004f8f934a8d80f6d0164af2e9c2086e2af088e09a378a4cfa9238ebd

Matching the header and tx-merkle root is only half the story, though — TRON headers commit to no state root, so the resulting state is verified separately (see Compatibility notes). That distinction is the whole reason this project is careful about the word "parity."

Motivation

This node exists because of trongoblin.com.

Running production infrastructure on TRON means living downstream of java-tron — its release cadence, its operational quirks, its resource profile. A second independent implementation in a different language is the cheapest way to harden the ecosystem: divergences get surfaced as bugs instead of silently propagating, snapshot and RPC paths get a second set of eyes, and operators get a node they can actually profile, debug, and tune without fighting a JVM.

Status

What works today:

  • ✅ Crypto vertical slice: secp256k1, SM2 (via pure-Rust sm2 crate), keccak256, ripemd160, sha256, base58check.
  • ✅ Proto / wire / types layer — produces hashes identical to java-tron for blocks and transactions across the live chain.
  • ✅ Block import: decode + validate + execute live mainnet blocks into per-store RocksDB state.
  • Block-STM parallel execution (optional, vm.parallel_exec). Within a block, transactions execute optimistically across cores with MVCC read/write-set tracking and re-execute only the ones that conflict, producing a result byte-identical to the serial loop — pinned by parallel-vs-serial equivalence tests, since TRON's absent state root means a silent divergence would otherwise be invisible. On a many-core machine, transaction-heavy mainnet blocks apply roughly 2× faster (more on the densest, contract-heavy blocks); light blocks skip the machinery and run serially.
  • ✅ Actuator dispatch: full coverage of java-tron's contract types (Transfer, AssetTransfer, Exchange*, FreezeBalance*, Witness*, Proposal*, TriggerSmartContract, CreateSmartContract, …).
  • ✅ TVM: smart-contract execution on a revm-based interpreter with TRON's energy schedule, the TRC-10 transfer fields + TRON-extended opcodes (0xd0..0xd4), the per-contract dynamic-energy model, and Sapling shielded-TRC-20 (Groth16) proving. Real mainnet contracts execute against snapshot state; read-only (triggerconstantcontract) return data for tested TRC-20s (USDT and others) matches java-tron byte-for-byte. Energy-accounting parity is close, with known residual gaps still being chased.
  • ✅ JSON-RPC + REST: the eth_* surface that java-tron exposes plus the /wallet/* REST endpoints, backed by chainbase reads.
  • ✅ gRPC server on the Wallet / WalletSolidity / Database / Monitor / Network services — no Status::unimplemented stubs left.
  • ✅ Mempool with signer recovery + dedup + expiration eviction + on-disk persistence (java-tron's pending queue is volatile; tron-goblin-node reloads pending txs across restarts), plus state-aware admission validation — the same precondition checks a peer runs on receive (fee / permission / balance / ref-block, and smart-contract Trigger/Create preconditions) so we don't relay transactions a peer would reject.
  • ✅ Snapshot import / export (import-snapshot, import-live, export-snapshot, verify-snapshot) for moving state to and from a java-tron data directory.
  • ✅ Multi-peer sync from public mainnet: a java-tron-style block-chain locator (SyncBlockChain → drain queue → re-request → live-tip BlockInventory advertise at head), pipelined with rate-limit + keepalive parity. A single active syncer is elected across the per-peer driver fleet (the rest stay connected as standby and fail over if it stalls), the node listens for inbound peers (binds the P2P port, completes the responder handshake, and serves SyncBlockChain → inventory and FetchInvData → blocks) so other nodes — java-tron included — can sync from it, and the Hello advertises a truthful solid/lowest block. Fetch is cooperative across the peer fleet — many peers download the backlog into a shared pool in parallel while one driver applies in chain order. Live-validated catching up a multi-day backlog off public mainnet and holding the tip, not just against a local reference node.
  • ✅ SR block production: when [witness] is configured, the daemon runs java-tron's DposTask loop — slot ownership check, drain mempool, produce + sign + apply + broadcast.
  • ✅ PBFT vote runtime: Prepare → Commit → solidify state machine driven off the SR runtime; signatures persist to PbftSignDataStore and LATEST_SOLIDIFIED_BLOCK_NUM advances.
  • ✅ Reorg-driven state rollback in the sync path. On a sibling-fork overtake the driver rolls the losing branch's state back (per-block undo records restore every mutated store, incl. head pointers), re-applies the winning branch, re-pushes reverted txs to the mempool, and recovers atomically if a re-apply fails. Two backends: BlockUndoStore (default) and a SnapshotManager-style overlay stack (--snapshot-reorg). The solidified/irreversible block is never crossed (forks diverging below it are rejected).
  • Built-in address-history indexer ([index] enable = true): TronGrid-compatible /v1/accounts/{address}/transactions[/trc20|/trc721|/internal] plus an event-search endpoint /v1/contracts/{address}/events, served from the node's own stores — no external indexer, no API keys. Backfill from a snapshot is automatic (head-first by default: recent history is queryable within seconds while the long tail fills in behind it), reorg-reconciled, restart-resuming, and the on-disk index is disposable — delete data_dir/index/ and the node rebuilds it. Same query params as TronGrid (limit, fingerprint, only_from/only_to, only_confirmed, timestamps, order_by), so existing TronWeb history code is a drop-in.
  • Historical-state archive ([index] capture_state_deltas = true): every block's committed write-set recorded as per-key versions, enabling getaccount / getaccountresource / triggerconstantcontract at any covered height via /v1/archive/... — each historical read is one seek, not a replay, and constant calls run against the full archived VM state of that height (block number/timestamp included).
  • Firehose ([index.firehose] enable = true): a durable, append-only log of applied blocks — decoded transfer facts, TRC20 logs, internal txs — with explicit UNWIND entries covering both reorgs and crash recovery, tailed by external consumers over gRPC (tronfirehose.Firehose/Tail, resume by sequence number). Reference consumers for Postgres, NATS JetStream, and ClickHouse ship as standalone workspace binaries.
  • ✅ Prometheus /metrics endpoint (--metrics-port, default 9090) exposes metrics across chain head, sync flow, reorg / fork-tree outcomes, SR block production, PBFT message traffic, mempool (size + accepted + evicted + rejected-by-reason labels), active peers (incl. inbound peers syncing from us + requests served), per-method RPC counters, and the indexer (cursor / lag / backfill edges, per-namespace row counters, archive coverage, firehose head-seq and unwind counters).

What doesn't work yet (real, currently-open gaps):

  • Long-running mainnet soak / endurance. Short live sessions pass; multi-hour, multi-day stability under realistic peer churn hasn't been characterized.
  • A couple of delegated-resource (Stake 2.0) refinements. The locked-delegation lifecycle is correct (delegate-with-lock, lock expiry, undelegate-after-expiry), but two java-tron behaviours aren't ported yet: the receiver bandwidth/energy usage-transfer on undelegate (a transient gap — the usage counters self-heal over the ~1-day decay window), and on-write maintenance of the DelegatedResourceAccountIndex lookup that backs getdelegatedresourceaccountindex (the store methods exist but aren't wired into the actuators).
  • Probably a number of other things. java-tron is large and old; some quirks will only surface when a specific client or workload hits them. This list will be updated as new items are discovered.

Tests & metrics

Parity work that doesn't have a test pinning it isn't real parity — java-tron's behaviour is too nuanced to maintain by inspection alone. So coverage is dense: every actuator branch, every RPC shape, every chainbase encoding has at least one test that fails if the byte layout drifts.

Metric Count
Workspace tests passing 2251
Ignored (gated on Sapling proving, ~50 MB params + 1–2 s each) 9
Integration test files (crates/*/tests/) 121
Source modules with #[cfg(test)] blocks 117

Per-crate breakdown of the test surface (where coverage lives is where parity risk lives):

Crate Tests Crate Tests
tron-node 366 tron-types 58
tron-actuator 321 tron-net 57
tron-rpc 314 tron-index 55
tron-tvm 297 tron-crypto 34
tron-chainbase 226 tron-mempool 25
tron-executor 149 tron-wallet 22
tron-consensus 88 tron-eventer 16
tron-grpc 66 tron-firehose-* 8
tron-proto 8 tron-replay 6

Notable test categories:

  • Behaviour-pinned against java-tron: actuator validate/execute paths, hash and signing vectors, RocksDB key encodings, RPC response shapes, energy / bandwidth accounting.
  • Live mainnet observation: peer handshake, adv-broadcast framing, and block-application tests run against captured live fixtures (see crates/tron-net/tests/live_mainnet.rs, crates/tron-node/tests/live_tip_observation.rs).
  • Shielded proving: #[ignore]-gated Groth16 round-trips for mint / transfer / burn under crates/tron-grpc/tests/create_shielded_*.rs. Run with cargo test --release -- --ignored.
  • Deliberate java-tron deviations: each of the ~handful of intentional behaviour gaps (e.g. createtransaction permissiveness, getaccount unknown-address shape) has a test that asserts our shape and references the java-tron site it diverges from.

What coverage doesn't include yet:

  • Reorg-driven state rollback is unit-tested at the sync-driver level (fork-switch head move, per-block state rollback, mempool re-push, mid-reorg failure recovery, both undo-store and snapshot-stack backends), but not yet exercised under a live sibling-fork overtake on mainnet.
  • Long-running soak / load tests against mainnet snapshots. Manual for now; CI integration is on the observability backlog.

The whole sweep (default + ignored) finishes in under 90 s on a modern laptop:

cargo test --workspace --release -- --include-ignored

Layout

The workspace is split into one crate per concern. java-tron's modules are big, monolithic Java packages; this repo flattens them out so each crate is something you can hold in your head.

Crate Role
tron-crypto secp256k1 / SM2 / keccak / ripemd / sha256 / base58check.
tron-proto Protobuf message types. Wire-compatible with java-tron.
tron-types Capsule wrappers over tron-proto — hash, id, merkle-root conventions.
tron-chainbase Storage: per-store key/value codecs over a pluggable KV backend.
tron-net Wire framing + message types for the TRON P2P protocol.
tron-mempool Validating mempool: decode + signer recovery + dedup + expiration.
tron-actuator Per-contract (validate, execute) pairs — reproduces java-tron actuator semantics.
tron-executor Block-level orchestrator. Validates structure, applies txs via the actuator dispatch table — serial or Block-STM parallel.
tron-consensus DPoS slot scheduling, witness validation, maintenance period, fork choice.
tron-tvm TRON precompiles + energy model + revm-based contract execution.
tron-rpc Ethereum-compatible JSON-RPC server backed by chainbase.
tron-grpc gRPC (Wallet / WalletSolidity / Database / Monitor / Network). Wraps tron-rpc.
tron-eventer Event subscribe / logsfilter — per-block, per-tx, per-contract-event/log triggers.
tron-index Built-in address-history indexer: extraction rules, backfill/follow engine, query layer for the /v1 history + event-search API, the versioned-KV historical-state archive, and the firehose segment log.
tron-wallet Key management + transaction signing CLI. Reads java-tron-compatible v3 keystores.
tron-replay CLI for generating + validating length-delimited TRON block streams.
tron-node Full-node daemon binary — opens stores, runs RPC, syncs blocks.

Three tron-firehose-* crates (-postgres, -nats, -clickhouse) are standalone reference consumers for the firehose stream — never linked into the node; each runs its own codegen over the same firehose.proto the node serves, so they build (and can be vendored) independently.

Four revm-* crates are vendored forks needed to plug TRON's TRC-10 transfer fields and the five TRON-extended opcodes (0xd0..0xd4) into revm's CALL machinery without re-implementing gas accounting and journal logic. All other revm crates come from crates.io unchanged.

Build

Prerequisites:

  • Rust 1.80+. Stable toolchain is fine.

  • Protoc. Used by tron-proto's build script. protoc --version should print 3.x or 5.x.

  • libclang. Pulled in transitively by rocksdblibrocksdb-sysbindgen. On Fedora / RHEL / Arch without the clang meta-package you'll need to create a libclang.so symlink — run the shim script once after cloning:

    ./scripts/setup-libclang.sh

    The script is idempotent and auto-detects the highest-versioned libclang on the host (Debian / Ubuntu / macOS paths included). .cargo/config.toml points LIBCLANG_PATH at the shim directory it creates.

Then:

cargo build --release

The full workspace compiles in ~3–5 minutes on a modern machine. Tests:

cargo test --workspace            # 2250+ tests, all defaults
cargo test --workspace --release -- --ignored
                                  # adds 9 Sapling-proving tests
                                  # (~50 MB Groth16 params + 1-2s each)

Run

Initialise a data directory:

./target/release/tron-node init --data-dir ./mainnet-data

Start the daemon against the mainnet seed peers:

./target/release/tron-node start \
    --data-dir ./mainnet-data \
    --rpc-port 8545

Or against a specific peer:

./target/release/tron-node start \
    --data-dir ./mainnet-data \
    --peer 18.221.130.41:18888

If you have your own java-tron node on the LAN and want a clean sync-from-genesis test against just that peer (no public-mainnet noise), there's a wrapper script that handles fresh-data-dir setup, TCP reachability pre-flight, log capture, and a post-run summary:

./scripts/sync-from-peer.sh <your-node-host>:18888 --max-blocks 100000

Run with --help for all options.

By default the node is a full peer: it listens on the P2P port (18888) and serves blocks to nodes that sync from it, in addition to dialing peers to sync itself. For other peers to actually reach you, that port must be open through your firewall / NAT (port-forward). To run as a firewalled, outbound-only sync client instead, set [p2p] listen = false.

To plant a java-tron snapshot first (skip the genesis-walk and start from a recent state):

./target/release/tron-node import-snapshot \
    --from ./path/to/java-tron-snapshot.tar.gz \
    --data-dir ./mainnet-data

The snapshot must be RocksDB. This node is RocksDB-only — there is no LevelDB backend. A java-tron LevelDB snapshot (db.engine = LEVELDB, the older default) will not open: RocksDB reads LevelDB SSTs only partially (you'll see Cannot find Properties block from file in the logs) and can crash trying to rewrite them. Use a RocksDB snapshot (db.engine = ROCKSDB — java-tron's recommended engine), or convert a LevelDB one first with java-tron's Toolkit db convert.

tron-node --help lists every subcommand with its flags.

Configuration is TOML, not java-tron's HOCON — config files are intentionally not drop-in. State directories are byte-exact compatible; runtime config is its own surface. A fully-annotated starting point ships at config.example.toml (every key set to its built-in default); copy it and pass it with --config.

Indexer, historical state & firehose

The node can be its own TronGrid: a built-in indexer serves address history, NFT transfers, internal transactions, and contract events from the node's own stores — self-hosted, no API keys, no external indexing stack. Enable it in the config:

[index]
enable = true            # address-history index + /v1 API
scope  = "trc20"         # native | trc20 (default) | all (adds event search)
# capture_state_deltas = true     # + historical-state archive (/v1/archive)

[index.firehose]
enable = true            # + the external-sink stream (gRPC Tail)

With a populated block store (e.g. right after import-snapshot) the index backfills automatically — no command, head-first by default so the most recent history is queryable within seconds — then follows the live head, reconciling reorgs by hash and resuming across restarts. The HTTP surface (same port as the REST API):

GET /v1/accounts/{address}/transactions             — native + contract calls
GET /v1/accounts/{address}/transactions/trc20       — TRC20 transfers
GET /v1/accounts/{address}/transactions/trc721      — NFT transfers (extension)
GET /v1/accounts/{address}/transactions/internal    — internal txs (extension)
GET /v1/contracts/{address}/events                  — event search (scope = "all")
GET /v1/archive/account?address=…&block=H           — state at height H
GET /v1/archive/accountresource?address=…&block=H
POST /v1/archive/triggerconstantcontract            — constant call at height H

Query params mirror TronGrid (limit, fingerprint pagination, only_from/only_to, only_confirmed/only_unconfirmed, min_timestamp/max_timestamp, order_by, contract_address), so existing TronWeb / TronGrid client code points here unmodified. Event search resolves event_name through the contract's on-chain ABI and returns ABI-decoded results when the ABI is stored.

Three properties worth knowing:

  • The index is disposable. data_dir/index/ can be deleted at any time; the node re-derives it from its own stores. Scope changes and format-version bumps rebuild automatically (and loudly).
  • The archive is not. capture_state_deltas records each block's committed write-set as per-key versions (one seek per historical read, no replay) — coverage starts when first enabled and cannot be back-filled, since deleted history isn't re-derivable.
  • TRC20/internal backfill needs transaction-info. Snapshots without transactionRetStore index native kinds only for pre-enable history (the gap is counted in metrics); once enabled, the node persists transaction-info for every newly-applied block.

The firehose is the push-side complement: a durable append-only log of applied blocks (decoded transfer facts, TRC20 logs, internal txs) with explicit UNWIND entries, so reorgs and crash recovery reach consumers through one protocol. External processes tail it over gRPC (tronfirehose.Firehose/Tail) resuming by sequence number; three reference consumers ship in-workspace — tron-firehose-postgres (exactly-once into an explorer schema), tron-firehose-nats (JetStream bridge), and tron-firehose-clickhouse (analytics schema). Format and cursor protocol: working/FIREHOSE.md.

Every [index] knob is annotated in config.example.toml.

Compatibility notes

  • Database: byte-exact, per-store RocksDB layout — RocksDB-only, no LevelDB backend. A java-tron snapshot is a tron-node data directory after import-snapshot, provided it was written with db.engine = ROCKSDB; LevelDB snapshots are not supported.
  • P2P: byte-exact handshake + adv-broadcast, inbound and outbound — the node dials peers to sync and also listens on the P2P port (18888 by default; [p2p] listen) to serve peers that sync from it. Identifies itself on the wire as tron-goblin/0.0.1.
  • JSON-RPC + gRPC: response shapes match java-tron's. Deliberate deviations (e.g. createtransaction permissiveness, getaccount on unknown addresses) are pinned in tests and documented at the call site.
  • Config: TOML, not HOCON. Pull settings explicitly when porting from a java-tron config.conf.
  • State parity is verified by reads, not by block hashes. TRON block headers commit to the transaction Merkle root but not to an enforced state root, so a state-computation bug can hide behind block hashes that are byte-for-byte identical to the reference chain. Parity of the resulting state is therefore checked by comparing RPC reads (getaccount, delegated-resource queries, …) against a java-tron node — not by hash equality alone. (This is exactly how the delegated-resource divergence in Status was found.)

Reference implementation

The .proto definitions needed to build are vendored at crates/tron-proto/vendored/java-tron/, so a fresh clone builds without needing the full java-tron repo on disk.

Parity work is still grounded in side-by-side reading of the java-tron source. If you want to run that comparison yourself, clone java-tron and tronprotocol/documentation-en next to this checkout — they're gitignored on purpose so this repo stays small:

git clone https://github.com/tronprotocol/java-tron.git
git clone https://github.com/tronprotocol/documentation-en.git tronprotocol/documentation-en

If you want the build to consume .proto files from a parallel java-tron clone instead of the vendored copy (useful when chasing a wire-format change before re-vendoring), point both build.rs scripts at it via:

export JAVA_TRON_PROTO_ROOT=$PWD/java-tron/protocol/src/main/protos
cargo build --release

Acknowledgements

This project stands on a stack of other people's hard work. Thanks in particular to:

  • java-tron — the reference implementation. Every parity decision in this repo was grounded by reading the Java source. Without it there is no spec to mirror.
  • revm — Dragan Rakita and the revm contributors. We use revm as a library and vendored four of its crates as forks to slot in TRON's TRC-10 transfer fields and the five TRON-extended opcodes (0xd0..0xd4) without reimplementing CALL's gas + journal logic.
  • RustCryptok256 (the secp256k1 path), sm2 (the SM2 signature path), sha2, sha3, ripemd, ecdsa, elliptic-curve. The whole crypto stack underneath tron-crypto is RustCrypto crates plus a thin TRON shim.
  • Zcash / Sapling cratessapling-crypto, bls12_381, jubjub, plus wagyu-zcash-parameters for the embedded ~50 MB Groth16 MPC parameters. Without these, the shielded TRC-20 (mint / transfer / burn) prover would have been a multi-month project on its own.
  • RocksDB and the rust-rocksdb bindings — the storage substrate every chainbase store opens against. java-tron's on-disk format is RocksDB; reusing the same engine is what makes byte-exact DB compatibility tractable.
  • Tokio, axum, tonic, and prost — the async runtime, HTTP server, gRPC + Protobuf stack underneath every network surface in the node.
  • eth_trie — Ethereum Merkle-Patricia-Trie semantics for the account-state-root path TRON inherits from Ethereum.
  • tracing — structured logging across every crate.

If you maintain a crate we depend on and you're not listed here, that's an oversight — please open an issue and we'll fix it.

License

LGPL-3.0-or-later. See LICENSE.

This matches java-tron's license. If you redistribute a modified version, the LGPL's source-availability terms apply to the modified crates.

About

A Rust implementation of the TRON full-node protocol.

Topics

Resources

License

LGPL-3.0, GPL-3.0 licenses found

Licenses found

LGPL-3.0
LICENSE
GPL-3.0
LICENSE.GPL

Stars

Watchers

Forks

Contributors