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 pluspip install substrate-interface aiohttpand it runs anywhere. - Own argparse CLI:
python faucet_bot.py --node-url ws://... --faucet-key //Alice. - aiohttp service.
POST /faucet {"dest": "0x...", "amount": <plancks>}submitsBalances.transfer_keep_alive.GET /healthreturns 200. - Rate-limited per destination (default 60s).
- Refuses to bind unless
System.chainmatches a known dev prefix (Development,Local Testnet,quip-local). Override with--allow-any-chainorQUIP_FAUCET_ALLOW_ANY_CHAIN=1. - Reconnects on
BrokenPipeError/ConnectionErrorso idle-stale websocket connections don't kill the service. - Serializes substrate calls behind a chain lock —
SubstrateInterfaceisn'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: trueis 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 = 16limit 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-minerclick group withkeygenandbootstrapsubcommands. Registered as a console script viaquip-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.py — query_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:
Verified
$ 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 doneMiners[account]storage entry exists on chain withdeposit=1 UNIT,proofs_submitted=0. - Full suite: 807 passed, 19 skipped, 0 failed (10 new + 797 pre-existing).
Things worth a careful look
- The faucet is dev-only. The chain-prefix check (
Development/Local Testnet/quip-local) is the safety net but--allow-any-chainexists. Document this clearly in the README rewrite (Phase 5) so nobody runs it against a production endpoint. BoundedVecwrapping:register_topologytakesNodesOf<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 inminer_bootstrap.py.- Keystore stores seed in plaintext for now. File mode is
0o600and the path defaults to~/.quip-miner/signing.json. Theencryptedfield reserves space for passphrase encryption when it lands in Phase 7. - The faucet's
_submit_transferhas 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 inSubstrateClient.submit_extrinsicfor long-running consumers — first one is the Phase 4 controller, address there. query_difficultyregression: this would have bitten anywhere we check for unsetStorageValue<_, 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_itemextraction (Phase 3)- New-head-driven mining controller (Phase 4)
MinerCoreextraction, 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)