Skip to content

SunSail — ARCHITECTURE.md

Status: v1.2 (2nd audit integrated) · Toolchain: Go 1.26 (toolchain go1.26.5) · Build: CGO_ENABLED=0, single static binary

This document is the source of truth for the architecture. The execution plan lived in SPRINTS_PLAN.md (S0–S7; retired after the implementation phase completed — the sprint history survives in CHANGELOG.md and in the historical references below); on any conflict, this document wins. External audit adjudications: Appendices A (round 1) and B (round 2).


1. Non-Negotiable Principles

  1. internal/, not pkg/. SunSail is a binary, not a library. Zero public API surface.
  2. Zero CGo. Signing via decred/dcrd/dcrec/secp256k1/v4 (pure Go). go-ethereum is banned.
  3. Vendored protobufs, committed generated code. Nobody building sun ever runs protoc.
  4. Zero hardcoded network prices. energyPrice, bandwidth cost, and Dynamic Energy Model parameters are read from the chain at runtime.
  5. External binaries are managed, not bundled. solc-tron and aderyn are downloaded on demand, version pinned in sail.toml, SHA-256 checksum mandatory.
  6. Embedded Solidity assets (sail.sol, init templates) via embed.FS.
  7. Zero home-grown Solidity parsers. solc is the only parser in the system (§5.2).
  8. Ground truth is java-tron, not prose. Every documentation divergence (CREATE2, precompiles, metering) is resolved empirically against the network: if the node contradicts the documentation, the node wins and an erratum is recorded here (§6, differential suite).

2. Build, Toolchain, and Cross-Platform

go.mod:      go 1.26            # MINIMUM language version
             toolchain go1.26.5 # pinned build version (current stable series)
Build:       CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" ./cmd/sun
Matrix:      linux/darwin/windows × amd64/arm64
  • go 1.26 is the language floor; the toolchain directive pins the exact build version and is bumped in CI on every Go minor/major release without touching the minimum. Go 1.26 (GA Feb-2026) is the current stable series; its default Green Tea GC directly benefits the TVM interpreter.
  • Cross-platform data directory: never a literal ~/.sail:
func sailHome() (string, error) {
    if h := os.Getenv("SAIL_HOME"); h != "" { return h, nil }
    home, err := os.UserHomeDir()
    if err != nil { return "", err }
    return filepath.Join(home, ".sail"), nil
}
// Managed binaries: filepath.Join(sailHome(), "bin", "solc-"+version+exeSuffix)
// exeSuffix = ".exe" when runtime.GOOS == "windows"

3. Project Tree

sun-sail/
├── cmd/sun/main.go                 # cobra wiring + exit codes. <50 lines.
│
├── internal/
│   ├── cli/                         # init.go compile.go test.go script.go estimate.go audit.go sonar.go
│   │   └── output.go                # Terminal rendering (raw ANSI) + global --json
│   ├── config/config.go             # sail.toml: strict validation, unknown key = error
│   ├── scaffold/scaffold.go         # sun init from embed.FS
│   │
│   ├── compiler/
│   │   ├── solcmgr.go               # solc-tron download/pin/checksum, concurrency lock
│   │   ├── pipeline.go              # Standard JSON I/O: solc as import resolver (§5.2)
│   │   ├── depgraph.go              # Depfile: import graph extracted from solc OUTPUT
│   │   ├── cache.go                 # Content-addressed cache + transitive invalidation
│   │   └── artifacts.go             # /out: ABI, TVM bytecode, srcmap-runtime, metadata
│   │
│   ├── address/address.go           # Base58Check ↔ Hex 0x41. Zero deps. Fuzzed.
│   ├── abi/                         # selector.go (keccak[:4]) + codec.go (encode/decode)
│   │
│   ├── crypto/
│   │   ├── sign.go                  # SignCompact → reordered R||S||V (§5.1)
│   │   └── sign_test.go             # java-tron vectors + recovery invariant
│   │
│   ├── rpc/
│   │   ├── pb/                      # generated *.pb.go (committed)
│   │   ├── client.go                # 1 ClientConn per (network, service Wallet|WalletSolidity)
│   │   ├── txbuilder.go             # ref_block, expiration, raw_data serialization, txid
│   │   ├── chainparams.go           # GetChainParameter + per-contract energy_factor, TTL cache
│   │   └── rpctest/                 # In-process Wallet/WalletSolidity server for hermetic tests
│   │
│   ├── tvm/
│   │   ├── interpreter.go           # Loop, stack, memory, frames
│   │   ├── opcodes.go               # Base EVM set + TVM divergences
│   │   ├── opcodes_tron.go          # 0xd0–0xdf against StateDB
│   │   ├── precompiles.go           # Exact transforms (§5.5)
│   │   ├── energy.go                # Basic metering + DEM layer (§5.4)
│   │   ├── addressing.go            # CREATE (txid) and CREATE2 derivation (§5.3)
│   │   └── state.go                 # StateDB interface
│   │
│   ├── state/
│   │   ├── mem.go                   # In-memory backend
│   │   └── fork.go                  # Fork backend: pinned block, lazy fetch, LRU, singleflight (§5.6)
│   │
│   ├── runner/
│   │   ├── runner.go                # *.t.sol discovery, worker pool, deterministic output
│   │   ├── cheatcodes.go            # Magic-address dispatch
│   │   └── trace.go                 # srcmap → line, Error/Panic decoding, -v/-vv/-vvv
│   │
│   ├── script/
│   │   ├── plan.go                  # Sequential simulation → []PlannedTx (§5.7)
│   │   └── broadcast.go             # BroadcastAll with confirmation policy (§5.7)
│   │
│   └── audit/aderyn.go              # Binary management + exec + TVM filter + output formats (§5.8)
│
├── assets/                          # sail.sol + templates (embed)
├── proto/                           # vendored Tron.proto, Contract.proto, api.proto
├── test/e2e/                        # Harness: builds the binary and runs full flows
├── go.mod  Makefile  .github/workflows/ci.yml

Dependency rule: cli → domain. Domain packages never import cli nor write to stdout: they return structs; cli/output.go renders. tvm knows nothing about gRPC: it consumes StateDB; state/fork.go is the one talking to rpc.


4. Dependency Map

Dependency Use Justification
stdlib (crypto/sha256, os/exec, embed, sync, context, path/filepath) Everything possible Always the first option
github.com/spf13/cobra CLI De-facto standard; no viper
github.com/pelletier/go-toml/v2 sail.toml Pure-Go TOML
google.golang.org/grpc + google.golang.org/protobuf protocol.Wallet / protocol.WalletSolidity services Spec mandate: native gRPC
github.com/decred/dcrd/dcrec/secp256k1/v4 ECDSA signing/recovery Pure Go (CGO=0), RFC 6979, canonical low-S
golang.org/x/crypto/sha3 Keccak-256 Not in stdlib
github.com/holiman/uint256 Interpreter 256-bit words (§5.5, S4) Pure Go, zero deps, allocation-free fixed [4]uint64; standalone module — go-ethereum remains banned
golang.org/x/crypto/ripemd160 Test-only: the 0x03 anti-port expectation Deprecated but stable. Production 0x03 needs no RIPEMD-160 at all — see Erratum E1
golang.org/x/sync/errgroup Bounded parallel compilation units (§5.2) Quasi-stdlib (x/ family); mandated by SPRINTS_PLAN S2
github.com/ulikunitz/xz .tar.xz extraction of the aderyn release asset (§5.8, S7) Pure Go, BSD-licensed, no relevant transitive deps; aderyn v0.6.8 publishes only xz archives and xz is not in the stdlib

Banned: go-ethereum (CGo), gotron-sdk (dependency tree, HTTP on critical paths), terminal UI frameworks, viper, and any Go Solidity parser.

Runtime-managed binaries: solc-tron and aderyn (pinned release binaries, checksums, under <SAIL_HOME>/bin/).


5. Critical Subsystem Designs

5.1 secp256k1 Signing — Mandatory Format Transform

decred is not a drop-in for the TRON/Ethereum format. ecdsa.SignCompact returns 65 bytes ordered [V | R | S] with V = 27 + recoveryID (+4 if compressed). TRON expects R || S || V with V ∈ {0, 1} (go-ethereum format; java-tron normalizes V ≥ 27 by subtracting, but we emit the canonical form):

// internal/crypto/sign.go
func Sign(priv *secp256k1.PrivateKey, txHash []byte) []byte {
    c := ecdsa.SignCompact(priv, txHash, false) // [V(1) | R(32) | S(32)], V = 27+recID
    sig := make([]byte, 65)
    copy(sig, c[1:])      // R || S
    sig[64] = c[0] - 27   // V ∈ {0,1}
    return sig
}

Guarantees decred already provides (verified by tests, never assumed): deterministic RFC 6979 nonces and canonical lower-half S. Suite invariant: RecoverCompact over the reordered signature must reconstruct the public key whose 0x41 || keccak256(pub)[12:] equals the signer address. Reference vectors: real java-tron/wallet-cli signatures over known raw_data.

5.2 Compilation Pipeline — solc as the Sole Import Resolver

No regex, no home-grown parser. The import graph is resolved and reported by the compiler itself:

  1. Invocation: solc --standard-json --base-path . --include-path lib, with sources referencing only root files via urls and settings.remappings from sail.toml. solc's Host Filesystem Loader resolves transitive imports within the allowed paths.
  2. Depfile: the Standard JSON output enumerates in sources/metadata every file that participated. depgraph.go persists {path → SHA-256(contents)} per target alongside the solc version and a settings hash.
  3. Cache: on the next invocation the depfile hashes are re-checked. Everything intact → hit, solc is not executed. Any change → recompilation of the dependent closure and depfile refresh.
  4. Fallback for old solc-tron (no --include-path): retry loop — inline sources, and on a File not found: X error, read X, add it to sources, retry. Converges in O(graph depth).
  5. outputSelection: ABI, evm.bytecode, evm.deployedBytecode.sourceMap, metadata. The srcmaps feed runner/trace.go directly.

5.3 Contract Address Derivation

CREATE: the address derives from the root txid (plus the internal creation nonce), with the 0x41 prefix. Consequence that shapes all scripting: a deployment address does not exist until the transaction is built, because the txid is SHA-256(raw_data) and raw_data includes ref_block and timestamp. See §5.7. In local tests with no real transaction, the interpreter synthesizes deterministic txids per frame (SHA-256(test_seed || counter)).

CREATE2 (opcode 0xf5) — two coexisting derivations. The reference documentation establishes a duality:

  • VM-native (what java-tron executes for the opcode, e.g. from inline assembly create2(...)):
addr21 = 0x41 || keccak256( 0x41 || sender_21bytes || salt_32bytes || keccak256(initCode) )[12:]
  • Compiler-emulated: for the high-level syntax new C{salt: s}(...), solc-tron emulates the EVM convention with the 0xff preimage in its internal address calculation.

The 0x41 and 0xff preimages produce completely different addresses — this is not a byte swap of the result. The expected address therefore depends on the syntactic path, not just on (sender, salt, initCode). Resolution in SunSail:

  1. sun sonar create2 --mode native|evm (default native). The output always prints the applied mode and a one-line warning about the duality, killing the classic mistake of precomputing with the wrong convention.
  2. The interpreter implements the opcode formula (0x41): what the node pushes onto the stack is not the compiler's decision.
  3. Mandatory empirical resolution (principle §1.8): the differential suite (S6) deploys on Nile via both syntactic paths (assembly and new{salt}) and compares the real addresses byte for byte. If observed behavior contradicts the prose, the node wins and this document receives an erratum. Until then, the duality is treated as documented canon, not verified fact.

ERRATUM E3 resolves the duality above — executed on Nile (S7): BOTH syntactic paths derive with the 0x41 opcode preimage; the "compiler-emulated 0xff" convention never appears on chain. See the Errata section.

Functional-spec extension: sonar create2 extends the action table of §2.7 of the Product Spec; it stays under sonar for coherence with its Swiss-army-knife spirit.

5.4 Two-Layer Energy Model and the sun estimate Strategy

The total energy of a call is the composition of two terms of different nature:

E_total = ⌊ E_basic × (10000 + energy_factor) / 10000 ⌋
  • Layer 1 — basic metering (E_basic): deterministic sum of per-opcode costs according to the active network version's schedule (versioned in energy.go; state-dependent costs such as SSTORE 20000/5000 are resolved against StateDB). This layer must reconcile ±0: any divergence is an interpreter bug and blocks the merge.
  • Layer 2 — Dynamic Energy Model: energy_factor (10⁴ precision) is a per-contract parameter, constant during each 6-hour maintenance cycle. Its evolution is not simulated: it is read from the chain (chainparams.go) at fork creation and applied as a pure multiplier.

Comparison targets (what makes arbitrary tolerances unnecessary): TriggerConstantContract returns energy_used (current-cycle penalty included) and energy_penalty, broken out. Therefore:

E_basic_local  ==  energy_used − energy_penalty     (±0, same pinned block)
E_total_local  ==  apply the factor read at fork    (deterministic by construction)

A percentage tolerance on E_basic would mask bugs: per-opcode costs have no stochastic component. The only legitimate source of divergence is comparing against different states — eliminated by pinning every comparison to the same solidified block.

sun estimate strategy (order of preference):

  1. rpc EstimateEnergy as the primary source. Critical operational nuance: available since java-tron 4.7.0.1 but gated on vm.estimateEnergy = true in the node's configuration, and many public nodes disable it. Detection is by capability at runtime (the node's error triggers the fallback), not by version.
  2. Fallback — TriggerConstantContract: energy_used is directly the total estimate (penalty included). It is broken down as basic = energy_used − energy_penalty and cross-checked by recomposing with the energy_factor from chainparams; if it does not reconcile, the discrepancy is reported, not hidden.
  3. Bandwidth = size of the serialized transaction (raw + signature + result margin); TRX burn using chainparams prices within the same session. Output: Energy / Bandwidth / TRX burn table + --json.

ERRATUM E4 qualifies the primary source — the node's EstimateEnergy.energy_required is a fee-limit binary-search upper bound (1-TRX bracket), NOT the exact E_total; only TriggerConstantContract reports exact figures. See the Errata section.

5.5 Compatibility Precompiles — Exact Transforms

Addr Name Exact TVM transform
0x03 RIPEMD-160 out = leftPad32( RIPEMD160( SHA-256( SHA-256(input) ) ) ) — double SHA-256 before applying RIPEMD-160. RIPEMD-160 does not disappear from the pipeline; the EVM divergence is in the pre-hashing. A test ported from Ethereum must fail here, and the suite asserts it.
0x09 BatchValidateSign (TIP-43) Batch signature verification; replaces EVM's Blake2F.
0x0a ValidateMultiSign (TIP-60) Native multi-signature verification.

BLS12-381 and shielded precompiles: explicit not supported in v1 error, never a silent success. Final conformance of all precompiles: differential against Nile (§6).

ERRATUM E1 applies to the 0x03 row above — the shipped java-tron transform contains no RIPEMD-160 step at all. See the Errata section at the end of this document; docs/TVM.md documents the implemented, node-faithful transform.

5.6 State and Forking

type StateDB interface {
    GetBalance(addr []byte) *big.Int
    GetCode(addr []byte) []byte
    GetStorage(addr, key []byte) []byte
    SetStorage(addr, key, val []byte)
    // + TRC-10 accounts, frozen/delegated resources for opcodes 0xd0–0xdf
}
  • state/mem.go: pure tests, zero network.
  • state/fork.go: solidified block pinned at creation; lazy fetch via gRPC with an LRU cache and singleflight. Writes go to a local overlay; the network is read-only. A fork never queries two different heights.

