Skip to content

Generator v3.1.0 - #5

Open
wanwiset25 wants to merge 59 commits into
masterfrom
generator-v3.1.0
Open

wanwiset25 wants to merge 59 commits into
masterfrom
generator-v3.1.0

Conversation

@wanwiset25

@wanwiset25 wanwiset25 commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

### Subnet Deployment v3.1.0

  • Integrate Nethermind XDC Client into Privatechain generator tool
  • Integrate Nethermind Subnet Client into Subnet generator wizard
  • Added genesis-to-chainspec converter (NMC node uses chainspec format for genesis file)
  • Pre-boot check for chainspec.json compatibility with genesis.json
  • Improve generator UI
  • Improve README
  • Updated versioning and defaults
  • Removed unused code

wanwiset25 added 24 commits July 6, 2026 04:57
- genesis-to-chainspec: stop zeroing masternode/protector/observer rewards, carry the values from genesis.config.XDPoS.v2.allConfigs
- start_xdpos.sh: take the full image ref via GENERATOR_IMAGE_VERSION instead of building it from a GENERATOR_VERSION tag
- config_gen.js: default docker_image_name to the generator-v3.0.0 tag
- xdpos_generator view: widen the custom version inputs to pure-input-2-3
- improve subnet generator UI
@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4a32f7b0-5af6-4ad0-905e-cb494af40409


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

wanwiset25 and others added 20 commits September 18, 2026 17:15
buildEngineParams() dropped the whole ENGINE_FORKS loop behind
`if (!opts.subnet)`, on the premise that the subnet engine binds none of
those names. It binds all of them:
XdcSubnetChainSpecEngineParameters derives from
XdcChainSpecEngineParameters and overrides only SealEngineType, so it
inherits every property.

The severe case is TIPUpgradeReward. Nethermind computes

  IsTipUpgradeRewardEnabled = (TipUpgradeReward ?? ulong.MaxValue) <= releaseStartBlock
  (Nethermind.Xdc/Spec/XdcChainSpecBasedSpecProvider.cs:85)

so a dropped key does not mean "off", it means "never enabled". A genesis
that states tipUpgradeRewardBlock would put the two clients on different
reward formulas and stall the chain at the first reward checkpoint.

Harmless on today's subnets, which state no tipUpgradeRewardBlock, but
silent and unguarded. Verified: with those keys present in genesis,
subnet mode now emits TIPUpgradeReward, TIPUpgradePenalty,
TipXDCXCancellationFeeBlock, Gas50xBlock and TipSigningBlock; XDPoS
output is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DEFAULT_ENGINE_SUBNET emitted XDCXAddrBinary on the strength of a rename
table that called it "a different name, not case". No such property
exists: grepping the whole Nethermind tree at master-4e36ba0 for
XDCXAddrBinary returns nothing. The bound name is XDCXAddressBinary
(Nethermind.Xdc/Spec/XdcChainSpecEngineParameters.cs:33), read into the
release spec at XdcChainSpecBasedSpecProvider.cs:95.

So the subnet XDCX address was silently discarded and the property stayed
null on every subnet chainspec the generator has produced.

The rest of the rename premise goes with it: the subnet engine class
overrides only SealEngineType, and MergeSignRange / BlackListHFNumber /
tradingStateAddressBinary bind case-insensitively. Comment block replaced
with what the code actually does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…laims (D3)

The comment above the table says it "is a spread rather than a second
full table" and that a subnet "only has to state what it disagrees
about". It was in fact ~40 restated entries, and it broke the invariant
the file header states: every transition falls back to 999999999999 so a
fork genesis never mentions stays off.

Ten of them fell back to real blocks instead -- eip150 at 2; eip155,
eip160, eip161abc, eip161d and MaxCodeSize at 3; eip140, eip211, eip214
and eip658 at 4 -- which would have switched replay protection, state
clearing, the EIP-170 cap and Byzantium on while the Go nodes ran with
them off.

