Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog.d/warnet-lab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Fixed

- The Warnet kind image tag is the release `28.0.0` (not an rbitcoin
version). Helm `semverCompare ">=0.17.0"` is true for that tag, so the
chart writes `[regtest]` before the tank's RPC and `addnode` lines.
`0.7.99` and any prerelease of it compare below `0.17.0` and omit the
section. The compose example stays `rbitcoin-warnet:local`.
- Lab tanks copy node log lines to stdout when `RBITCOIN_LOG_STDOUT=1`,
and the RPC proxy accepts Warnet's fork-observer `rpcauth` login for
its whitelisted chain methods while the tank `rpcpassword` user stays
unrestricted. A tank whose conf omits `head_scale` uses tiny heads
inside the lab image; `head_scale=mainnet` still selects mainnet heads.
43 changes: 32 additions & 11 deletions docs/core-functional.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,30 @@ python3 scripts/core-functional/create_cache.py --ensure

## Warnet lab (all-rbitcoin tanks)

Not an operator musl Release. Image tag `rbitcoin-warnet:local`. Warnet
stays Core Helm (`bitcoin.conf`, `rpcuser`/`rpcpassword`, `addnode=tank-N`,
`pidof bitcoind`). The node is not taught Core conf; the Python shim
translates `addnode=` to `--connect host:18444`, seeds `{datadir}/rpc.token`
from `rpcpassword`, and binds the test proxy on `rpcbind=0.0.0.0`. Basic
auth accepts any username whose password matches that token.
Not an operator musl Release. The compose example keeps image tag
`rbitcoin-warnet:local`. Kind and Helm use `rbitcoin-warnet:28.0.0`.
That tag is the chart's Core-version gate, not the rbitcoin version.
Warnet passes it to `semverCompare ">=0.17.0"` and, when the compare
succeeds, writes `[regtest]` before `rpcport`, `rpcpassword`, `rpcauth`,
and `addnode`. A prerelease does not count: Warnet main strips `-…` and
then compares, and `0.7.99` is below `0.17.0`, so the section is omitted
while `helm template` still exits 0. `local` is not a semantic version
(Warnet 1.1.20 compares the raw tag; current Warnet main normalizes a few
Core shapes and still rejects that fallback). A release tag `>= 0.17.0`
with no prerelease, such as `28.0.0`, emits the section on both.
Warnet stays Core Helm (`bitcoin.conf`, `rpcuser`/`rpcpassword`,
`addnode=tank-N`, `pidof bitcoind`). The node is not taught Core conf; the
Python shim translates `addnode=` to `--connect host:18444`, seeds
`{datadir}/rpc.token` from `rpcpassword`, and binds the test proxy on
`rpcbind=0.0.0.0`. Basic auth accepts the cookie, any username whose
password matches that token, and Core `rpcauth=` (HMAC-SHA256, salt as
the key). `rpcwhitelistdefault=0` limits only users named in
`rpcwhitelist=`, so fork-observer's published login can call
`getchaintips`, `getblockheader`, `getblockhash`, `getblock`, and
`getnetworkinfo`, and the tank `rpcuser` is not locked out. The image
sets `RBITCOIN_LOG_STDOUT=1` (node lines on container stdout and in
`debug.log`) and `RBITCOIN_LAB_HEAD_SCALE=tiny` when conf omits
`head_scale`. Conf `head_scale=mainnet` still selects mainnet heads.