ERRATUM E2 applies to the storage fetch above — java-tron's gRPC surface exposes NO contract-storage read; storage is fetched through the node's JSON-RPC module (eth_getStorageAt), sanctioned solely in the fork storage reader. See the Errata section at the end of this document; docs/FORKING.md documents the implemented model.

5.7 Scripting API — Full Plan, Broadcast with TRON Semantics

The product spec requires computing the costs of all sequenced transactions before emitting any. Combined with §5.3 (CREATE address = f(txid)), the forced design is real sequential construction:

type PlannedTx struct {
    Raw       *pb.Transaction // signed, broadcast-ready
    TxID      [32]byte        // SHA-256(raw_data) — locally derivable
    Deploys   []Address       // addresses derived from THIS txid
    Energy    uint64          // basic + DEM
    Bandwidth uint64
    BurnTRX   uint64
}

Plan(ctx)                         → []PlannedTx, error
BroadcastAll(ctx, plan, policy)   → error

Plan simulates on a pinned fork, builds tx N, derives its txid, and injects the resulting addresses into tx N+1.

Confirmation policy — TRON semantics, not Ethereum-style "N confirmations". In TRON a transaction's state is binary: unsolidified (revertible) or solidified (confirmed by >2/3 of the SRs; irreversible by construction). There is no meaningful intermediate confirmation count, so the policy does not model one:

Policy Semantics Advance to the next tx
none Broadcast and continue Immediate
included Visible via GetTransactionInfoById on the FullNode Fast; revertible until solidification
solidified (default) Visible via the protocol.WalletSolidity service Irreversible downstream
  • Endpoints: broadcast always via protocol.Wallet (FullNode); solidified confirmation via protocol.WalletSolidity. Hence the §3 rule: one ClientConn per (network, service); both endpoints are declared per network in sail.toml.
  • Timing: solidification ≈ 19 blocks × 3 s ≈ 60 s. Per-transaction timeout: default 90 s, configurable. Per-script expiration window configurable within the protocol maximum.
  • Lost / expired tx: if a tx under included disappears before solidifying, or expires without inclusion, the planner re-derives the plan suffix (new ref_block/expiration → new txids → new dependent CREATE addresses) and retries from that point. Under solidified the problem does not propagate: the planner never advances without solidification of the previous tx.

5.8 sun audit — Aderyn with Controlled Degradation and a Specified Output

Execution: per file with --output json; a file unparseable due to TVM syntax (trcToken, msg.tokenvalue, Stake 2.0 APIs) is reported SKIPPED (TVM syntax) without aborting the rest. Post-filter of rules not applicable to the TVM (e.g. selfdestruct warnings with EVM semantics); filtered findings are shown collapsed under --show-suppressed, never silenced without a trace. The output always includes a fixed limitations section: "static analysis with EVM semantics; TVM divergences not modeled".

Output format:

  • Terminal: table grouped by severity — SEVERITY · FILE:LINE · RULE · DESCRIPTION — using the cli/output.go ANSI conventions.
  • --json (global flag): stable structured dump for CI.
  • --report <path.md>: passthrough of Aderyn's native markdown report.
  • --fail-on <low|medium|high>: exit code ≠ 0 if any unfiltered finding has severity ≥ threshold. Default: audit reports and exits 0 — the failure policy belongs to CI, not to the tool.

6. Framework Testing Strategy

Level Scope When it runs
Unit + table-driven Every domain package Each make check
Fuzzing address (Base58 round-trip), abi, revert decoder 30 s/target in CI; committed corpus
Golden tests compile artifacts per solc version Every CI run
Hermetic gRPC estimate, txbuilder, fork, broadcast against rpc/rpctest (with and without EstimateEnergy support) Every CI run — zero network per-PR
E2E test/e2e: real binary, init → compile → test → sonar → audit in a tmpdir Every CI run
Differential //go:build differential against Nile: metering ±0, CREATE2 via both syntactic paths, precompile 0x03, signing, estimate, broadcast Nightly + manual pre-release

The differential suite is the interpreter's quality gate and the arbiter of every documentation ambiguity (§1.8).


Appendix A — 1st Audit Adjudication

# Auditor's point Verdict Resolution
1 "Go 1.26 does not exist; stable is 1.22" Rejected Stale knowledge (~2024). Go 1.26 GA Feb-2026, current minor series (1.26.5).
2 decred is not a drop-in for R||S||V signing Accepted SignCompact emits [V\|R\|S] with V=27+recID. Wrapper + invariant + java-tron vectors (§5.1).
3 Sprint 4 was overly ambitious Accepted Plan restructured in SPRINTS_PLAN.md (S0–S7).
4 CREATE2: "keccak with 0xff, then replace the first byte with 0x41" Partial Incorrect mechanism: the opcode hashes with the 0x41 preimage. Differential tests adopted (§5.3).
5 Relax metering to ±1% Partial Valid concern, wrong remedy: compare E_basic ±0 against energy_used − energy_penalty on a pinned block; DEM as a deterministic multiplier (§5.4).
6 Fragile regex; use a Go parser or solc's graph Partial solc as resolver + depfile from its output + fallback for old solc; no third-party parser (§5.2).
7 Aderyn may fail on TVM syntax Accepted Per-file degradation, visible post-filter, explicit limitations (§5.8).
8 The framework's own testing is missing Accepted Full strategy in §6; Sprint 0 dedicated to the harness.
9 ~/.sail is UNIX-centric Accepted os.UserHomeDir() + SAIL_HOME + .exe suffix (§2).
10 sun script needs a full dry-run + BroadcastAll Accepted and extended Plan/BroadcastAll API (§5.7) with the constraint the auditor missed: CREATE address = f(txid) ⇒ real sequential construction.

Appendix B — 2nd Audit Adjudication

# Finding Verdict Resolution
1 sonar create2 missing from §2.7 of the Product Spec (High severity) Accepted, severity lowered to Low It is documentation sync: nothing functional breaks, so it is not "High". create2 remains under sonar (Swiss-army-knife coherence, as the auditor himself prefers); the spec delta is recorded in §5.3.
2 CREATE2 duality — native (0x41) vs compiler-emulated (0xff) — unresolved in the architecture Accepted The best finding in the report: the address depends on the syntactic path. Triple resolution in §5.3: --mode native\|evm with a warning, interpreter bound to the opcode formula, and empirical ground truth — differential via both paths on Nile; the node overrides the prose.
3 EstimateEnergy missing from the estimate strategy Accepted with nuance True for ARCHITECTURE.md in isolation (it was already in SPRINTS_PLAN.md S3/S6, which the auditor did not receive). Formalized in §5.4 with the operational nuance the auditor omits: the real gate is not the node version but vm.estimateEnergy = true — capability detection at runtime, not version gating.
4 audit output format unspecified Accepted §5.8: severity table, --json, --report, --fail-on with a non-blocking default (the failure policy belongs to CI).
5 BroadcastAll policy undetailed; proposes "N confirmations (1 testnet / 6 mainnet)" Partial The gap was real; the remedy imports a foreign model. TRON has no meaningful "N confirmations": the state is binary unsolidified/solidified (>2/3 SRs, irreversible). Policy none\|included\|solidified with Wallet/WalletSolidity endpoints, 90 s timeout, and suffix re-derivation on loss/expiry (§5.7).
6 "Go 1.26 is no longer the latest (1.27 probably GA in August)" Partial Speculative, self-contradicting premise: "probably in August" means it has not shipped today; 1.26.5 is the current stable. The recommended practice (1.26 as minimum, floating toolchain) is made explicit in §2 — it was already the intent of the pinning.
7 0x03 "computes SHA-256 twice instead of RIPEMD-160", citing TIP-43 Partial The request (fix the exact order) is valid; the auditor's specification is doubly wrong: the pipeline is RIPEMD160(SHA256(SHA256(x))) — RIPEMD-160 does not disappear — and TIP-43 is BatchValidateSign (0x09), not 0x03. Exact transform fixed in §5.5 and arbitrated by the differential suite.

Errata

E1 — §5.5, precompile 0x03 transform (S4, source-grounded; CONFIRMED ON-CHAIN, S7)

On-chain confirmation (2026-07-19, Nile, pinned solidified block 69329380): a deployed helper staticcalling 0x03 returned, for all four reference inputs (lengths 0/3/32/100), bytes identical to SHA-256(SHA-256(input)[0:20]) — and distinct from both the Ethereum formula and this document's original pre-erratum formula. The erratum is final.

The table specifies out = leftPad32(RIPEMD160(SHA-256(SHA-256(input)))). java-tron's shipped implementation — byte-identical from at least GreatVoyage-v4.7.4 through master f8ff7c76f45ab41d9bb76922329657506819701b, with the real RIPEMD-160 already commented out in Odyssey-v3.0.0 — computes:

out32 = SHA-256( SHA-256(input)[0:20] )

RIPEMD-160 never runs, and there is no padding step (the second SHA-256 is full-width). Under the Tier-3 rule (observed node behavior beats every document), internal/tvm/precompiles.go implements the node's transform, and the conformance suite asserts that BOTH the Ethereum-ported expectation and this document's original formula fail against it.

This erratum also corrects the adjudication of Appendix B, finding 7: the auditor's substantive claim — "0x03 computes SHA-256 twice instead of RIPEMD-160" — was correct about the node (their TIP citation was still wrong), and the adjudication's counter-formula repeated the documentation error the audit was pointing at. The final empirical confirmation landed with the S7 key-enabled differential run (see the blockquote above).

E2 — §5.6, fork storage fetch "via gRPC" (S6, API-surface-grounded; Nile-probed)

§5.6 specifies the fork's lazy fetch — account, code, storage — as gRPC. The node's actual API surface makes that impossible for storage: the vendored api.proto (and java-tron through the S6-pinned master) exposes no contract-storage read on any gRPC service — not on Wallet, WalletSolidity, WalletExtension, or Database, and the Account message carries no storage either. The only storage read java-tron ships is eth_getStorageAt on its JSON-RPC HTTP module (GreatVoyage-v4.4.0+).