Not reachable today: a subnet genesis states eip150Block 2, eip155Block
3, eip158Block 3 and byzantiumBlock 4, and the fallbacks were a verbatim
copy of those, so genesis always won. Right by transcription, with
nothing keeping it right.

The spread also restores the 17 keys the table omitted entirely, so both
engines now emit one key set.

Verified on a real subnet genesis: 17 params keys added, no value
changed, genesis block and accounts byte-identical, XDPoS output
untouched.

Also corrects the note above the table -- a subnet runs XDC-Subnet, not
the XDPoSChain binary -- and says why the table must not be restated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The XDPoSSubnet map omitted TipXDCX, TIPXDCXMinerDisable and
TIPXDCXReceiverDisable, with a comment calling them "not used at all" on
the subnet engine. All three are inherited from
XdcChainSpecEngineParameters and bound:

  - AddTransitions() adds each to the release-spec boundary set
    (XdcChainSpecEngineParameters.cs:135-142)
  - IsTIPXDCXMiner / IsTIPXDCXReceiver are computed from them
    (XdcChainSpecBasedSpecProvider.cs:80-81)

With those three added the two maps are identical, so they collapse into
one and the per-engine selector goes away.

TipXDCXCancellationFeeBlock was covered by the D1 guard removal; it
matters twice, since TRANSITION_FORKS relies on it to activate
Constantinople and Istanbul on configs that never set istanbulBlock.
Verified: a subnet genesis stating tipXDCXCancellationFeeBlock 5 now
yields eip145Transition and eip152Transition at 5.

Also aligns blackListHFNumber -> BlackListHFNumber. Collapsing the maps
exposed that the two engines emitted the same property under different
casing; Nethermind declares it BlackListHFNumber
(XdcChainSpecEngineParameters.cs:85). Binding is case-insensitive so
behaviour is unchanged, but the drift is the same class of defect as D2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… zero (D5)

Nethermind binds each v2Configs entry to V2ConfigParams, whose fields are
non-nullable value types with `init` accessors, and its CheckConfig only
validates that a switchRound: 0 entry exists and that no round repeats
(XdcChainSpecEngineParameters.cs:109-118). A field genesis omits is not
unset, it is 0, and nothing downstream can tell the difference.

checkV2Config() now runs per entry, after the subnet maxMasternodes
default is merged in, and names every missing field.

The required set is per engine, because the two Go clients declare
different V2Configs:

  XDPoS  -- XDPoSChain params/config_xdpos.go, 14 fields; a generated
            XDPoS genesis states all 14.
  Subnet -- XDC-Subnet params/config.go, 5 fields; a subnet genesis
            states exactly those 5. Requiring the other nine would
            reject every valid subnet genesis. maxMasternodes is required
            on top, since SubnetMasternodesCalculator reads it and the
            converter injects it.

The nine subnet-absent fields are safe at zero, and not by luck: the
subnet path reads none of them. Nethermind registers SubnetPenaltyHandler
(XdcSubnetModule.cs:32), which uses the hardcoded
XdcConstants.MinimumMinerBlockPerEpoch = 1 and never reads
LimitPenaltyEpoch or MinimumSigningTx; XDC-Subnet hardcodes the same 1.
The reward and protector/observer cap fields are read only by
XdcRewardCalculator, and XDC-Subnet has no protector/observer tiers.

main() now catches a translate() failure and reports it as a message and
exit 1 instead of an unhandled stack trace -- this runs in a container
where the trace is all an operator would see.

Verified: both real genesis files still convert; a subnet genesis
converted without --subnet now fails naming all nine fields, which is the
forgot-the-flag case that previously produced a silently wrong chainspec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"0xundefined" (D6)

normalizeAddress() returned '0x' + String(addr), so a genesis missing
both spellings of the foundation wallet produced the literal
foundationWalletAddr = "0xundefined", exit 0, no warning. The node then
died at load with

  System.IO.InvalidDataException: Error when loading chainspec
  (hex string of odd length)

which names neither the field nor the file.

normalizeAddress() now passes undefined/null through, and
buildEngineParams() throws naming the field. The `||` in the shared
object also becomes pick(), so an address of 0x0 -- falsy as a string
only if empty, but the pattern is wrong either way -- cannot fall through
to the second spelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nethermind parses the genesis masternode set out of genesis.extraData
only when SwitchBlock == 0; otherwise it reads the engine param
genesisMasternodes, which defaults to an empty array
(XdcChainSpecBasedSpecProvider.cs:101-112). The converter has no source
for that key and never emitted it.

Generated deployments were safe by accident, since genesis states
switchBlock: 0. Set it to anything else and the node boots reporting the
correct genesis hash with no masternodes and no error.

buildEngineParams() now throws rather than emit a chainspec that looks
right and has no validator set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Osaka warnings work, but translate() runs in-process inside the
container-manager, so console.error landed in the container log while the
operator's page said "Config generation success". check-chainspec.sh
re-runs the converter in the foreground, which was the only way one was
ever seen.

translate() now takes an optional opts.warnings array and pushes into it;
with no array it still writes to console.error, so the CLI and
check-chainspec.sh are unchanged. generate() and generateXdpos() pass a
sink and return it as a third element, and both submit templates render
the warnings alongside the success message.

Verified: the CLI still prints to stderr; the sink captures with the
console silent; both templates render a warning list and omit the section
entirely when there is none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n (D9)

check-chainspec.sh documents that the engine "is recorded in gen.env when
the deployment is generated". Only the Subnet path wrote it. The check
happened to be right anyway, because no flag is the XDPoS default -- the
safety was the default, not the record.

That left a live failure mode: the script read the environment first, so

  $ CHAINSPEC_ENGINE=XDPoSSubnet ./scripts/check-chainspec.sh
  MISMATCH: chainspec.json does not match genesis.json    # exit 3

on a working XDPoS network, and without --dry-run it archives the correct
chainspec and replaces it with a subnet one -- engine renamed, fork keys
stripped. An exported value left over from another deployment in the same
shell is enough.

Two changes: genGenXdposEnv() now records CHAINSPEC_ENGINE=XDPoS, and the
script prefers the recorded value over the environment, saying so when
they disagree. The environment is still the fallback for a deployment
generated before this, whose gen.env records nothing.

Verified all five cases: XDPoS and Subnet deployments each resolve
correctly with no env, with a stray opposing env (ignored, with a note),
and a legacy gen.env with no record still honours the environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… XDC (D10)

The per-node env file set NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP.
Nethermind's own XDC config sets FilterDiscoveryNodesByRecentIp instead
(Nethermind.Runner/configs/xdc.json). Both properties exist on
NetworkConfig and both default to true, so the discovery-side filter
stayed on regardless -- the setting was not doing what it looked like it
did. Upstream also carries DiscoveryVersion V4, EnableEnrDiscovery false
and DisableDiscV4DnsFeeder true, none of which were set here.

Both flags plus the three discovery settings now live in xdc-nmc.json and
xdc-nmc-subnet.json, and the per-node env var is dropped from
gen_xdpos.js and gen_env.js -- it is a shared setting, not a per-node one.
(The subnet path was setting it to true, i.e. the default, so it had no
effect at all.)

Alignment work rather than a bug fix: a 3-node run converged on discv4
anyway. TargetBlockGasLimit values are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every generated Nethermind node ran at debug: tens of thousands of lines
in twenty minutes, for a node that is working correctly.

Applied to both generators. The review named only gen_xdpos.js, but
gen_compose.js:53 carried the same flag for subnet Nethermind nodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
header properly (D12)

The file header already argued for dropping homesteadBlock,
byzantiumBlock and eip158Transition because Nethermind binds none of
them. The same test, run against Nethermind.Specs and Nethermind.Xdc at
master-4e36ba0, fails for 26 more keys.

engine (2): period, rewardCheckpoint. Neither exists on
XdcChainSpecEngineParameters. The mine period comes from
v2Configs[].minePeriod, which is bound.

genesis (5): nonce, mixHash, coinbase, number, gasUsed.
ChainSpecGenesisJson declares none of them -- ChainSpecLoader.cs:356-357
reads the nonce and mixHash from Genesis.Seal.Ethereum, and :369 reads
the beneficiary from Genesis.Author. These are now emitted as
seal.ethereum and author. number and gasUsed are dropped: genesis is
always block 0 with no gas used, and the loader hardcodes the number.

params (19): six with no ChainSpecParamsJson property in any form
(eip1234, eip2718, eip3554, eip4399, eip6049, eip7516) and thirteen
declared only as eipNNNNTransitionTimestamp, which a block-numbered value
cannot bind. Audited by matching every emitted key against the binding
class rather than by hand; the other 42 are genuinely bound. A comment
records the finding so they are not re-added.

The stated rationale for emitting them -- "impossible to switch on for
Nethermind alone by accident" -- does not survive nothing reading them.
eip1234Transition is the illustration: IsEip1234Enabled derives from the
Constantinople block, i.e. eip145Transition, which this converter
defaults to 0, so it was on from block 0 regardless of the 999999999999
beside it.

The genesis five had teeth. It cost nothing so far only because a
generated genesis has all five at their defaults -- which is why the
hashes matched -- but a non-zero coinbase, nonce or mixHash would have
silently given Nethermind a different genesis block.

Genesis hash re-verified: the emitted nonce (0), mixHash (zero) and
author (zero address) are exactly the values ChainSpecLoader was already
defaulting to, so the header is byte-identical and existing node data
directories stay valid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…k name (D13)

Two unrelated bits of the same scaffolding.

network_mode: "host" was set on every Go node in gen_xdpos.js and
gen_compose.js, and injectNetworkConfig deleted it again from every
service before writing the compose file, assigning a bridge IP instead.
It never reached a generated deployment. Removed at the source, along
with the now-redundant delete, so the next reader does not reason from
it.

exec.js passed no --name to translate(), so every chainspec was called
"xdpos-chain" while gen.env recorded the operator's NETWORK_NAME. Both
generate paths now pass it, and check-chainspec.sh reads NETWORK_NAME
back out of gen.env.

Both halves have to land together: verified that a chainspec generated
with --name my-subnet passes the check when the name is read back, and
reports a one-line name mismatch (exit 3, and a rewrite without
--dry-run) when it is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tip2019Block to 1 (D14)

These two are the only entries in DEFAULT_ENGINE_SUBNET that switch a
fork ON where genesis states nothing, and the only justification was
"match a working Nethermind+Go subnet deployment". TipTrc21Fee is read by
the transaction-execution path, so that was the weakest claim in the
file.

Both values are correct. Checked against XinFinOrg/XDC-Subnet, the client
a subnet actually runs:

  tip2019Block: 1 -- Nethermind: IsTIP2019 = TIP2019Block <=
    releaseStartBlock, on from block 1. XDC-Subnet hardcodes
    common.TIP2019Block = 1, isForked() from block 1. Agree.
  TipTrc21Fee: 1 -- Nethermind: (TipTrc21Fee ?? MaxValue) <=
    releaseStartBlock, on from block 1. XDC-Subnet hardcodes
    common.TIPTRC21Fee = 0 but tests it strictly, `BlockNumber.Cmp(...)
    > 0` (core/state_transition.go:271), so live from block 1 as well.
    Agree.

No value changes. The comment now carries the reason for each, and warns
against "correcting" the 1 to 0 to match the Go constant -- the Go test
is strict, so 0 would enable it a block early.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to D13, which introduced this. gen.env is written as
NETWORK_NAME=${input["text-subnet-name"]}, so an input without that field
-- /submit_preconfig posts req.body straight through -- records the
literal string "undefined". translate() meanwhile saw JS undefined and
fell back to DEFAULT_CHAIN_NAME, so the chainspec is called
"xdpos-chain".

