From a5f2072d3f04796430ea03dcaf7916001579a0c2 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 5 Sep 2025 15:03:40 -0400 Subject: [PATCH 1/5] add startupProbe to bitcoincore pod (default none) Like we do with LND, this will allow a network.yaml to specify a one-time wallet-creation command that will execute as soon as possible automatically when the node is ready for RPC --- resources/charts/bitcoincore/templates/pod.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/charts/bitcoincore/templates/pod.yaml b/resources/charts/bitcoincore/templates/pod.yaml index ea9679d2b..f7e154147 100644 --- a/resources/charts/bitcoincore/templates/pod.yaml +++ b/resources/charts/bitcoincore/templates/pod.yaml @@ -65,6 +65,8 @@ spec: {{- toYaml .Values.readinessProbe | nindent 8 }} tcpSocket: port: {{ index .Values.global .Values.global.chain "RPCPort" }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 8 }} resources: {{- toYaml .Values.resources | nindent 8 }} volumeMounts: From 88ae3f98dd83149c7462d2f9ea1e384c5ddfad20 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 5 Sep 2025 15:04:21 -0400 Subject: [PATCH 2/5] scenarios commander: generatetoaddress routes to signet miner --- .../charts/commander/templates/rbac.yaml | 2 +- resources/scenarios/commander.py | 173 ++++++++++++++++++ src/warnet/deploy.py | 2 +- 3 files changed, 175 insertions(+), 2 deletions(-) diff --git a/resources/charts/commander/templates/rbac.yaml b/resources/charts/commander/templates/rbac.yaml index 4348f6471..d3d62b77d 100644 --- a/resources/charts/commander/templates/rbac.yaml +++ b/resources/charts/commander/templates/rbac.yaml @@ -44,7 +44,7 @@ metadata: app.kubernetes.io/name: {{ .Chart.Name }} rules: - apiGroups: [""] - resources: ["pods", "namespaces", "configmaps", "pods/log"] + resources: ["pods", "namespaces", "configmaps", "pods/log", "pods/exec"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py index 067cd4cc1..e1ccba71c 100644 --- a/resources/scenarios/commander.py +++ b/resources/scenarios/commander.py @@ -7,15 +7,42 @@ import pathlib import random import signal +import struct import sys import tempfile import threading from time import sleep from kubernetes import client, config +from kubernetes.stream import stream from ln_framework.ln import CLN, LND, LNNode from test_framework.authproxy import AuthServiceProxy +from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height +from test_framework.messages import ( + CBlock, + CBlockHeader, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + from_binary, + from_hex, + ser_string, + ser_uint256, + tx_from_hex, +) from test_framework.p2p import NetworkThread +from test_framework.psbt import ( + PSBT, + PSBT_GLOBAL_UNSIGNED_TX, + PSBT_IN_FINAL_SCRIPTSIG, + PSBT_IN_FINAL_SCRIPTWITNESS, + PSBT_IN_NON_WITNESS_UTXO, + PSBT_IN_SIGHASH_TYPE, + PSBTMap, +) +from test_framework.script import CScriptOp from test_framework.test_framework import ( TMPDIR_PREFIX, BitcoinTestFramework, @@ -24,6 +51,11 @@ from test_framework.test_node import TestNode from test_framework.util import PortSeed, get_rpc_proxy +SIGNET_HEADER = b"\xec\xc7\xda\xa2" +PSBT_SIGNET_BLOCK = ( + b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed +) + NAMESPACE = None pods = client.V1PodList(items=[]) cmaps = client.V1ConfigMapList(items=[]) @@ -502,3 +534,144 @@ def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool ) == to_num_peers ) + + def generatetoaddress(self, generator, n, addr, sync_fun=None, **kwargs): + if generator.chain == "regtest": + blocks = generator.generatetoaddress(n, addr, invalid_call=False, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + if generator.chain == "signet": + mined_blocks = 0 + block_hashes = [] + + def bcli(method, *args, **kwargs): + return generator.__getattr__(method)(*args, **kwargs) + + while mined_blocks < n: + # gbt + tmpl = bcli("getblocktemplate", {"rules": ["signet", "segwit"]}) + # address for reward + reward_spk = bytes.fromhex(bcli("getaddressinfo", addr)["scriptPubKey"]) + # create coinbase tx + cbtx = CTransaction() + cbtx.vin = [ + CTxIn( + COutPoint(0, 0xFFFFFFFF), + script_BIP34_coinbase_height(tmpl["height"]), + 0xFFFFFFFF, + ) + ] + cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)] + cbtx.vin[0].nSequence = 2**32 - 2 + cbtx.rehash() + # assemble block + block = CBlock() + block.nVersion = tmpl["version"] + block.hashPrevBlock = int(tmpl["previousblockhash"], 16) + block.nTime = tmpl["curtime"] + if block.nTime < tmpl["mintime"]: + block.nTime = tmpl["mintime"] + block.nBits = int(tmpl["bits"], 16) + block.nNonce = 0 + block.vtx = [cbtx] + [tx_from_hex(t["data"]) for t in tmpl["transactions"]] + witnonce = 0 + witroot = block.calc_witness_merkle_root() + cbwit = CTxInWitness() + cbwit.scriptWitness.stack = [ser_uint256(witnonce)] + block.vtx[0].wit.vtxinwit = [cbwit] + block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce)))) + # create signet txs for signing + signet_spk = tmpl["signet_challenge"] + signet_spk_bin = bytes.fromhex(signet_spk) + txs = block.vtx[:] + txs[0] = CTransaction(txs[0]) + txs[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER) + hashes = [] + for tx in txs: + tx.rehash() + hashes.append(ser_uint256(tx.sha256)) + mroot = block.get_merkle_root(hashes) + sd = b"" + sd += struct.pack(" Date: Mon, 8 Sep 2025 14:32:02 -0400 Subject: [PATCH 3/5] ln_init: dont assume node[0] is the miner --- resources/scenarios/ln_init.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py index db096b6d4..59a7aeaf7 100644 --- a/resources/scenarios/ln_init.py +++ b/resources/scenarios/ln_init.py @@ -26,6 +26,12 @@ def set_test_params(self): def add_options(self, parser): parser.description = "Fund LN wallets and open channels" parser.usage = "warnet run /path/to/ln_init.py" + parser.add_argument( + "--miner", + dest="miner", + type=str, + help="Select one tank by name as the blockchain miner", + ) def run_test(self): ## @@ -38,7 +44,18 @@ def run_test(self): # MINER ## self.log.info("Setting up miner...") - miner = self.ensure_miner(self.nodes[0]) + if self.options.miner: + self.log.info(f"Parsed 'miner' argument: {self.options.miner}") + mining_tank = self.tanks[self.options.miner] + elif "miner" in self.tanks: + # or choose the tank with the right name + self.log.info("Found tank named 'miner'") + mining_tank = self.tanks["miner"] + else: + mining_tank = self.nodes[0] + self.log.info(f"Using tank {mining_tank.tank} as miner") + + miner = self.ensure_miner(mining_tank) miner_addr = miner.getnewaddress() def gen(n): From 24493a1bcb419e845bc26aabb852785d5acd868e Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 8 Sep 2025 15:28:14 -0400 Subject: [PATCH 4/5] build image: include bitcoin-util from now on --- docker-bake.hcl | 2 +- src/warnet/image_build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-bake.hcl b/docker-bake.hcl index 5e88f6372..93a51e33d 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -59,7 +59,7 @@ target "cmake-base" { inherits = ["maintained-base"] dockerfile = "./Dockerfile.dev" args = { - BUILD_ARGS = "-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON" + BUILD_ARGS = "-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON" } } diff --git a/src/warnet/image_build.py b/src/warnet/image_build.py index 6aacfb395..b71be6d34 100644 --- a/src/warnet/image_build.py +++ b/src/warnet/image_build.py @@ -23,7 +23,7 @@ def build_image( action: str, ): if not build_args: - build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "' + build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "' else: build_args = f'"{build_args}"' From c5b4a98a1e109a20ba238a5e064326181b92dfff Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 8 Sep 2025 15:49:48 -0400 Subject: [PATCH 5/5] test: cover signet miner using bitcoin-util grinder --- .../test_scenarios/signet_grinder.py | 19 +++++++++++++++ test/data/signet/network.yaml | 2 ++ test/signet_test.py | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 resources/scenarios/test_scenarios/signet_grinder.py diff --git a/resources/scenarios/test_scenarios/signet_grinder.py b/resources/scenarios/test_scenarios/signet_grinder.py new file mode 100644 index 000000000..1bee09409 --- /dev/null +++ b/resources/scenarios/test_scenarios/signet_grinder.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +from commander import Commander + + +class SignetGrinder(Commander): + def set_test_params(self): + self.num_nodes = 0 + + def run_test(self): + self.generatetoaddress(self.tanks["miner"], 1, "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch") + + +def main(): + SignetGrinder().main() + + +if __name__ == "__main__": + main() diff --git a/test/data/signet/network.yaml b/test/data/signet/network.yaml index e5e3a4fc2..630f6f626 100644 --- a/test/data/signet/network.yaml +++ b/test/data/signet/network.yaml @@ -1,5 +1,7 @@ nodes: - name: miner + image: + tag: "29.0-util" - name: tank-1 image: tag: "0.16.1" diff --git a/test/signet_test.py b/test/signet_test.py index 8f16b9a5c..424a88d7f 100755 --- a/test/signet_test.py +++ b/test/signet_test.py @@ -22,6 +22,7 @@ def run_test(self): self.setup_network() self.check_signet_miner() self.check_signet_recon() + self.check_signet_scenario_miner() finally: self.cleanup() @@ -60,6 +61,28 @@ def check_scenario_clean_exit(): self.wait_for_predicate(check_scenario_clean_exit) + def check_signet_scenario_miner(self): + before_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + + self.log.info("Generate 1 signet block from a scenario using the bitcoin-util grinder") + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + scenario_file = self.scen_dir / "test_scenarios" / "signet_grinder.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --admin") + self.wait_for_all_scenarios() + + after_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + assert after_count - before_count == 1 + + deployed = scenarios_deployed() + found = False + for sc in deployed: + if "grinder" in sc["name"]: + found = True + log = self.warnet(f"logs {sc['name']}") + assert "Error grinding" not in log + assert found + if __name__ == "__main__": test = SignetTest()