Skip to content

Commit a6c2b72

Browse files
authored
Merge pull request #569 from frstrtr/btc/g3b-redrive-bip35-harness
btc: g3b POPULATED re-drive harness (turnkey #566 BIP35 proof)
2 parents f24fd54 + 41627df commit a6c2b72

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

tools/testnet/g3b_redrive_bip35.sh

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env bash
2+
# g3b_redrive_bip35.sh
3+
# BTC G3b POPULATED re-drive harness — proves the #566 BIP 35 mempool-pull fix.
4+
#
5+
# PR #566 root-caused the G3b POPULATED residual: main_btc subscribed
6+
# new_tx -> mempool.add_tx, which only catches txs announced via `inv` AFTER
7+
# connect. Txs already RESIDENT in the bitcoind mempool at connect time (a
8+
# pre-seeded regtest mempool) were never requested, so a won block was
9+
# coinbase-only (nTx=1). The fix exposes Node::enable_mempool_request() and
10+
# calls it from main_btc after start_p2p (mirrors main_dgb), issuing a BIP 35
11+
# `mempool` pull so the embedded TemplateBuilder sees the resident txs.
12+
#
13+
# This harness is the deploy-card ARTIFACT: it composes the exact re-drive
14+
# recipe (acknowledged by integrator 2026-06-27) into one invocation so the
15+
# moment #566 merges and the operator taps the deploy card, the POPULATED
16+
# proof runs and fails-closed on all four criteria.
17+
#
18+
# THE FOUR PASS CRITERIA (fail-closed — ALL must hold):
19+
# 1. NODE_BLOOM advertised: bitcoind launched with -peerbloomfilters=1, so
20+
# the peer carries the NODE_BLOOM (0x04) service bit. WITHOUT it the BIP 35
21+
# request is a no-op (c2pool logs "Skipped ... peer lacks NODE_BLOOM" to
22+
# avoid a disconnect) and the whole proof is vacuous — so this is the hard
23+
# guard against a false pass.
24+
# 2. The fix FIRED: c2pool logged "Sent BIP 35 mempool request" — the #566
25+
# enable_mempool_request() path actually issued the pull on connect.
26+
# 3. The won block CONNECTED: getblock confirmations >= 1 (on the active
27+
# chain, not an orphan).
28+
# 4. The block is POPULATED: nTx > 1 (expected nTx == 6 = 5 seeded + coinbase).
29+
# A coinbase-only block (nTx == 1) is the residual and a FAIL.
30+
#
31+
# The seeding discipline that makes the proof meaningful (criterion the fix
32+
# targets): the 5 txs are injected into the bitcoind mempool BEFORE c2pool
33+
# connects and are NOT followed by a generate — they stay resident and
34+
# unconfirmed, reachable ONLY via the BIP 35 pull, invisible to inv relay.
35+
#
36+
# Modes:
37+
# g3b_redrive_bip35.sh --dry-run
38+
# SELF-CONTAINED. No host, no binary, no deploy. Proves the four-criteria
39+
# assert (a) PASSES on a golden POPULATED re-drive log + block and
40+
# (b) BITES on each failure shape (NODE_BLOOM skipped, request never sent,
41+
# un-connected block, coinbase-only nTx=1). This is the acceptance gate.
42+
#
43+
# g3b_redrive_bip35.sh --go --host H --bin /path/c2pool-btc \
44+
# [--bitcoind 127.0.0.1:18443] [--stratum 9332] \
45+
# [--seed-n 5] [--rpc 'bitcoin-cli -regtest ...']
46+
# LIVE re-drive on .121. REQUIRES an operator-tapped deploy card — it
47+
# deploys/relaunches a binary and reseeds the mempool, which is a
48+
# deploy + restart op. Never run without explicit decision-card approval.
49+
# The binary MUST be a REBUILD that contains #566 (the last-deployed .121
50+
# binary predates the fix — re-driving stale would falsely fail).
51+
#
52+
set -euo pipefail
53+
54+
HOST=""
55+
BIN=""
56+
BITCOIND="127.0.0.1:18443"
57+
STRATUM="9332"
58+
SEED_N=5
59+
RPC="bitcoin-cli -regtest"
60+
MODE=""
61+
SETTLE_SECS=30
62+
63+
while [ $# -gt 0 ]; do
64+
case "$1" in
65+
--dry-run) MODE="dry"; shift ;;
66+
--go) MODE="go"; shift ;;
67+
--host) HOST="$2"; shift 2 ;;
68+
--bin) BIN="$2"; shift 2 ;;
69+
--bitcoind) BITCOIND="$2"; shift 2 ;;
70+
--stratum) STRATUM="$2"; shift 2 ;;
71+
--seed-n) SEED_N="$2"; shift 2 ;;
72+
--rpc) RPC="$2"; shift 2 ;;
73+
*) echo "unknown arg: $1" >&2; exit 2 ;;
74+
esac
75+
done
76+
77+
# Expected populated tx count = seeded txs + 1 coinbase.
78+
EXPECT_NTX=$((SEED_N + 1))
79+
80+
# ── the four-criteria assert — the heart of the harness ──────────────────────
81+
# $1 = c2pool startup/connect LOG, $2 = `getblock <hash> 1` JSON of the won
82+
# block. Returns 0 iff ALL FOUR criteria hold. Pure grep over the real
83+
# LOG_INFO lines emitted by btc/coin/p2p_node.hpp + a python tx-count check on
84+
# the getblock JSON. Identical logic in --dry-run and --go so the dry-run
85+
# faithfully proves what the live path checks.
86+
assert_populated_redrive() {
87+
local log="$1" blockjson="$2" expect_ntx="$3" ok=1
88+
echo "[four-criteria POPULATED re-drive assert]"
89+
90+
# 1 + 2: the BIP 35 request actually fired AND the peer had NODE_BLOOM.
91+
# "Sent BIP 35 mempool request" is logged ONLY when m_request_mempool_on_connect
92+
# is set (the #566 fix) AND the peer advertised NODE_BLOOM. Its presence
93+
# proves criteria 1 and 2 together; the "Skipped" line MUST be absent.
94+
if grep -qF "Sent BIP 35 mempool request" "$log"; then
95+
echo " PASS #566 fired: BIP 35 mempool request SENT on connect (criterion 2)"
96+
else
97+
echo " FAIL no 'Sent BIP 35 mempool request' — fix did not fire (criterion 2)"; ok=0
98+
fi
99+
if grep -qF "Skipped BIP 35 mempool request" "$log"; then
100+
echo " FAIL peer lacked NODE_BLOOM — request skipped; launch bitcoind with -peerbloomfilters=1 (criterion 1)"; ok=0
101+
else
102+
echo " PASS peer advertised NODE_BLOOM, request not skipped (criterion 1)"
103+
fi
104+
105+
# 3 + 4: the won block CONNECTED and is POPULATED (nTx > 1).
106+
python3 - "$blockjson" "$expect_ntx" <<'PY'
107+
import json, sys
108+
blk = json.load(open(sys.argv[1])); expect = int(sys.argv[2])
109+
conf = blk.get("confirmations", -1)
110+
tx = blk.get("tx", [])
111+
ntx = len(tx)
112+
ok = True
113+
def chk(p, msg):
114+
print((" PASS " if p else " FAIL ") + msg); return p
115+
ok &= chk(conf >= 1, "won block CONNECTED (confirmations %d >= 1) (criterion 3)" % conf)
116+
ok &= chk(ntx > 1, "block is POPULATED: nTx=%d > 1 (coinbase-only=FAIL); expected %d (criterion 4)" % (ntx, expect))
117+
sys.exit(0 if ok else 1)
118+
PY
119+
[ $? -eq 0 ] || ok=0
120+
121+
[ "$ok" = 1 ]
122+
}
123+
124+
# ── DRY-RUN: self-contained proof, no host/binary/deploy ─────────────────────
125+
if [ "$MODE" = "dry" ]; then
126+
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
127+
128+
# Golden GOOD connect log — #566 fired, peer had NODE_BLOOM.
129+
cat > "$tmp/good.log" <<EOF
130+
[BTC] c2pool-btc starting — net=regtest
131+
[BTC] bitcoind P2P: ${BITCOIND}
132+
[BTC] start_p2p — enable_mempool_request() armed (BIP 35)
133+
[BTC] Peer handshake complete services=0x409
134+
[BTC] Sent BIP 35 mempool request (peer has NODE_BLOOM)
135+
[BTC] mempool: added 5 txs from peer announcement
136+
EOF
137+
# GOOD won block — connected, populated (5 seeded + coinbase = 6).
138+
python3 - "$tmp/good.json" "$EXPECT_NTX" <<'PY'
139+
import json, sys
140+
n = int(sys.argv[2])
141+
json.dump({"confirmations": 1, "tx": ["cb"] + ["t%d" % i for i in range(n - 1)]}, open(sys.argv[1], "w"))
142+
PY
143+
144+
# BAD log A — peer lacked NODE_BLOOM (forgot -peerbloomfilters=1): request skipped.
145+
cat > "$tmp/bad_skipped.log" <<EOF
146+
[BTC] start_p2p — enable_mempool_request() armed (BIP 35)
147+
[BTC] Peer handshake complete services=0x401
148+
[BTC] Skipped BIP 35 mempool request — peer lacks NODE_BLOOM (0x401), would cause disconnect
149+
EOF
150+
# BAD log B — fix never fired (stale pre-#566 binary): no request line at all.
151+
cat > "$tmp/bad_nofire.log" <<EOF
152+
[BTC] start_p2p complete
153+
[BTC] Peer handshake complete services=0x409
154+
EOF
155+
# BAD block A — un-connected/orphan.
156+
printf '%s\n' '{"confirmations":-1,"tx":["cb","t1","t2","t3","t4","t5"]}' > "$tmp/bad_unconnected.json"
157+
# BAD block B — coinbase-only (the residual itself).
158+
printf '%s\n' '{"confirmations":1,"tx":["cb"]}' > "$tmp/bad_coinbaseonly.json"
159+
160+
echo "== G3b POPULATED re-drive (#566) — DRY-RUN (self-contained four-criteria proof) =="
161+
echo "seed_n=${SEED_N} expect_ntx=${EXPECT_NTX} bitcoind=${BITCOIND}"
162+
echo
163+
echo "[1/5] golden GOOD log + POPULATED connected block -> MUST pass"
164+
if assert_populated_redrive "$tmp/good.log" "$tmp/good.json" "$EXPECT_NTX"; then
165+
echo " => PASS (as required)"
166+
else
167+
echo " => UNEXPECTED FAIL"; exit 1
168+
fi
169+
echo
170+
echo "[2/5] NODE_BLOOM skipped (no -peerbloomfilters=1) -> MUST bite (criterion 1)"
171+
if assert_populated_redrive "$tmp/bad_skipped.log" "$tmp/good.json" "$EXPECT_NTX" >/dev/null 2>&1; then
172+
echo " => UNEXPECTED PASS — does not bite, FAIL"; exit 1
173+
else echo " => correctly REJECTED"; fi
174+
echo
175+
echo "[3/5] fix never fired (stale pre-#566 binary) -> MUST bite (criterion 2)"
176+
if assert_populated_redrive "$tmp/bad_nofire.log" "$tmp/good.json" "$EXPECT_NTX" >/dev/null 2>&1; then
177+
echo " => UNEXPECTED PASS — does not bite, FAIL"; exit 1
178+
else echo " => correctly REJECTED"; fi
179+
echo
180+
echo "[4/5] un-connected/orphan won block -> MUST bite (criterion 3)"
181+
if assert_populated_redrive "$tmp/good.log" "$tmp/bad_unconnected.json" "$EXPECT_NTX" >/dev/null 2>&1; then
182+
echo " => UNEXPECTED PASS — does not bite, FAIL"; exit 1
183+
else echo " => correctly REJECTED"; fi
184+
echo
185+
echo "[5/5] coinbase-only block (the residual, nTx=1) -> MUST bite (criterion 4)"
186+
if assert_populated_redrive "$tmp/good.log" "$tmp/bad_coinbaseonly.json" "$EXPECT_NTX" >/dev/null 2>&1; then
187+
echo " => UNEXPECTED PASS — does not bite, FAIL"; exit 1
188+
else echo " => correctly REJECTED"; fi
189+
echo
190+
echo "DRY-RUN PASS: four-criteria assert passes on a fired+NODE_BLOOM+connected+populated"
191+
echo "re-drive and bites on each failure shape (skipped / not-fired / un-connected / coinbase-only)."
192+
echo "The live --go re-drive is DEPLOY-GATED (operator card) and needs a #566 REBUILD on .121."
193+
exit 0
194+
fi
195+
196+
# ── LIVE: operator-tapped deploy card required ───────────────────────────────
197+
if [ "$MODE" != "go" ]; then
198+
echo "refusing to run: pass --dry-run (self-contained) or --go (operator deploy-card approved)" >&2
199+
exit 2
200+
fi
201+
[ -n "$HOST" ] || { echo "--go requires --host H" >&2; exit 2; }
202+
[ -n "$BIN" ] || { echo "--go requires --bin /path/c2pool-btc (a #566 REBUILD, not the stale deployed binary)" >&2; exit 2; }
203+
204+
cat <<EOF
205+
================================================================================
206+
G3b POPULATED RE-DRIVE (#566) — LIVE (--go)
207+
host=${HOST} bin=${BIN} bitcoind=${BITCOIND} stratum=${STRATUM}
208+
seed_n=${SEED_N} expect_ntx=${EXPECT_NTX}
209+
*** DEPLOY + RESTART op. Requires an operator-tapped deploy card. The binary
210+
MUST contain #566 (a REBUILD) — re-driving a stale binary falsely fails. ***
211+
================================================================================
212+
EOF
213+
214+
# 1. Confirm bitcoind advertises NODE_BLOOM (the hard prereq). getnetworkinfo
215+
# localservicesnames must include NETWORK_LIMITED/BLOOM; we assert BLOOM.
216+
echo "[prereq] confirming bitcoind advertises NODE_BLOOM (-peerbloomfilters=1)"
217+
if ! ssh "$HOST" "${RPC} getnetworkinfo" | grep -qi "BLOOM"; then
218+
echo " HARD STOP: bitcoind does NOT advertise NODE_BLOOM. Relaunch it with" >&2
219+
echo " -peerbloomfilters=1 before re-driving (else BIP 35 pull is a no-op)." >&2
220+
exit 1
221+
fi
222+
echo " NODE_BLOOM advertised — criterion 1 prereq satisfied."
223+
224+
# 2. Seed N txs into the mempool BEFORE c2pool connects; do NOT generate after.
225+
echo "[seed] injecting ${SEED_N} resident unconfirmed txs (NO generate after)"
226+
ssh "$HOST" "for i in \$(seq 1 ${SEED_N}); do \
227+
addr=\$(${RPC} getnewaddress); \
228+
${RPC} sendtoaddress \$addr 0.01 >/dev/null; \
229+
done; echo mempool_size=\$(${RPC} getmempoolinfo | grep -o '\"size\": *[0-9]*')"
230+
231+
# 3. Launch c2pool-btc AFTER the mempool is seeded (so the txs are resident at
232+
# connect and reachable only via the BIP 35 pull).
233+
echo "[launch] starting c2pool-btc (post-seed)"
234+
ssh "$HOST" "pkill -f 'c2pool-btc' || true; \
235+
nohup ${BIN} --regtest --bitcoind ${BITCOIND} --stratum ${STRATUM} \
236+
> /tmp/c2pool_g3b_redrive.log 2>&1 & echo launched pid=\$!"
237+
238+
echo "[wait] ${SETTLE_SECS}s for handshake + BIP 35 pull..."
239+
ssh "$HOST" "for i in \$(seq 1 ${SETTLE_SECS}); do \
240+
grep -qE 'Sent BIP 35 mempool request|Skipped BIP 35 mempool request' /tmp/c2pool_g3b_redrive.log && break; \
241+
sleep 1; done"
242+
243+
echo
244+
echo "Connect log captured to /tmp/c2pool_g3b_redrive.log on ${HOST}."
245+
echo "BLOCK-FOUND is RIG-GATED (#387/#388 bitaxe or an RPC-tuned solve). Once a"
246+
echo "won block crosses target, capture its hash and run the assert:"
247+
echo
248+
echo " log=\$(ssh ${HOST} 'cat /tmp/c2pool_g3b_redrive.log')"
249+
echo " h=\$(ssh ${HOST} '${RPC} getbestblockhash')"
250+
echo " blk=\$(ssh ${HOST} \"${RPC} getblock \$h 1\")"
251+
echo " # then: assert_populated_redrive <log> <blockjson> ${EXPECT_NTX}"
252+
echo
253+
echo "PASS = all four criteria green. Report [s=done] + the /tmp/c2pool_g3b_redrive.log"
254+
echo "evidence to integrator for the deploy-card ARTIFACT. Coinbase-only (nTx=1) = FAIL."

0 commit comments

Comments
 (0)