Under the Tier-3 rule (the node's observed surface beats every document), the fork backend reads storage through a JSON-RPC client that is narrowly sanctioned: one file (internal/rpc/jsonrpc.go), one consumer (internal/state/fork.go), physically unreachable from the transaction, broadcast, and estimate paths. The gRPC-only networking rule stands everywhere else, and check-invariants flags net/http anywhere outside this reader (plus the pre-existing solcmgr/audit download sanctions). Each network's sail.toml block gains an optional jsonrpc endpoint consumed only here.

Pinning across both transports. The reader requests every read at the pinned solidified height via a hex block tag. java-tron's JSON-RPC, however, historically serves only the latest tag — confirmed empirically on Nile (S6 differential probe, block 69328029: eth_getStorageAt with a numeric tag answers QUANTITY not supported, just support TAG as latest). The capability is detected at runtime from the node's response (never version-sniffed, mirroring the §5.4 EstimateEnergy pattern), probed once at fork creation, and surfaced as StoragePinned() == false — a CLI warning and a differential probe report it; mixed-height reads are never silent. On a tag-capable node the pin is exact.

The S6 differential suite validated the implemented model against Nile: a fork-backed local execution of USDT balanceOf reproduced the node's TriggerConstantContract byte-for-byte with E_basic_local == energy_used − energy_penalty ±0 (935 == 935 − 0, pinned solidified block 69328028).

E3 — §5.3, the CREATE2 "compiler-emulated 0xff" convention does not exist on chain (S7, Nile-observed)

§5.3 documented a duality: the opcode path derives with the 0x41 preimage while "for the high-level syntax new C{salt}(...), solc-tron emulates the EVM convention with the 0xff preimage". The mandatory empirical resolution (S7 differential, real deploys via BOTH syntactic paths from one factory on Nile, pinned solidified block 69329318) refutes the second half:

new C{salt}:            observed TD1B8oPdnyyCQPUKopn2bvCzAgMJL3TBQH == 0x41 formula   (0xff formula: TGmHGyGME…)
inline-assembly create2: observed TJuHEkQJCxa2zUYezvSQYaskD5HqhFtbVZ == 0x41 formula   (0xff formula: TPyTuJ217…)

Both syntactic paths derive with the 0x41 opcode preimage. The expected address depends only on (sender, salt, initCode) — never on the syntactic path. The interpreter (already 0x41-only) and sonar create2 --mode native were correct as built; --mode evm is retained strictly as a cross-chain parity helper (what the same inputs would derive on an EVM chain), and its warning text now states the resolution instead of the refuted duality. The 0xff byte still never appears in internal/tvm/. The child-init-code-hash cross-check (node GetContractInfo vs the committed artifact) passed in the same run, ruling out a bytecode-mismatch explanation.

E4 — §5.4, EstimateEnergy.energy_required is a fee-limit search bound, not E_total (S7, source-grounded; Nile-confirmed)

The S6 differential asserted E_total_local == EstimateEnergy (SPRINTS_PLAN S6, derived from §5.4's "primary source" framing). The node computes something else. java-tron's Wallet.estimateEnergy (master, the S4-pinned lineage) binary-searches the fee limit in sun: starting from low = energyFee × energy_used and high = getMaxFeeLimit (chain parameter), it probes 2×low and then halves the bracket until low + TRX_PRECISION ≥ high (TRX_PRECISION = 1 TRX = 10⁶ sun), answering

energy_required = ⌈ high / energyFee ⌉

— an upper bound with 1-TRX granularity, up to ≈2× the exact figure for cheap calls. Confirmed live on Nile (S7): a 16 623-energy call at 100 sun/energy answered 24 935 — exactly the reproduced search bound (⌈2 493 450 / 100⌉), and TriggerConstantContract on the same state answered the exact 16 623 that matched E_total_local ±0.

Resolution (Tier-3): the exact comparison target for the two-layer composition is TriggerConstantContract (energy_used/energy_penalty), as the metering battery already pins; the differential now validates EstimateEnergy against an exact local reproduction of the node's search (still ±0, in the node's own terms — the assertion was corrected to match the node, not relaxed). sun estimate's primary source remains EstimateEnergy: its figure is precisely what a user should set as a fee-limit-safe energy budget, and the fallback path continues to report the exact breakdown.

E5 — §1/§2, release-size criterion <12 MB → <18 MB (S8, measured; owner-approved)

The original <12 MB static-binary criterion was an estimate made before the real weight of the mandated network stack was known. Measured reality (S7, -s -w -trimpath, CGO_ENABLED=0, stripped): 15.9 MB for linux/amd64 at S7 HEAD, with the S6 HEAD already at 15.5 MB — the S7 additions (sun script, sun audit, the pure-Go xz reader) cost ~0.4 MB. The floor is set by components this architecture mandates and cannot substitute:

  • google.golang.org/grpc + the protobuf runtime (the gRPC-only networking rule, §1);
  • the generated bindings for the full vendored TRON protos, including their embedded descriptor blobs (proto/ is vendored verbatim and never pruned by hand — §1.6/§3);
  • Go's runtime plus crypto/tls/x509 (TLS endpoints on public nodes);
  • the interpreter, compiler pipeline, and CLI themselves are a minor share.

No build flag or allowed-library substitution reduces this meaningfully, and the alternatives would break stronger rules (hand-pruning vendored protos, or compressed binaries that change the artifact's nature). The criterion is therefore corrected to <18 MB — adjusted to measured reality with headroom for the remaining non-implementation work, not arbitrarily relaxed. CLAUDE.md (§1, §8), docs/BUILD.md, and the release-build skill carry the amended figure; the current binary passes with ~2 MB of margin.