Now that check-chainspec.sh reads NETWORK_NAME back, those two disagree:
the check re-translates with --name undefined, sees a one-line name
difference, and archives and rewrites a correct chainspec on every run.
That is precisely the failure the --name plumbing exists to prevent.

The script now ignores an empty or "undefined" value, which also covers
deployments already generated with one recorded, and exec.js writes an
empty value rather than "undefined" going forward.

Verified: "undefined" and "" yield no flag; a real name and a name
containing spaces are both passed as a single argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two stale claims in the mapping block.

period and rewardCheckpoint were still listed as mapped to engine params;
D12 stopped emitting them because neither is a property of
XdcChainSpecEngineParameters.

The v2Configs TODO claimed the masternode/protector/observer reward
amounts are "zeroed, not carried". They are carried verbatim: puppeth
computes them (calcMasternodeRewards in cmd/puppeth/wizard_genesis.go)
and writes them into V2.CurrentConfig, which aliases AllConfigs[0], and
the converter passes the whole entry through. A recent genesis carries
masternodeReward 63.42, minimumMinerBlockPerEpoch 5, limitPenaltyEpoch 5
and minimumSigningTx 30. Replaced with what the code does, including the
new checkV2Config() guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D10 moved this setting out of the per-node env files and into the shared
Nethermind configs, but set it to false in both. That silently changed
subnet behaviour: the per-node env had it true there, and false only on
the XDPoS path.

Restores the original per-engine values, now stated where the rest of the
network settings live:

  xdc-nmc.json         (XDPoS)  false   -- was false in gen_xdpos.js
  xdc-nmc-subnet.json  (Subnet) true    -- was true in gen_env.js

FilterDiscoveryNodesByRecentIp stays false on both. That is the flag
upstream's own XDC config sets, and the one D10 was about: the peer-side
filter and the discovery-side filter are separate properties, and only
the discovery-side one was never being configured at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file had accumulated a 150-line header that restated the mapping
tables key by key, plus comments orphaned by the D12 key removals. The
duplicated header had already drifted twice and needed fixing twice.

Structural changes:

- Header 150 -> 64 lines. The key-by-key mapping lists are gone; the
  tables are the mapping and are now pointed at rather than copied. What
  remains is the four things the tables cannot say for themselves: the two
  Go clients, XDPoSChain's non-canonical fork schedule, the two "off"
  conventions, and the silently-ignored-key hazard.
- buildEngineParams(): the D6/D7 guards had been spliced into the middle
  of the comment explaining the default-table spread, cutting that
  explanation in half. Guards moved to the top with their own note; the
  spread explanation is whole again and the three merge passes are
  numbered.
- Removed comments left dangling by D12: eip1234's rationale (sitting
  above eip1283) and eip2718's (above eip2929), plus an empty "// Osaka"
  heading and a misplaced beacon-chain aside.
- Compressed the DEFAULT_ENGINE_SUBNET and checkV2Config notes roughly in
  half without dropping the evidence.

Corrections found while checking the claims against source:

- eip2028: the comment said xdc-testnet.json "agrees: 999999999999". It
  omits the key entirely. Corrected.
- Header rule on fallbacks said every params transition falls back to
  999999999999. Eleven sit at 0, for forks the chain genuinely runs from
  block 0. Corrected.
- Reference spec path was /nethermind/chainspec/; it is
  src/Nethermind/Chains/. Corrected.
- Dropped the stale nethermindeth/nethermind:xdc-fixes references; the
  binding claims are checked against master-4e36ba0, the image the
  deployment pins.

Everything kept was re-verified against source: InitialBaseFee
12500000000, MaxCodeSize 24576/32768, MergeSignRange 15,
RangeReturnSigner 150, TxDataNonZeroGas 68 with EIP2028's 16 unreferenced,
IsIstanbul's OR over TIPXDCXCancellationFeeBlock, IsEIP1559,
newBerlinInstructionSet's commented-out enable2929,
newEip1559InstructionSet = Shanghai + 2929/3529/3860, London = +3198 only,
Shanghai = +3855 only, EIP-3541's 0xEF check, 3860 via IntrinsicGas, 2929
/2930 via StateDB.Prepare and the txpool type gate, 3651's warm coinbase,
1283's Constantinople-and-not-Petersburg window, EIP-2935 via
ProcessParentBlockHash, and Apothem's 61290000/71550000/83600000 splits.

Comments only: converter output is byte-identical on both engines, all
genesis files still convert and round-trip, and all three guards fire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nspec

generation

generate() has gated chainspec failure on nethermindCount since the subnet
client was integrated (3deb7b5) -- an all-Go subnet never reads the
chainspec, so a failure there must not block the deployment.
generateXdpos() never got the same gate and failed the deployment
unconditionally, even with NUM_NETHERMIND=0.

This pairs with 7028294, which stopped docker-up.sh running the pre-check
when no service mounts /work/chainspec.json. That half was right: verified
on the two generated deployments in the tree, an all-Go compose resolves
to zero chainspec targets and skips the check, while one with two
Nethermind nodes resolves to two and runs it. Generation was the half
still able to stop an all-Go network over a file nothing mounts.

generateXdpos() now counts Nethermind nodes exactly as genGenXdposEnv
records NUM_NETHERMIND -- keyed on customversion-checkbox, where the
subnet path keys on customclient-checkbox -- and only fails when one was
actually requested.

When it is not fatal, the failure now goes into the warnings array on both
paths instead of the container log alone, so the operator sees why there
is no chainspec on the success page rather than discovering it when they
later add a Nethermind node.

Not reachable with a normally generated genesis -- every genesis in the
tree still converts -- but D5, D6 and D7 added three deliberate throws to
the converter, so the asymmetry had more ways to bite than before.

Verified: both paths now resolve identically across five input shapes (no
checkbox, checkbox with count 0, count set but checkbox off, and counts 1
and 3), and the non-fatal warning renders on both success pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wanwiset25 and others added 2 commits September 21, 2026 23:02
…firmation

Stops the containers, then deletes every xdcchain* data directory, so the
next docker-up.sh starts the chain from block 0. Keys, genesis.json,
chainspec.json and the env files are left alone.

It does nothing until the operator types exactly Y -- lowercase y, yes
and everything else cancel without stopping or deleting anything. Before
asking, it prints the deployment path, the network name, the chain id,
the chainspec name, the current head block, and the absolute path and
size of every directory it would remove, plus the chain data and whole
deployment totals, so the answer is given against what will actually go.

The head block is cross-checked against the chain id. RPC ports are
host-wide, so a node answering on 8545 need not belong to this
deployment; only nodes whose eth_chainId matches genesis are counted, and
any that do not are reported as a warning to check the directory. Without
that, an unrelated chain on the same machine reads as this one's height.

Deleting by glob, so the target is pinned rather than assumed. The data
directories are defined as exactly one level above the script:

  - the script's own directory is resolved with pwd -P and must be named
    "scripts", so a copy left anywhere else refuses outright
  - the deployment root is its parent and must hold both
    docker-compose.yml and genesis.json; never / or $HOME
  - targets must be direct children of that root, real directories, and
    still inside it once resolved -- a symlinked xdcchain* is listed under
    "Not touching" and never followed
  - everything is deleted by absolute path, not a relative glob
  - if stopping the containers fails, nothing is deleted

So it operates on its own deployment whatever the working directory is.

Works on both deployment shapes: an XDPoS deployment gets docker-down.sh
at its root and that is used; a subnet one does not, so it falls back to
compose directly, taking the single profile when there is one and
requiring it as an argument when there are several.

Copied into generated deployments by gen.js and gen_xdpos.js.

Verified against fake deployments of both shapes: n / y / yes all cancel
with the data intact; Y proceeds; a failing docker-down deletes nothing
and exits with its code; no chain data exits 0 before prompting; a
missing docker-compose.yml or a script outside scripts/ refuses; a
symlink pointing outside is skipped with its target untouched; and run by
absolute path from a directory that itself contained xdcchain1 and
xdcchain9, it removed the deployment's directory and left both decoys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts D11 (52a4dfa), which lowered the Nethermind nodes to --log=info,
and drops the per-node NMC<i>_LOG_LEVEL mechanism built on top of it.