Hostname `--connect` / `addnode add` resolve at each dial on a blocking
thread and retry every 2 seconds until a live session exists. `--connect`
Expand All @@ -322,8 +340,9 @@ cargo build -p rbitcoin-node
./scripts/core-functional/warnet/example.sh
```

The example conf sets `head_scale=tiny` so a tank does not fallocate mainnet
heads. `example.sh` copies a **bookworm-linked** `rbitcoin-node` into the image
The example conf sets `head_scale=tiny`. The lab image does the same when
conf omits it, so a Helm tank does not fallocate mainnet heads. `example.sh`
copies a **bookworm-linked** `rbitcoin-node` into the image
(a nix devshell binary will not exec: its dynamic loader is not in Debian).
It starts `tank1` first (`addnode=tank0`), waits until that RPC is up at
height 0 with no peer, then starts `tank0`, mines one regtest block, and
Expand All @@ -339,17 +358,19 @@ Needs Docker + kind on an operator host. Do not open mainnet. Wallet keys
are RAM-only.

```bash
docker build -t rbitcoin-warnet:local \
docker build -t rbitcoin-warnet:28.0.0 \
--build-arg NODE_BIN=target/dev/debug/rbitcoin-node \
-f scripts/core-functional/warnet/Dockerfile .
kind load docker-image rbitcoin-warnet:local
kind load docker-image rbitcoin-warnet:28.0.0
python3 -m venv .venv && source .venv/bin/activate
pip install warnet
warnet setup
warnet new /tmp/rbtc-warnet
```

Three tanks, ring `addnode`, unique `rpcpassword`, `pullPolicy: Never`.
Three tanks, ring `addnode`, unique `rpcpassword`. Set
`image.repository` to `rbitcoin-warnet`, `image.tag` to `28.0.0`,
and `pullPolicy: Never`.
Pass when `miner_std.py --interval=10 --mature` leaves all three
`getblockcount` values equal and greater than 0. Fail classes: CrashLoop
`pidof`; 401 on RPC; height only on the miner (DNS or bind); ImagePullBackOff
Expand Down
42 changes: 37 additions & 5 deletions scripts/core-functional/bitcoind
Original file line number Diff line number Diff line change
Expand Up @@ -678,15 +678,24 @@ def translate(
if log_level:
cmd += ["--log-level", log_level]
cmd += extra_node_args
for scale in read_conf_keys(datadir / "bitcoin.conf", "head_scale"):
if scale == "tiny" or scale == "mainnet":
cmd += ["--head-scale", scale]
for host in read_conf_keys(datadir / "bitcoin.conf", "addnode") + addnodes:
if not host:
continue
if ":" not in host:
host = f"{host}:{p2pport}"
cmd += ["--connect", host]
scales = [
s for s in read_conf_keys(datadir / "bitcoin.conf", "head_scale") if s
]
# Lab entrypoint sets RBITCOIN_LAB_HEAD_SCALE=tiny. An explicit conf
# value wins, including mainnet. Unset env leaves the node default.
if not scales:
lab_scale = os.environ.get("RBITCOIN_LAB_HEAD_SCALE", "").strip()
if lab_scale == "tiny":
scales = ["tiny"]
for scale in scales:
if scale == "tiny" or scale == "mainnet":
cmd += ["--head-scale", scale]

pw = (conf.get("rpcpassword") or "").strip()
if pw:
Expand Down Expand Up @@ -751,6 +760,14 @@ def main(argv: list[str] | None = None) -> int:
# TestNode tails this process's stderr and requires it empty on a clean
# stop. Core writes logs to debug.log (printtoconsole=0). Keep that shape:
# node stdio → {datadir}/debug.log; only `Error:` lines on shim stderr.
# The lab image sets RBITCOIN_LOG_STDOUT=1 so kubectl / warnet logs see
# the same bytes. Functional tests leave the switch off.
log_stdout = os.environ.get("RBITCOIN_LOG_STDOUT", "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
log_path = node_dir / "debug.log"
prior = log_path.stat().st_size if log_path.is_file() else 0
log_f = open(log_path, "ab", buffering=0)
Expand Down Expand Up @@ -782,6 +799,12 @@ def main(argv: list[str] | None = None) -> int:
for mapped in apply_line(text, rules):
log_f.write((mapped + "\n").encode())
log_f.flush()
if log_stdout and raw:
try:
sys.stdout.buffer.write(raw)
sys.stdout.buffer.flush()
except OSError:
pass

pump = threading.Thread(target=_pump, daemon=True)
pump.start()
Expand Down Expand Up @@ -856,8 +879,17 @@ def main(argv: list[str] | None = None) -> int:

try:
core_datadir = node_dir.parent
rpcbind = read_conf(core_datadir / "bitcoin.conf").get("rpcbind") or ""
proxy = RpcProxy((proxy_bind_host(rpcbind), public_rpc), node_url, _cookie)
conf_path = core_datadir / "bitcoin.conf"
conf = read_conf(conf_path)
rpcbind = conf.get("rpcbind") or ""
proxy = RpcProxy(
(proxy_bind_host(rpcbind), public_rpc),
node_url,
_cookie,
rpcauth_lines=read_conf_keys(conf_path, "rpcauth"),
whitelist_lines=read_conf_keys(conf_path, "rpcwhitelist"),
whitelist_default=conf.get("rpcwhitelistdefault"),
)
except OSError:
if child.poll() is None:
child.terminate()
Expand Down
68 changes: 68 additions & 0 deletions scripts/core-functional/bitcoind.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,74 @@ else
FAIL=$((FAIL + 1))
fi

# Lab image: no head_scale in conf, entrypoint default selects tiny.
# Explicit head_scale=mainnet still wins. Unset lab env leaves the node default.
HS_DD="$WORKDIR/warnet-no-scale"
mkdir -p "$HS_DD"
printf 'regtest=1\nrpcpassword=secret0\nrpcbind=0.0.0.0\naddnode=tank-0001\nport=18444\nrpcport=18443\n' >"$HS_DD/bitcoin.conf"
OUT_HS="$(RBITCOIN_LAB_HEAD_SCALE=tiny "$SHIM" --print-cmd -datadir="$HS_DD" -regtest 2>/dev/null)" || OUT_HS=""
if printf '%s' "$OUT_HS" | grep -q -- "--head-scale tiny" \
&& printf '%s' "$OUT_HS" | grep -q -- "--connect tank-0001:18444" \
&& [[ "$(cat "$HS_DD/regtest/rpc.token")" == "secret0" ]]; then
echo "ok - lab default head scale is tiny when conf omits it"
PASS=$((PASS + 1))
else
echo "not ok - lab default head scale is tiny (got: $OUT_HS)"
FAIL=$((FAIL + 1))
fi
printf 'regtest=1\nhead_scale=mainnet\nport=18444\nrpcport=18443\n' >"$HS_DD/bitcoin.conf"
OUT_HM="$(RBITCOIN_LAB_HEAD_SCALE=tiny "$SHIM" --print-cmd -datadir="$HS_DD" -regtest 2>/dev/null)" || OUT_HM=""
if printf '%s' "$OUT_HM" | grep -q -- "--head-scale mainnet" \
&& ! printf '%s' "$OUT_HM" | grep -q -- "--head-scale tiny"; then
echo "ok - conf head_scale=mainnet overrides the lab default"
PASS=$((PASS + 1))
else
echo "not ok - conf head_scale=mainnet overrides the lab default (got: $OUT_HM)"
FAIL=$((FAIL + 1))
fi
# Conf still says mainnet; drop it and leave the lab env unset.
printf 'regtest=1\nport=18444\nrpcport=18443\n' >"$HS_DD/bitcoin.conf"
OUT_HU="$(env -u RBITCOIN_LAB_HEAD_SCALE "$SHIM" --print-cmd -datadir="$HS_DD" -regtest 2>/dev/null)" || OUT_HU=""
if ! printf '%s' "$OUT_HU" | grep -q -- "--head-scale"; then
echo "ok - omitted head_scale without the lab env stays the node default"
PASS=$((PASS + 1))
else
echo "not ok - omitted head_scale without the lab env (got: $OUT_HU)"
FAIL=$((FAIL + 1))
fi

# Lab log switch: node stdout is tee'd. Off, the shim stays quiet.
MARK="warnet-log-marker-$RANDOM-$$"
LOG_FAKE="$WORKDIR/rbitcoin-node-logmark"
printf '%s\n' '#!/bin/sh' "printf '%s\n' '$MARK'" 'exit 0' >"$LOG_FAKE"
chmod +x "$LOG_FAKE"
LOG_DD="$WORKDIR/log-on"
mkdir -p "$LOG_DD"
printf 'regtest=1\nport=18444\nrpcport=18443\n' >"$LOG_DD/bitcoin.conf"
LOG_OUT="$(RBITCOIN_NODE="$LOG_FAKE" RBITCOIN_LOG_STDOUT=1 "$SHIM" -datadir="$LOG_DD" -regtest 2>"$WORKDIR/log-on.err")" || true
if printf '%s\n' "$LOG_OUT" | grep -qx -- "$MARK" \
&& ! grep -q -- "$MARK" "$WORKDIR/log-on.err" \
&& grep -qx -- "$MARK" "$LOG_DD/regtest/debug.log"; then
echo "ok - RBITCOIN_LOG_STDOUT=1 tees the node line to stdout and debug.log"
PASS=$((PASS + 1))
else
echo "not ok - log stdout on (out=$(printf %q "$LOG_OUT") err=$(cat "$WORKDIR/log-on.err") log=$(cat "$LOG_DD/regtest/debug.log" 2>/dev/null))"
FAIL=$((FAIL + 1))
fi
LOG_DD2="$WORKDIR/log-off"
mkdir -p "$LOG_DD2"
printf 'regtest=1\nport=18444\nrpcport=18443\n' >"$LOG_DD2/bitcoin.conf"
LOG_OUT2="$(RBITCOIN_NODE="$LOG_FAKE" env -u RBITCOIN_LOG_STDOUT "$SHIM" -datadir="$LOG_DD2" -regtest 2>"$WORKDIR/log-off.err")" || true
if ! printf '%s\n' "$LOG_OUT2" | grep -q -- "$MARK" \
&& ! grep -q -- "$MARK" "$WORKDIR/log-off.err" \
&& grep -qx -- "$MARK" "$LOG_DD2/regtest/debug.log"; then
echo "ok - log switch off keeps the node line in debug.log only"
PASS=$((PASS + 1))
else
echo "not ok - log stdout off (out=$(printf %q "$LOG_OUT2") err=$(cat "$WORKDIR/log-off.err") log=$(cat "$LOG_DD2/regtest/debug.log" 2>/dev/null))"
FAIL=$((FAIL + 1))
fi

assert_fail_msg "port 65536 invalid" "Error: Invalid port specified in -port: '65536'" \
env RBITCOIN_NODE="$FAKE" "$SHIM" --print-cmd -datadir="$DATADIR" -regtest -listen -port=65536
assert_fail_msg "port 0 invalid" "Error: Invalid port specified in -port: '0'" \
Expand Down
Loading
Loading