Skip to content

Latest commit

 

History

History
185 lines (151 loc) · 8.1 KB

File metadata and controls

185 lines (151 loc) · 8.1 KB

BIP300 regtest validation stack

Local stack for validating that simplepool's pool_mode=pps coinbase shape is accepted as a valid drivechain deposit by the canonical LayerTwo-Labs enforcer.

Stack

                       ┌──────────────┐  GBT 18444
                       │  enforcer    │◀────── simplepool (pps mode)
                       │              │
              ┌────────┤              ├────────┐
              │  ZMQ   │              │ gRPC   │
              │ 29010  └──────────────┘ 50051  │
              ▼              │                 ▼
        ┌──────────┐         │           (events,
        │ bitcoind │◀────────┘   sidechain CRUD)
        │ regtest  │  RPC 18443
        │ patched  │
        └──────────┘
  • bitcoind-patched (v30.2): BIP300/301-aware Bitcoin Core fork, regtest mode.
  • bip300301_enforcer: validates BIP300 deposits, serves the getblocktemplate simplepool talks to. Its wallet runs with --wallet-sync-source=disabled — it stays in sync purely from incoming blocks, which is complete on a from-genesis regtest chain, so no electrs/Electrum server is needed.
  • thunder (L2-S9): the actual sidechain node, connected to the enforcer via gRPC on 50051, RPC on 6009 (matches what payout/lib/thunder.js expects out of the box).

Env knobs (all scripts): REGTEST_DIR relocates chain state, logs and pidfiles away from .regtest/; REGTEST_BIN_DIR relocates the binary cache (downloaded zips + extracted binaries) independently of the data (default: $REGTEST_DIR/bin), so data can be wiped or moved without re-downloading; REGTEST_SKIP_THUNDER=1 skips downloading/starting thunder; REGTEST_WALLETLESS=1 starts the enforcer with no wallet at all (template rewards go to a fixed regtest address). CI uses all of these. Sidechain activation works walletless: the block producer's persisted ack policy (SetAckAllProposals) makes every enforcer-built coinbase carry the M1/M2 commitments, and blocks are mined via MiningService/GenerateToAddress (enforcer PR #477 — the enforcer prebuilt must be new enough to have it).

Quickstart

scripts/regtest/setup.sh             # download prebuilts + write configs
scripts/regtest/start.sh             # bring up bitcoind, enforcer, thunder
scripts/regtest/status.sh            # ps-style summary
scripts/regtest/activate-thunder.sh  # propose + mine sidechain #9 until active
scripts/regtest/thunder-init.sh      # generate Thunder wallet mnemonic + address
scripts/regtest/validate.sh          # activate, mine 150, probe GBT, print runbook
scripts/regtest/inspect-coinbase.sh  # after mining: parse tip's coinbase
scripts/regtest/stop.sh

activate-thunder.sh and thunder-init.sh are both idempotent — re-running once the state is set up is a no-op.

Everything lives under .regtest/ (gitignored): binaries in .regtest/bin/, chain state in .regtest/data/, logs in .regtest/logs/, pidfiles in .regtest/run/.

What's verified today

Running start.sh brings up the full stack cleanly on aarch64-darwin (macOS Apple Silicon):

  • bitcoind-patched v30.2 listens on 127.0.0.1:18443.
  • electrs indexes the regtest chain on 127.0.0.1:60401.
  • enforcer syncs to tip in ~5s and serves getblocktemplate on 127.0.0.1:18444.

validate.sh mines 150 blocks to a P2WPKH miner address, calls GBT on the enforcer, and prints the next-step runbook.

End-to-end validation status

All infra steps verified. A tiny CPU stratum miner (cpuminer.js) connects to a pool_mode=pps simplepool running against the enforcer, finds a regtest block in seconds, and submits it. The block is accepted into the regtest chain by bitcoind- patched and the enforcer; bitcoind classifies the coinbase output as "type": "drivechain"; the OP_RETURN destination immediately follows; inspect-coinbase.sh confirms the 5-output layout.

Critical finding from running the loop: the enforcer DOES NOT credit coinbase outputs as drivechain deposits. A side-by-side test:

deposit source enforcer Ctip update
simplepool coinbase, OP_DRIVECHAIN(9) value 49.5 BTC NO (Ctip stays empty)
WalletService/CreateDepositTransaction 1 BTC YES (Ctip → 100,000,000 sats)

Both blocks are accepted into the chain. The difference is at the consensus-level deposit-recognition layer of the enforcer: it requires the deposit tx to spend real, mature, post-coinbase UTXOs to prove the BTC committed for crossover was actually spendable. Coinbase outputs fail that check.

This empirically answers the question my early research flagged as unclear: "Can a coinbase output be a valid Thunder deposit?" The answer is no, at least against the current LayerTwo-Labs enforcer.

Architectural impact

The PPS design's working assumption — "every block's coinbase deposits directly to Thunder, so the pool never custodies BTC" — needs revision. Options for the follow-up:

  1. Two-step deposit. Coinbase pays the pool's BTC P2WPKH; a separate service spends accumulated coinbase UTXOs into a proper CreateDepositTransaction periodically. Pool DOES custody BTC, briefly. Lowest implementation cost.
  2. Per-block deposit tx. Pool builds and broadcasts a deposit tx in the same block as the coinbase. Higher coordination cost.
  3. Re-examine the enforcer's deposit rule for a path that does work (e.g. is there a flag, or did the rule change in a recent release?).

The drivechain coinbase builders we landed are still useful — they produce a well-formed (if not Ctip-crediting) coinbase shape, and the parsing/structural assertions stand. The shape becomes useful again if option 3 turns up a way to make the rule permit it.

Running it

scripts/regtest/setup.sh         # one-time, downloads prebuilts
scripts/regtest/start.sh
scripts/regtest/validate.sh      # activates sidechain #9, bootstraps,
                                 # prints a proxy.conf snippet
# in another terminal — start the pool with the printed config:
./build/simplepool /tmp/regtest-proxy.conf
# in a third terminal — find a block:
node scripts/regtest/cpuminer.js --timeout 60

# after a block lands, parse its coinbase:
scripts/regtest/inspect-coinbase.sh

# expect:
#   output count: 5
#   [N]   value=49.5  type=drivechain  asm=OP_NOP5 9 1
#   [N+1] value=0     type=nulldata    asm=OP_RETURN <payload>
#   [N+2] value=0.5   type=witness_v0_keyhash

To verify the deposit-recognition finding for yourself, side-by-side with a canonical deposit:

# Ctip BEFORE a canonical deposit (after the coinbase deposit attempt):
scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \
  '{"sidechain_number":9}'
# → {} (no Ctip — coinbase deposit was ignored)

# Issue a canonical deposit:
scripts/enforcer-rpc.sh cusf.mainchain.v1.WalletService/CreateDepositTransaction \
  '{"sidechain_id":9, "address":"11111111111111111111", "value_sats":100000000, "fee_sats":1000}'
# ... and mine it:
scripts/enforcer-rpc.sh cusf.mainchain.v1.MiningService/GenerateToAddress \
  '{"blocks":1, "address":"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080"}'

# Ctip AFTER:
scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \
  '{"sidechain_number":9}'
# → {"ctip": {"txid": {...}, "value": "100000000"}}

Why this is structured as a runbook, not a one-shot test

End-to-end BIP300 validation crosses three async processes, a sidechain activation flow that takes multiple blocks, and a stratum miner — wrapping all of that in a single green/red CI check would hide where breakage actually occurred. Each script does one job loud-and-clear so a failure points at exactly one component.

That said, there IS a one-shot composition of these scripts for CI: tests/test_e2e_regtest.sh (run by .github/workflows/integration_tests.yaml). It runs the minimal stack (no thunder, walletless enforcer), prints a loud stage banner before each step, and dumps the relevant logs on failure — so a red CI run still points at the exact component that broke.