Nethermind exposes the level only through --log, a command-line option
with no config property behind it, and Init.LogRules, whose value is
Pattern:Level pairs and which throws at startup if given a bare level.
Neither makes a good per-node knob in a generated deployment, so the
compose command carries the level directly again, as it did originally.

The D11 finding still stands -- debug is tens of thousands of lines in
twenty minutes on a healthy node -- so change this line to info once the
current debugging is done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wanwiset25 and others added 2 commits September 25, 2026 16:56
Two bugs, both hit on the first real subnet run.

It demanded a profile. A subnet has three (machine1, services, subswap),
so it refused with "name the one to reset" and did nothing. Naming one
would have been wrong anyway: `down` with a single --profile leaves the
other profiles' containers running, and a running node whose data
directory has just been deleted keeps writing into a path that no longer
exists. It now takes no argument and stops every configured profile,
passing one --profile flag per entry from `config --profiles` -- verified
that plain `down` with no --profile leaves profiled services up, so they
have to be named.

It did not export HOSTPWD. A subnet docker-compose.yml interpolates it
into every volume path, so each compose call printed "The HOSTPWD
variable is not set" once per service and resolved the paths to /xdcchain1
rather than the deployment's. It is now exported as the deployment root,
which is what it means.

The docker-down.sh branch goes with the profile argument: that script
only resolves a profile and calls compose, which this now does directly
and for every profile, so both deployment shapes take one path.

Also corrects two messages that told the operator to restart with
./docker-up.sh, which a subnet deployment does not have -- it starts from
the wizard.

Verified on a subnet-shaped deployment with four containers across three
profiles: all four stopped, both data directories removed, genesis.json,
chainspec.json and gen.env kept, and no HOSTPWD warning. The
single-profile XDPoS shape still resets with no argument, and the
cancel-unless-Y path is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tches

gen hardcoded the bootnode's enode id on the claim that "the bootnode
container always starts from the same bootnode.key". No key was ever
shipped. start-bootnode.sh only writes bootnode.key when it is handed a
key through PRIVATE_KEY or PRIVATE_KEY_FILE, so it fell through to
`bootnode -genkey`, and that key lands in the container's /work, which
is not the mounted volume -- a fresh identity on every recreate.

Every node was therefore configured with an enode nothing answered to.
On a four-node subnet each one logged "Online bootnodes: 0" every 31s
and never validated the bootnode, so the mesh was whatever the nodes
caught from NEIGHBORS in the first seconds: node1 and node2 each held a
single link to node4, and node3 lost its three entries within 30s and
sat at block 0 with no peers. Being a validator, its leader slot then
timed out every fourth round.

Generate the key in config_gen, write it to bootnodes/bootnode.key, and
derive the enode id from it -- an enode id is the uncompressed secp256k1
public key minus its 0x04 prefix -- so the two cannot drift. The
bootnode reads the key back through PRIVATE_KEY_FILE, set on the service
rather than in common.env, which the relayer, stats and frontend
containers also read and where PRIVATE_KEY is a generic enough name to
be picked up. BOOTNODE_PK reuses an existing bootnode identity.

gen_xdpos carried the same hardcoded enode in two more places, for
bootnodes.list and for each masternode env file; both now go through
gen_env.bootnodeEnode, which is exported for that.

Verified against go-ethereum's own `bootnode -writeaddress`, not just
ethers: on the subnet path the written key derives to exactly the enode
in all four subnet*nmc.env, on the XDPoS path to the one in
bootnodes.list and masternode*nmc.env, and BOOTNODE_PK=0x662e81df...
reproduces e902b89c... identically across runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wanwiset25 and others added 2 commits September 25, 2026 18:01
Resetting repeatedly during a peering investigation means answering the
same prompt every time, and `./scripts/reset-chain.sh -Y` looked like it
did something: the script took no arguments and ignored what it was
given, so it printed the summary and sat at the prompt anyway.

-y, -Y or --yes skips the prompt. -Y as well as -y because Y is the
literal reply the prompt asks for, so it is the spelling a hand reaches
for first. An unrecognised argument is now refused with exit 2 rather
than ignored, which is what made the original mistake invisible, and the
parsing runs ahead of every guard so a typo costs a millisecond instead
of a full summary.

--yes replaces the prompt and nothing else. The deployment-shape check,
the / and $HOME refusal, the docker-compose.yml and genesis.json
requirement, and the symlink and resolves-outside-root skipping all stay
unconditional, and the summary is still printed -- it is the record of
what was destroyed.

The one exception is the foreign-chain notice. It fires when a node
answering on this deployment's ports reports a different chain id, and
it is addressed to whoever is reading. Unattended there is no one, so
--yes stops on it instead of rolling past, with no override flag: the
recourse is an interactive run, which shows the same notice and lets it
be judged. Deleting is still bounded to $root/xdcchain* either way, so
this is about not discarding a signal, not about what is at risk.

A piped `echo Y` keeps working, since that is the escape hatch people
already have. Reaching EOF without an answer now names --yes rather than
reporting a bare "Cancelled".

Verified on a throwaway deployment: unknown argument refused with exit
2; -y, -Y and --yes each reset and exit 0; piped Y unchanged; piped n
and EOF both leave the data in place with exit 1; a live node of another
chain id on 8545 makes --yes exit 2 having stopped and deleted nothing;
and the script moved out of scripts/ still refuses under --yes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The right value depends on which clients a deployment runs, so a static
one in the xdc-nmc*.json that every node mounts alike was bound to be
wrong for half the deployments -- and both files were:

  xdc-nmc-subnet.json  true   correct mixed, strands an all-Nethermind net
  xdc-nmc.json         false  correct all-Nethermind, churns when mixed

All Nethermind needs false. The nodes boot together, learn each other at
once through the bootnode and dial simultaneously; the collisions leave
every retry suppressed for the filter's five-minute window, and the peer
manager sits on "EligibleCandidates: 2, Tried: 0" with nodes at zero
peers. On a four-node subnet needing three to agree that is a stalled
chain, observed twice: blocks [21,21,21,0] peers [1,2,1,0] on one run,
and stuck at block 1 with two nodes isolated on another. Upstream
Nethermind disables the filter for Hive and the E2E sync tests, which
are the same shape -- several nodes, one host, private addresses.

Mixed with the Go client needs true, which is the opposite of what the
mesh argument above suggests. The Go client has no such filter and
redials without pause, so an unfiltered Nethermind tears those sessions
down and rebuilds them continuously: 451 failed connections per second,
"snappy: corrupt input" on every Go-to-Nethermind handshake, the Go
node's own admin.peers holding no Nethermind peer in 400 consecutive
samples, and 10 MB of log in fourteen minutes. The chain still ran, on
the churn rather than on any session. Leaving the filter on lets an
established session stay up instead.

So it moves to the per-node env, computed from num_nethermind, which is
where it lived before D10 folded it into the shared config; the static
value is dropped from both JSON files so there is one source of truth
and nothing silently overridden.

Underneath this is an interop bug -- devp2p snappy framing between
xdcsubnets v0.3.2 and Nethermind on the xdpos2 capability, while
Nethermind-to-Nethermind on xdc165 is unaffected. Until that is fixed
this setting decides only how hard the two clients retry past it.

Verified by generating both paths at three mixes: 4 of 4 gives false, 2
of 4 gives true, 0 of 4 emits no Nethermind env file at all, and no
copied config still carries the key. The mixed value is the one tested
at 2 of 4, where neither client group reaches quorum alone, so the chain
advances only if votes really do cross between them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
DockerHub — 71483cb3 Deployed Sep 25, 2026 by wanwiset25 via build and push image to registry #106
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