Skip to content

slave (M3): add slave boot and lifecycle - #38

Open
syntrust wants to merge 34 commits into
slave-m2from
slave-m3
Open

slave (M3): add slave boot and lifecycle#38
syntrust wants to merge 34 commits into
slave-m2from
slave-m3

Conversation

@syntrust

@syntrust syntrust commented Jul 14, 2026

Copy link
Copy Markdown

Milestone M3 of #17, stacked on #26 (base: slave-m2).

Boots a slave identity end to end: slave --cluster_config <file> --node_id S0 starts every owned shard, runs until SIGINT/SIGTERM, and shuts down cleanly. No network I/O.

What's included

  • qkc/slave: SlaveBackend with a branch-keyed shard registry (mirrors pyquarkchain SlaveServer); the slave process itself owns no database. Stop() is idempotent and blocks until every shard and chaindb is closed.
  • cmd/slave: default run action — a drop-in for how pyquarkchain's cluster.py launches a slave — with signal-driven shutdown; a second signal force-quits.

Test plan

  • go build ./cmd/slave && go test ./qkc/... ./cmd/slave/ — 131 tests / 12 packages green; gofmt/vet clean.
  • New tests: slave boot + reopen over the {mainnet, devnet} fixtures; rollback via a failing ChainService injected through Options.Chain (pebble's directory lock proves the DBs were closed); idempotent Stop; cmd-level bootSlave pipeline with DB_PATH_ROOT redirected to a temp dir.
  • Manual demo: boot S0 from the devnet fixture → per-shard shard started genesis=0x… head=0^C exits 0 → rerun on the same datadir logs existing genesis validated per shard (via qkc/shard.ReconcileGenesisRecord, slave (M2): stand each shard at its real QKC minor genesis block #26).

🤖 Generated with Claude Code

syntrust and others added 9 commits June 23, 2026 18:14
Boot a slave from a pyquarkchain-compatible cluster_config.json. The slave
hosts the shards assigned to one slave identity and performs no network I/O
yet (tracking issue #17).

- cmd/slave: `config` (validate + print a normalized per-slave summary) and
  `genesis` (derive the root genesis block, hash byte-identical to
  pyquarkchain's GenesisManager.create_root_block())
- qkc/config: cluster config loader (load.go) and checked-in singularity
  mainnet/devnet configs
- qkc/genesis: root-block derivation
- qkc/types: root block, token balances, helpers
- Makefile: `make slave` target

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etadata

Add the per-shard host the slave boots each of its shards through:

- genesis.go: Genesis descriptor built from ShardGenesis (fields + parsed
  ALLOC) with a Petersburg-only ChainConfig, ChainID = BASE_ETH_CHAIN_ID +
  chain_id + 1 (pyquarkchain config.py:363); Fingerprint() gives the
  descriptor a deterministic identity the stub reports as its genesis hash.
- services.go: the ShardChain seam (genesis hash, head, Stop) between the
  slave skeleton and the geth-core shard-chain task, plus the Options
  injection points for #4 (engine/miner), #5 (master conn), and sync; the
  stub chain reports head height 0 at the descriptor's identity.
- rawdb.go: GenesisMeta record (root-genesis linkage + xshard cursor at
  (root_height, 0, 0), genesis.py:92) under a single QKC-prefixed key,
  encoded with qkc/serialize; ReconcileGenesisMeta passes reopen only on an
  exact match and hard-errors naming both genesis hashes and the db path.
  Marked temporary: re-implemented, not patched, once QKC block format (#1)
  lands.
- shard.go: Shard{Branch, cfg, db, chain}; New opens an isolated pebble
  chaindb under {datadir}/shard-0x{full_shard_id}/, reconciles the metadata,
  and constructs the chain through the seam; Stop is idempotent and closes
  chain then db.

qkc/config gains ETH_CHAIN_ID parsing on ChainConfig: pyquarkchain forces
ETH_CHAIN_ID = BASE_ETH_CHAIN_ID + CHAIN_ID + 1 on load (config.py:534), so
Validate() accepts a configured value only when consistent.

Tests cover the descriptor (table-driven over the mainnet/devnet fixtures),
fingerprint determinism/sensitivity, metadata round-trip, reconcile, the
milestone demo (boot one shard into a temp dir, reopen idempotently), and
the loud mismatch errors for shard- and root-genesis config changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pyquarkchain always derives ETH_CHAIN_ID as BASE_ETH_CHAIN_ID + CHAIN_ID + 1
and overwrites any configured value with that derivation on load
(config.py:534); accept a configured value only when consistent with it.
@syntrust syntrust changed the title qkc/slave: add slave boot and lifecycle (M3) slave (M3): add slave boot and lifecycle Jul 14, 2026
@syntrust
syntrust marked this pull request as ready for review July 14, 2026 11:11
syntrust and others added 11 commits July 15, 2026 10:10
Shard.New logs "existing genesis validated" on reopen and "genesis
metadata written" on first boot, using the new existed return value.
An empty DB_PATH_ROOT means an ephemeral in-memory database in pyquarkchain
(use_mem_db, cluster_config.py:247) and in goquarkchain (geth's ephemeral-node
convention). Previously it silently created a persistent pebble db in the
process working directory.
pyquarkchain's arithmetic is unbounded, so BASE_ETH_CHAIN_ID + CHAIN_ID + 1
may exceed uint32; the uint32 sum wrapped silently (worst case to 0, which
reads as "absent" and passes any configured value).
… rule set

- The ETH_CHAIN_ID derivation is computed in uint64, matching pyquarkchain's
  unbounded arithmetic; the uint32 sum wrapped silently (worst case to 0,
  which reads as "absent" and puts a wrong replay-protection chain id into
  the EVM rule set).
- Fingerprint() now encodes the whole ChainConfig instead of just the chain
  id, so a change to the compiled-in fork schedule changes every fingerprint
  and forces a loud re-bootstrap instead of executing new semantics on an
  old db.
ReconcileGenesisMeta no longer writes on a fresh db: Shard.New commits the
record once the chain stands at that genesis, so a boot that fails at chain
construction leaves no record behind and the retry re-runs the fresh path
instead of reporting a never-validated record as existing.
…boot

The slave performs no network I/O of any kind, but registering all of
geth's debug.Flags made --pprof start an HTTP listener and --pyroscope
push to a remote server. Register an allowlisted subset instead: logging
and file-based profiling only, fail-closed against upstream additions,
with a test pinning the boundary in both directions.

Install the SIGINT/SIGTERM handler before any resource opens, so a
signal that lands during config load or shard boot still funnels into
the blocking Stop() path instead of the OS default termination. A
watcher restores default signal handling the moment the first signal
lands, keeping the second-signal force-quit available even mid-boot.
Covered by a reexec subprocess test that signals at the 'slave booting'
line and asserts a clean exit plus a reopenable datadir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pattern was added with a leading space, which is significant in
gitignore syntax, so it never matched anything. Remove it rather than
fix it and restore the trailing newline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopt the final CLI shape in the milestone that creates these files: newApp
assembles the app for tests, the cluster_config/node_id flags register globally
without Required (actions validate presence via loadClusterConfig), and only
non-networked debug flags are exposed, pinned by TestNoNetworkedDebugFlags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
syntrust and others added 12 commits July 16, 2026 16:15
Backport the final shape of the shard package into the milestone that creates
it: DBDirName/ParseDBDirName canonicalize the shard chaindb directory names,
and the stub-genesis surrogate paths carry their follow-up TODO markers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Take the finalized cmd/slave CLI shape from slave-m2 and relocate the boot
tests it superseded: the fixture helpers, TestBootSlave, and
TestBootSlaveRejectsBadNodeID move from slave_test.go (owned by slave-m1) into
run_test.go, which also adopts its final goleak TestMain form; run.go and
qkc/slave pick up their follow-up TODO markers. main.go keeps only the
one-line app.Action wiring on top of slave-m2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stone

Drop the milestone markers and the not-yet-implemented tracker, adopt the final
fixtures/pyquarkchain cross-validation section (including the virtualenv note in
the singularity regeneration command), so later milestones only add sections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README baseline now carries the final section structure; keep only the
'Running a slave' section on top (its milestone marker dropped with the rest).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pyquarkchain derivation snippet already lives (with seal/serialize output)
in qkc/config/singularity/README.md; link to that section instead of carrying
a trimmed copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The record is a bootstrap stamp standing in for the genesis block, not a
block "meta" in the pyquarkchain sense — half its fields live in the
header there — so free the word before the QKC block format (#1) ports
MinorBlockMeta. The chaindb key moves with it (QKC-genesis-meta ->
QKC-genesis-record); no migration by design: the record is delete-not-
migrate scaffolding, and a stub-era datadir simply re-stamps on its next
boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant