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¶
internal/, notpkg/. SunSail is a binary, not a library. Zero public API surface.- Zero CGo. Signing via
decred/dcrd/dcrec/secp256k1/v4(pure Go).go-ethereumis banned. - Vendored protobufs, committed generated code. Nobody building
sunever runsprotoc. - Zero hardcoded network prices.
energyPrice, bandwidth cost, and Dynamic Energy Model parameters are read from the chain at runtime. - External binaries are managed, not bundled.
solc-tronandaderynare downloaded on demand, version pinned insail.toml, SHA-256 checksum mandatory. - Embedded Solidity assets (
sail.sol,inittemplates) viaembed.FS. - Zero home-grown Solidity parsers. solc is the only parser in the system (§5.2).
- 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.26is the language floor; thetoolchaindirective 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:
- Invocation:
solc --standard-json --base-path . --include-path lib, withsourcesreferencing only root files viaurlsandsettings.remappingsfromsail.toml. solc's Host Filesystem Loader resolves transitive imports within the allowed paths. - Depfile: the Standard JSON output enumerates in
sources/metadataevery file that participated.depgraph.gopersists{path → SHA-256(contents)}per target alongside the solc version and a settings hash. - 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.
- Fallback for old solc-tron (no
--include-path): retry loop — inline sources, and on aFile not found: Xerror, readX, add it tosources, retry. Converges in O(graph depth). outputSelection: ABI,evm.bytecode,evm.deployedBytecode.sourceMap, metadata. The srcmaps feedrunner/trace.godirectly.
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(...)):
- Compiler-emulated: for the high-level syntax
new C{salt: s}(...), solc-tron emulates the EVM convention with the0xffpreimage 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:
sun sonar create2 --mode native|evm(defaultnative). 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.- The interpreter implements the opcode formula (
0x41): what the node pushes onto the stack is not the compiler's decision. - 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
0x41opcode preimage; the "compiler-emulated0xff" 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:
- Layer 1 — basic metering (
E_basic): deterministic sum of per-opcode costs according to the active network version's schedule (versioned inenergy.go; state-dependent costs such as SSTORE 20000/5000 are resolved againstStateDB). 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):
rpc EstimateEnergyas the primary source. Critical operational nuance: available since java-tron 4.7.0.1 but gated onvm.estimateEnergy = truein 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.- Fallback —
TriggerConstantContract:energy_usedis directly the total estimate (penalty included). It is broken down asbasic = energy_used − energy_penaltyand cross-checked by recomposing with theenergy_factorfromchainparams; if it does not reconcile, the discrepancy is reported, not hidden. - Bandwidth = size of the serialized transaction (raw + signature + result margin); TRX burn using
chainparamsprices within the same session. Output:Energy / Bandwidth / TRX burntable +--json.
ERRATUM E4 qualifies the primary source — the node's
EstimateEnergy.energy_requiredis a fee-limit binary-search upper bound (1-TRX bracket), NOT the exactE_total; onlyTriggerConstantContractreports 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
0x03row 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.mddocuments 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 andsingleflight. 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.mddocuments 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);solidifiedconfirmation viaprotocol.WalletSolidity. Hence the §3 rule: oneClientConnper (network, service); both endpoints are declared per network insail.toml. - Timing: solidification ≈ 19 blocks × 3 s ≈ 60 s. Per-transaction timeout: default 90 s, configurable. Per-script
expirationwindow configurable within the protocol maximum. - Lost / expired tx: if a tx under
includeddisappears 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. Undersolidifiedthe 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 thecli/output.goANSI 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:auditreports 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
0x03returned, for all four reference inputs (lengths 0/3/32/100), bytes identical toSHA-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:
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
— 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.