Skip to content

Commit 455222c

Browse files
authored
Merge pull request #739 from pinheadmz/signet-miner
Support signet mining in scenarios
2 parents ad2a7e6 + c5b4a98 commit 455222c

File tree

10 files changed

+241
-5
lines changed

10 files changed

+241
-5
lines changed

docker-bake.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ target "cmake-base" {
5959
inherits = ["maintained-base"]
6060
dockerfile = "./Dockerfile.dev"
6161
args = {
62-
BUILD_ARGS = "-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON"
62+
BUILD_ARGS = "-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON"
6363
}
6464
}
6565

resources/charts/bitcoincore/templates/pod.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ spec:
6565
{{- toYaml .Values.readinessProbe | nindent 8 }}
6666
tcpSocket:
6767
port: {{ index .Values.global .Values.global.chain "RPCPort" }}
68+
startupProbe:
69+
{{- toYaml .Values.startupProbe | nindent 8 }}
6870
resources:
6971
{{- toYaml .Values.resources | nindent 8 }}
7072
volumeMounts:

resources/charts/commander/templates/rbac.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ metadata:
4444
app.kubernetes.io/name: {{ .Chart.Name }}
4545
rules:
4646
- apiGroups: [""]
47-
resources: ["pods", "namespaces", "configmaps", "pods/log"]
47+
resources: ["pods", "namespaces", "configmaps", "pods/log", "pods/exec"]
4848
verbs: ["get", "list", "watch"]
4949
---
5050
apiVersion: rbac.authorization.k8s.io/v1

resources/scenarios/commander.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,42 @@
77
import pathlib
88
import random
99
import signal
10+
import struct
1011
import sys
1112
import tempfile
1213
import threading
1314
from time import sleep
1415

1516
from kubernetes import client, config
17+
from kubernetes.stream import stream
1618
from ln_framework.ln import CLN, LND, LNNode
1719
from test_framework.authproxy import AuthServiceProxy
20+
from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height
21+
from test_framework.messages import (
22+
CBlock,
23+
CBlockHeader,
24+
COutPoint,
25+
CTransaction,
26+
CTxIn,
27+
CTxInWitness,
28+
CTxOut,
29+
from_binary,
30+
from_hex,
31+
ser_string,
32+
ser_uint256,
33+
tx_from_hex,
34+
)
1835
from test_framework.p2p import NetworkThread
36+
from test_framework.psbt import (
37+
PSBT,
38+
PSBT_GLOBAL_UNSIGNED_TX,
39+
PSBT_IN_FINAL_SCRIPTSIG,
40+
PSBT_IN_FINAL_SCRIPTWITNESS,
41+
PSBT_IN_NON_WITNESS_UTXO,
42+
PSBT_IN_SIGHASH_TYPE,
43+
PSBTMap,
44+
)
45+
from test_framework.script import CScriptOp
1946
from test_framework.test_framework import (
2047
TMPDIR_PREFIX,
2148
BitcoinTestFramework,
@@ -24,6 +51,11 @@
2451
from test_framework.test_node import TestNode
2552
from test_framework.util import PortSeed, get_rpc_proxy
2653

54+
SIGNET_HEADER = b"\xec\xc7\xda\xa2"
55+
PSBT_SIGNET_BLOCK = (
56+
b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed
57+
)
58+
2759
NAMESPACE = None
2860
pods = client.V1PodList(items=[])
2961
cmaps = client.V1ConfigMapList(items=[])
@@ -502,3 +534,144 @@ def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool
502534
)
503535
== to_num_peers
504536
)
537+
538+
def generatetoaddress(self, generator, n, addr, sync_fun=None, **kwargs):
539+
if generator.chain == "regtest":
540+
blocks = generator.generatetoaddress(n, addr, invalid_call=False, **kwargs)
541+
sync_fun() if sync_fun else self.sync_all()
542+
return blocks
543+
if generator.chain == "signet":
544+
mined_blocks = 0
545+
block_hashes = []
546+
547+
def bcli(method, *args, **kwargs):
548+
return generator.__getattr__(method)(*args, **kwargs)
549+
550+
while mined_blocks < n:
551+
# gbt
552+
tmpl = bcli("getblocktemplate", {"rules": ["signet", "segwit"]})
553+
# address for reward
554+
reward_spk = bytes.fromhex(bcli("getaddressinfo", addr)["scriptPubKey"])
555+
# create coinbase tx
556+
cbtx = CTransaction()
557+
cbtx.vin = [
558+
CTxIn(
559+
COutPoint(0, 0xFFFFFFFF),
560+
script_BIP34_coinbase_height(tmpl["height"]),
561+
0xFFFFFFFF,
562+
)
563+
]
564+
cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)]
565+
cbtx.vin[0].nSequence = 2**32 - 2
566+
cbtx.rehash()
567+
# assemble block
568+
block = CBlock()
569+
block.nVersion = tmpl["version"]
570+
block.hashPrevBlock = int(tmpl["previousblockhash"], 16)
571+
block.nTime = tmpl["curtime"]
572+
if block.nTime < tmpl["mintime"]:
573+
block.nTime = tmpl["mintime"]
574+
block.nBits = int(tmpl["bits"], 16)
575+
block.nNonce = 0
576+
block.vtx = [cbtx] + [tx_from_hex(t["data"]) for t in tmpl["transactions"]]
577+
witnonce = 0
578+
witroot = block.calc_witness_merkle_root()
579+
cbwit = CTxInWitness()
580+
cbwit.scriptWitness.stack = [ser_uint256(witnonce)]
581+
block.vtx[0].wit.vtxinwit = [cbwit]
582+
block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce))))
583+
# create signet txs for signing
584+
signet_spk = tmpl["signet_challenge"]
585+
signet_spk_bin = bytes.fromhex(signet_spk)
586+
txs = block.vtx[:]
587+
txs[0] = CTransaction(txs[0])
588+
txs[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER)
589+
hashes = []
590+
for tx in txs:
591+
tx.rehash()
592+
hashes.append(ser_uint256(tx.sha256))
593+
mroot = block.get_merkle_root(hashes)
594+
sd = b""
595+
sd += struct.pack("<i", block.nVersion)
596+
sd += ser_uint256(block.hashPrevBlock)
597+
sd += ser_uint256(mroot)
598+
sd += struct.pack("<I", block.nTime)
599+
to_spend = CTransaction()
600+
to_spend.nVersion = 0
601+
to_spend.nLockTime = 0
602+
to_spend.vin = [
603+
CTxIn(COutPoint(0, 0xFFFFFFFF), b"\x00" + CScriptOp.encode_op_pushdata(sd), 0)
604+
]
605+
to_spend.vout = [CTxOut(0, signet_spk_bin)]
606+
to_spend.rehash()
607+
spend = CTransaction()
608+
spend.nVersion = 0
609+
spend.nLockTime = 0
610+
spend.vin = [CTxIn(COutPoint(to_spend.sha256, 0), b"", 0)]
611+
spend.vout = [CTxOut(0, b"\x6a")]
612+
# create PSBT for miner wallet signing
613+
psbt = PSBT()
614+
psbt.g = PSBTMap(
615+
{
616+
PSBT_GLOBAL_UNSIGNED_TX: spend.serialize(),
617+
PSBT_SIGNET_BLOCK: block.serialize(),
618+
}
619+
)
620+
psbt.i = [
621+
PSBTMap(
622+
{
623+
PSBT_IN_NON_WITNESS_UTXO: to_spend.serialize(),
624+
PSBT_IN_SIGHASH_TYPE: bytes([1, 0, 0, 0]),
625+
}
626+
)
627+
]
628+
psbt.o = [PSBTMap()]
629+
psbt = psbt.to_base64()
630+
# sign PSBT
631+
psbt_signed = bcli("walletprocesspsbt", psbt=psbt, sign=True, sighashtype="ALL")
632+
if not psbt_signed.get("complete", False):
633+
self.log.error("PSBT signing failed, aborting...")
634+
return block_hashes
635+
# decode signed PSBT
636+
signed_psbt = PSBT.from_base64(psbt_signed["psbt"])
637+
scriptSig = signed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTSIG, b"")
638+
scriptWitness = signed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS, b"\x00")
639+
signed_block = from_binary(CBlock, signed_psbt.g.map[PSBT_SIGNET_BLOCK])
640+
signet_solution = ser_string(scriptSig) + scriptWitness
641+
# finish block
642+
signed_block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(
643+
SIGNET_HEADER + signet_solution
644+
)
645+
signed_block.vtx[0].rehash()
646+
signed_block.hashMerkleRoot = signed_block.calc_merkle_root()
647+
try:
648+
headhex = CBlockHeader.serialize(signed_block).hex()
649+
cmd = ["bitcoin-util", "grind", headhex]
650+
newheadhex = stream(
651+
sclient.connect_get_namespaced_pod_exec,
652+
name=generator.tank,
653+
namespace=NAMESPACE,
654+
command=cmd,
655+
stderr=True,
656+
stdin=False,
657+
stdout=True,
658+
tty=False,
659+
)
660+
if "not found" in newheadhex:
661+
raise Exception(newheadhex)
662+
newhead = from_hex(CBlockHeader(), newheadhex.strip())
663+
signed_block.nNonce = newhead.nNonce
664+
signed_block.rehash()
665+
except Exception as e:
666+
self.log.info(
667+
f"Error grinding signet PoW with bitcoin-util in {generator.tank}: {e}".strip()
668+
)
669+
self.log.info(" re-attempting with a single python thread...")
670+
signed_block.solve()
671+
# submit block
672+
bcli("submitblock", signed_block.serialize().hex())
673+
block_hashes.append(signed_block.hash)
674+
mined_blocks += 1
675+
self.log.info(f"Generated {mined_blocks} signet blocks")
676+
677+
return block_hashes

resources/scenarios/ln_init.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def set_test_params(self):
2626
def add_options(self, parser):
2727
parser.description = "Fund LN wallets and open channels"
2828
parser.usage = "warnet run /path/to/ln_init.py"
29+
parser.add_argument(
30+
"--miner",
31+
dest="miner",
32+
type=str,
33+
help="Select one tank by name as the blockchain miner",
34+
)
2935

3036
def run_test(self):
3137
##
@@ -38,7 +44,18 @@ def run_test(self):
3844
# MINER
3945
##
4046
self.log.info("Setting up miner...")
41-
miner = self.ensure_miner(self.nodes[0])
47+
if self.options.miner:
48+
self.log.info(f"Parsed 'miner' argument: {self.options.miner}")
49+
mining_tank = self.tanks[self.options.miner]
50+
elif "miner" in self.tanks:
51+
# or choose the tank with the right name
52+
self.log.info("Found tank named 'miner'")
53+
mining_tank = self.tanks["miner"]
54+
else:
55+
mining_tank = self.nodes[0]
56+
self.log.info(f"Using tank {mining_tank.tank} as miner")
57+
58+
miner = self.ensure_miner(mining_tank)
4259
miner_addr = miner.getnewaddress()
4360

4461
def gen(n):
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env python3
2+
3+
from commander import Commander
4+
5+
6+
class SignetGrinder(Commander):
7+
def set_test_params(self):
8+
self.num_nodes = 0
9+
10+
def run_test(self):
11+
self.generatetoaddress(self.tanks["miner"], 1, "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch")
12+
13+
14+
def main():
15+
SignetGrinder().main()
16+
17+
18+
if __name__ == "__main__":
19+
main()

src/warnet/deploy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str
401401
debug=False,
402402
source_dir=SCENARIOS_DIR,
403403
additional_args=None,
404-
admin=False,
404+
admin=True,
405405
namespace=namespace,
406406
)
407407
wait_for_pod_ready(name, namespace=namespace)

src/warnet/image_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def build_image(
2323
action: str,
2424
):
2525
if not build_args:
26-
build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "'
26+
build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "'
2727
else:
2828
build_args = f'"{build_args}"'
2929

test/data/signet/network.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
nodes:
22
- name: miner
3+
image:
4+
tag: "29.0-util"
35
- name: tank-1
46
image:
57
tag: "0.16.1"

test/signet_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def run_test(self):
2222
self.setup_network()
2323
self.check_signet_miner()
2424
self.check_signet_recon()
25+
self.check_signet_scenario_miner()
2526
finally:
2627
self.cleanup()
2728

@@ -60,6 +61,28 @@ def check_scenario_clean_exit():
6061

6162
self.wait_for_predicate(check_scenario_clean_exit)
6263

64+
def check_signet_scenario_miner(self):
65+
before_count = int(self.warnet("bitcoin rpc tank-1 getblockcount"))
66+
67+
self.log.info("Generate 1 signet block from a scenario using the bitcoin-util grinder")
68+
self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios"
69+
scenario_file = self.scen_dir / "test_scenarios" / "signet_grinder.py"
70+
self.log.info(f"Running scenario from: {scenario_file}")
71+
self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --admin")
72+
self.wait_for_all_scenarios()
73+
74+
after_count = int(self.warnet("bitcoin rpc tank-1 getblockcount"))
75+
assert after_count - before_count == 1
76+
77+
deployed = scenarios_deployed()
78+
found = False
79+
for sc in deployed:
80+
if "grinder" in sc["name"]:
81+
found = True
82+
log = self.warnet(f"logs {sc['name']}")
83+
assert "Error grinding" not in log
84+
assert found
85+
6386

6487
if __name__ == "__main__":
6588
test = SignetTest()

0 commit comments

Comments
 (0)