Skip to content

feat(shadow-fork): fork mainnet, and run a witness slate that can solidify - #220

Open
Sunny6889 wants to merge 2 commits into
tronprotocol:developfrom
Sunny6889:feat/shadow-fork-mainnet-multiwitness
Open

feat(shadow-fork): fork mainnet, and run a witness slate that can solidify#220
Sunny6889 wants to merge 2 commits into
tronprotocol:developfrom
Sunny6889:feat/shadow-fork-mainnet-multiwitness

Conversation

@Sunny6889

@Sunny6889 Sunny6889 commented Aug 20, 2026

Copy link
Copy Markdown

What does this PR do?
Makes scripts/poc-shadow-fork.sh able to fork mainnet, and to run more than one witness.

# unchanged default — single-witness Nile PoC
./scripts/poc-shadow-fork.sh all

# fork mainnet with a slate large enough to solidify
SHADOW_FORK_NETWORK=mainnet SHADOW_FORK_WITNESS_COUNT=27 \
  ./scripts/poc-shadow-fork.sh all
  • SHADOW_FORK_NETWORK flows into both the snapshot download and the intent's network. Those must agree — java-tron validates the genesis block against the network HOCON at boot and crash-loops with "Genesis block modify" on a mismatch. Peer isolation stays as it was (empty seed list + bumped node.p2p.version).
  • SHADOW_FORK_WITNESS_COUNT generates that many keypairs, writes one witnesses entry and one funded account each, gives every node its own ports and data directory, and deploys through trond network create instead of apply so the peers find each other. fork.conf and the intent are generated rather than sed-substituted, because their arrays scale with the count.
  • New trond shadow-fork keygen subcommand (--count, --out, --output json) replaces the tronpy call the script used for witness keys. The derivation already existed for txgen, so tools/txgen/address.go moves to internal/tronaddr and both share one copy; the txgen diff is mechanical.

No new tool or binary — the pipeline strings together snapshot download, shadow-fork keygen, shadow-fork mutate, network create and config validate.

Defaults are unchanged (nile, one witness) and key stashes written before this PR still resolve. SchemaVersion 1.15.0 → 1.16.0 (MINOR: new command, no existing schema touched).

Why are these changes required?

  • Nile is not mainnet. It lacks mainnet's accounts, contracts and token supply, so a fork of it cannot answer what an upgrade or proposal does to the state that actually exists — which is the point of shadow-forking.
  • One witness never solidifies. The chain produces blocks, but the confirmation count sits at 1, far below the 2/3 a block needs to become irreversible. Anything that reads solidified state was untestable, and a solidification stall could not be reproduced at all.
  • tronpy was a barrier to entry. The script shelled out to Python to make a single keypair; its own comment noted trond had no subcommand for it. A Go project's test harness needed pip install tronpy before it would run.

This PR has been tested by:

  • Unit Tests
  • Manual Testing

Follow up

Extra details

scripts/poc-shadow-fork.sh shelled out to Python's tronpy to make the
one keypair a fork needs, because — as its own comment said — trond had
no subcommand for it. That put `pip install tronpy` between an operator
and a shadow fork, for forty lines of secp256k1 and Keccak this
repository already carries for txgen.

Add `trond shadow-fork keygen`, which fills the gap the parent command's
doc comment anticipated, and point the script at it. The derivation
moves from tools/txgen into internal/tronaddr so both callers share one
copy rather than growing a second.

With --out the key goes to a 0600 file as shell exports, which is the
shape the script sources; without it the pair goes to stdout. The JSON
form deliberately omits the private key when --out was given: the caller
asked for it on disk, and stdout ends up in logs and agent transcripts.

Python is no longer a prerequisite anywhere in the shadow-fork path.
The PoC could only do one thing: a single-witness fork of Nile. Both
limits mattered.

Nile does not have mainnet's accounts, contracts or token supply, so a
fork of it cannot answer what an upgrade does to the state that exists.
SHADOW_FORK_NETWORK now picks the chain; the value flows into the
snapshot download and into the intent's `network`, which has to agree
with it because java-tron checks the genesis block against the network
HOCON at boot and crash-loops on a mismatch.

One witness produces blocks but never solidifies them — the
confirmation count sits at 1, far below the 2/3 a block needs before
java-tron treats it as irreversible. Nothing that reads solidified
state could be tested, and a solidification stall could not be
reproduced at all. SHADOW_FORK_WITNESS_COUNT now sets the slate size:
`trond shadow-fork keygen --count` generates the keypairs, the script
writes one witnesses entry and one funded account per witness, gives
each node its own ports and data directory, and deploys through
`trond network create` rather than `apply` so the peers find each
other.

fork.conf and the intent are generated rather than sed-substituted:
their arrays are as long as the witness count, which a placeholder
template cannot express. Each node gets a copy of the mutated database
because java-tron opens its LevelDB exclusively.

Defaults are unchanged — nile, one witness — so the cheap PoC still
runs as before, and a stash written before --count existed still
resolves: the first witness stays unsuffixed, the rest are numbered
from 2.
@Sunny6889
Sunny6889 force-pushed the feat/shadow-fork-mainnet-multiwitness branch from e3d49f8 to d526753 Compare August 20, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants