FORKING.md — The Pinned-Fork Backend (sun test --fork-url)¶
Living reference for internal/state/fork.go and the --fork-url /
--fork-block flags of sun test. Tier-1 authority is
docs/ARCHITECTURE.md §5.6 with Erratum E2; the empirical arbiter is the
S6 differential suite (test/differential/).
1. What a fork is¶
Tests run on the local interpreter over the live state of a real network,
pinned to one solidified block: accounts, code and storage are fetched
lazily on first touch, cached for the fork's lifetime, and never written back
— every write lands in a per-test local overlay. <network> is a name
declared in sail.toml carrying three endpoints:
[networks.nile]
fullnode = "grpc.nile.trongrid.io:50051" # protocol.Wallet
solidity = "grpc.nile.trongrid.io:50061" # protocol.WalletSolidity
jsonrpc = "https://nile.trongrid.io/jsonrpc"
A raw host:port is rejected: TRON needs the Wallet/WalletSolidity pair (and
the jsonrpc endpoint) — a single URL cannot carry the trio, and mis-pairing
endpoints across networks is exactly the mistake the config prevents.
2. Where each datum comes from¶
| Datum | Source | Service |
|---|---|---|
| Pinned block header (number, timestamp, witness) | GetNowBlock2 / GetBlockByNum2 |
WalletSolidity (solidified) |
| Chain id | genesis block id (GetBlockByNum2(0)) |
WalletSolidity |
| Energy price | GetChainParameters (getEnergyFee) — never hardcoded |
Wallet |
| Accounts (balance, type, TRC-10, FrozenV2/UnfreezingV2, votes, permissions) | GetAccount |
WalletSolidity (solidified) |
Contract code + energy_factor |
GetContractInfo (runtimecode) |
Wallet (the only service exposing it) |
| Storage | eth_getStorageAt — JSON-RPC, Erratum E2 |
the jsonrpc endpoint |
Erratum E2. java-tron's gRPC surface exposes no contract-storage read
(verified across every service in the vendored api.proto). The only storage
read the node ships is eth_getStorageAt on its JSON-RPC module. The reader
lives in internal/rpc/jsonrpc.go, its sole consumer is state/fork.go, and
the gRPC-only rule stands everywhere else (check-invariants enforces both
directions).
3. The pin — and the storage-tag caveat¶
A fork fixes one solidified block at creation (--fork-block, or the current
solidified head). "Pinned" means:
- The
BlockContext(NUMBER/TIMESTAMP/coinbase/chain id, energy price) is captured once, at the pin. - Every gRPC state read goes to the WalletSolidity service.
- Every fetched key is cached for the fork's lifetime — a datum is observed exactly once; a fork never queries two different heights.
- Storage reads request the pinned height as a hex block tag.
Caveat (empirically settled on Nile): java-tron's JSON-RPC serves only
the latest tag — a numeric tag answers QUANTITY not supported, just
support TAG as latest (differential probe, S6). The capability is detected
at runtime from the node's response (one probe at fork creation, mirroring
the EstimateEnergy pattern — never version sniffing). When the node is
latest-only, StoragePinned() reports false, sun test prints a warning,
and reads fall back to latest — surfaced, never silently mixed. On a
tag-capable node the pin is exact. TRON nodes serve no historical state over
gRPC either, so --fork-block pins the block context while state reads
remain latest-solidified; for stable target contracts (the common case) the
distinction is immaterial, and the differential metering suite is the proof
the pragmatics hold (±0 on Nile).
4. Caching, concurrency, failure¶
- LRU + singleflight. Storage slots live in a bounded LRU (4096 entries);
accounts and code in plain maps (few per run). All fetches go through
singleflight: two goroutines asking for the same cold key produce exactly one network call — the per-contract worker goroutines of the runner share oneForkSource. - Overlay.
ForkSource.NewState()opens a fresh per-test overlay: remote data is seeded into it unjournaled (base state — it survivesRevertToSnapshot), local writes journal normally, frame reverts restore the seeded remote values, and nothing is ever written back to the network. - Zero == absent. A zero word from
eth_getStorageAtis served as an absent row (GetStorage → nil): on chain a zeroed slot has no row, and the absent-row distinction is what java-tron's SSTORE pricing keys on. - Sticky errors.
tvm.StateDBhas no error returns, so the first fetch failure poisons the fork: later reads answer zero without further network calls, andFork.Err()reports it. The runner checks it after every test and fails the test with the fork error — incomplete state never passes (or fails) silently on fabricated zeros. - Not carried by gRPC (documented zeros for remote accounts):
per-delegation detail (
GetDelegatedOut*), legacy Stake 1.0 expiries.
5. The DEM layer on a fork¶
Each contract's energy_factor is read via GetContractInfo and frozen at
first fetch for the fork's lifetime (factors are constant within a 6-hour
maintenance cycle). The interpreter attributes consumed energy per contract
and reports EnergyPenalty = Σ⌊usage_c × factor_c / 10000⌋
(docs/TVM.md §DEM); sun test shows (energy: N + P penalty) when P > 0.
Locally deployed test contracts have factor 0.
6. Hermetic testing and recorded fixtures¶
go test ./internal/state is fully hermetic: rpctest (in-process gRPC
node) plus an in-process JSON-RPC stub replay real recorded Nile
responses from internal/state/testdata/nile/fixtures.json.
To re-record the fixtures (manual, network required, no key needed):
The recorder captures the solidified head, genesis id, chain parameters, the Nile USDT account/contract info, four storage slots, and the observed storage-tag probe result, stamping the recording time and block.
7. Differential validation (S6 keyless; S7 full key-enabled run)¶
- Fork-backed local
balanceOfon Nile USDT vs the node'sTriggerConstantContract(solidity service): return data byte-identical,E_basic_local == energy_used − energy_penalty±0 (935 == 935 − 0, S6 block 69328028; re-confirmed S7 at 69329318). - Storage pin probe: Nile is latest-only (see §3), surfaced as designed (re-confirmed S7).
- The full deploy-based suite ran with a funded key on 2026-07-19 and is
green: the seven-scenario metering battery ±0 on a fork of the freshly
deployed contract, the CREATE2 duality resolution (Erratum E3), the
0x03on-chain confirmation (E1 final), the E_total composition ±0 with the EstimateEnergy search-bound reproduction (Erratum E4), and thesun scriptplan-vs-receipts battery ±0. Full numbers:docs/TVM.md §3c.