Phase 2: standalone faucet bot + quip-miner bootstrap subcommand

Summary

Second MR of the v0.1 → v0.2 series. Adds the dev-side bootstrap workflow: an operator can run quip-miner bootstrap against the docker chain and end up with a funded, registered miner account ready for Phase 4's controller. The faucet that backs the funding step ships as a fully standalone script (faucet_bot.py at repo root) so it can be deployed independently of the miner code.

Depends on Phase 1 (!76 (merged)). Until Phase 1 lands on v0.2, this MR's diff includes Phase 1's commits.

What's in this MR

faucet_bot.py (new, standalone)

  • Self-contained script — no imports from shared/. Copy the file plus pip install substrate-interface aiohttp and it runs anywhere.
  • Own argparse CLI: python faucet_bot.py --node-url ws://... --faucet-key //Alice.
  • aiohttp service. POST /faucet {"dest": "0x...", "amount": <plancks>} submits Balances.transfer_keep_alive. GET /health returns 200.
  • Rate-limited per destination (default 60s).
  • Refuses to bind unless System.chain matches a known dev prefix (Development, Local Testnet, quip-local). Override with --allow-any-chain or QUIP_FAUCET_ALLOW_ANY_CHAIN=1.
  • Reconnects on BrokenPipeError / ConnectionError so idle-stale websocket connections don't kill the service.
  • Serializes substrate calls behind a chain lock — SubstrateInterface isn't thread-safe and concurrent requests racing the websocket caused torn-JSON / broken-pipe failure modes.

shared/keystore.py

  • JSON sr25519 keystore (0o600, atomic rename on write).
  • Plaintext seed for the dev workflow; encrypted: true is rejected with a clear "supported in Phase 7" error so the on-disk format already anticipates the passphrase-encrypted scheme.
  • generate, load, load_or_generate.

shared/miner_bootstrap.py

  • Idempotent bootstrap pipeline: keystore → connect → optional sudo-seed Difficulty + register_topology → ensure funded → register_miner.
  • Default seed topology: Zephyr Z(2,2) (80 nodes / 356 edges) for fast dev iteration. Above the chain's MinNodes = 16 limit and small enough to mine in seconds on CPU.
  • BoundedVec<T, _> arguments are wrapped as (value,) per substrate-interface's composite-struct encoding rule — comment in source.

quip_cli.py

  • New quip-miner click group with keygen and bootstrap subcommands. Registered as a console script via quip-miner = "quip_cli:miner_main".
  • The faucet bot is intentionally not a quip-miner subcommand — it ships as a standalone file at repo root.

shared/substrate_client.pyquery_difficulty regression fix

substrate-interface decodes StorageValue<_, T> as T::default() when storage is empty, not None. The bootstrap's idempotency check (if await client.query_difficulty() is None: seed) wrongly thought the chain was seeded with all-zero difficulty on a fresh dev chain. Fixed by honoring meta_info[result_found]. Added regression test that asserts the returned value, if any, is never the all-zeros default-struct.

Verification

  • Unit tests (tests/test_keystore.py — 9 cases): file mode, overwrite refusal, round-trip, schema validation, encrypted-keystore rejection.
  • Integration tests (tests/test_substrate_faucet.py — 4 cases against the docker chain): funding settles, malformed requests rejected, rate limit returns 429, health endpoint responds 200.
  • End-to-end manual verification against the local docker chain:
    $ python faucet_bot.py --node-url ws://localhost:9944 --port 8087 &
    $ quip-miner bootstrap --node-url ws://localhost:9944 \
          --signer-key /tmp/bootstrap-fresh.json \
          --faucet-url http://127.0.0.1:8087 --seed-chain
    bootstrap complete
      ss58 address       : 5Ebqrit3TfymL1eQh5Pj7MpepGLQzEAHsmRBdd1QC2pqjDn9
      balance (plancks)  : 10000000000000
      miner registered   : True
    $ quip-miner bootstrap ... # second run: idempotent
      miner registered   : True
      topology seeded    : False  # already done
    Verified Miners[account] storage entry exists on chain with deposit=1 UNIT, proofs_submitted=0.
  • Full suite: 807 passed, 19 skipped, 0 failed (10 new + 797 pre-existing).

Things worth a careful look

  1. The faucet is dev-only. The chain-prefix check (Development / Local Testnet / quip-local) is the safety net but --allow-any-chain exists. Document this clearly in the README rewrite (Phase 5) so nobody runs it against a production endpoint.
  2. BoundedVec wrapping: register_topology takes NodesOf<T> = BoundedVec<u32, _> which the chain metadata exposes as a 1-field composite. substrate-interface needs the value wrapped as (nodes_list,). Easy to miss when adding new pallet calls — flagged with a comment in miner_bootstrap.py.
  3. Keystore stores seed in plaintext for now. File mode is 0o600 and the path defaults to ~/.quip-miner/signing.json. The encrypted field reserves space for passphrase encryption when it lands in Phase 7.
  4. The faucet's _submit_transfer has a retry-once-on-broken-pipe loop. SubstrateInterface's underlying websocket goes stale after idle; reconnecting on the first failure recovers cleanly. Same fix may be needed in SubstrateClient.submit_extrinsic for long-running consumers — first one is the Phase 4 controller, address there.
  5. query_difficulty regression: this would have bitten anywhere we check for unset StorageValue<_, T> storage. Worth a quick scan in Phase 4 for similar patterns when adding more storage queries.

Out of scope (later phases)

  • BaseMiner.mine_work_item extraction (Phase 3)
  • New-head-driven mining controller (Phase 4)
  • MinerCore extraction, telemetry rewrite, P2P deletion sweep (Phase 5)
  • End-to-end miner-finds-a-block validation (Phase 6)
  • Passphrase-encrypted keystores + hybrid sr25519+ML-DSA-44 signing (Phase 7)
  • QuantumComputeMempool integration (Phase 8)

Merge request reports

Loading