Skip to content

Commit 2d819fa

Browse files
committed
Merge bitcoin/bitcoin#29032: signet: omit commitment for some trivial challenges
6ee32aa test: signet tool genpsbt and solvepsbt commands (Sjors Provoost) 0a99d99 signet: miner skips PSBT step for OP_TRUE (Sjors Provoost) cdfb70e signet: split decode_psbt miner helper (Sjors Provoost) Pull request description: [BIP325](https://github.com/bitcoin/bips/blob/master/bip-0325.mediawiki) mentions the following rule: > In the special case where an empty solution is valid (ie scriptSig and scriptWitness are both empty) this additional commitment can optionally be left out. This special case is to allow non-signet-aware block generation code to be used to test a custom signet chain where the challenge is trivially true. Such a signet can be created using e.g. `-signetchallenge=51` (`OP_TRUE`). However `contrib/signet/miner` won't omit the commitment. This PR improves the miner by skipping the PSBT for known trivial scripts (just `OP_TRUE` and trivial pushes for now). This prevents it from appending the 4 byte signet header to the witness commitment, as allowed by the above rule. --- Previously the script would fail with `PSBT signing failed`, making it difficult to mine. This is no longer the case. ACKs for top commit: achow101: ACK 6ee32aa theStack: re-ACK 6ee32aa danielabrozzoni: ACK 6ee32aa Tree-SHA512: e47fbf471f2909286a6c1c073799ea388b9c19551afcce96cf9af45cc48d25c02f1e48e08861a88b604361e2c107a759d5baf393da8a37360de419f31651758a
2 parents f999c37 + 6ee32aa commit 2d819fa

File tree

4 files changed

+160
-33
lines changed

4 files changed

+160
-33
lines changed

contrib/signet/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ These steps can instead be done explicitly:
8080

8181
This is intended to allow you to replace part of the pipeline for further experimentation (eg, to sign the block with a hardware wallet).
8282

83+
For custom signets with a trivial challenge such as `OP_TRUE` and `OP_2` the walletprocesspsbt step can be skipped.

contrib/signet/miner

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__fi
1818
PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
1919
sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
2020

21-
from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height # noqa: E402
21+
from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height, SIGNET_HEADER # noqa: E402
2222
from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, from_binary, from_hex, ser_string, ser_uint256, tx_from_hex, MAX_SEQUENCE_NONFINAL # noqa: E402
2323
from test_framework.psbt import PSBT, PSBTMap, PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_SIGHASH_TYPE # noqa: E402
2424
from test_framework.script import CScript, CScriptOp # noqa: E402
@@ -28,7 +28,6 @@ logging.basicConfig(
2828
level=logging.INFO,
2929
datefmt='%Y-%m-%d %H:%M:%S')
3030

31-
SIGNET_HEADER = b"\xec\xc7\xda\xa2"
3231
PSBT_SIGNET_BLOCK = b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed
3332
RE_MULTIMINER = re.compile(r"^(\d+)(-(\d+))?/(\d+)$")
3433

@@ -66,22 +65,31 @@ def signet_txs(block, challenge):
6665

6766
return spend, to_spend
6867

69-
def decode_psbt(b64psbt):
68+
def decode_challenge_psbt(b64psbt):
7069
psbt = PSBT.from_base64(b64psbt)
7170

7271
assert len(psbt.tx.vin) == 1
7372
assert len(psbt.tx.vout) == 1
7473
assert PSBT_SIGNET_BLOCK in psbt.g.map
74+
return psbt
7575

76+
def get_block_from_psbt(psbt):
77+
return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK])
78+
79+
def get_solution_from_psbt(psbt, emptyok=False):
7680
scriptSig = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTSIG, b"")
7781
scriptWitness = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS, b"\x00")
78-
79-
return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
82+
if emptyok and len(scriptSig) == 0 and scriptWitness == b"\x00":
83+
return None
84+
return ser_string(scriptSig) + scriptWitness
8085

8186
def finish_block(block, signet_solution, grind_cmd):
82-
block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
83-
block.vtx[0].rehash()
84-
block.hashMerkleRoot = block.calc_merkle_root()
87+
if signet_solution is None:
88+
pass # Don't need to add a signet commitment if there's no signet signature needed
89+
else:
90+
block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
91+
block.vtx[0].rehash()
92+
block.hashMerkleRoot = block.calc_merkle_root()
8593
if grind_cmd is None:
8694
block.solve()
8795
else:
@@ -93,10 +101,7 @@ def finish_block(block, signet_solution, grind_cmd):
93101
block.rehash()
94102
return block
95103

96-
def generate_psbt(tmpl, reward_spk, *, blocktime=None, poolid=None):
97-
signet_spk = tmpl["signet_challenge"]
98-
signet_spk_bin = bytes.fromhex(signet_spk)
99-
104+
def new_block(tmpl, reward_spk, *, blocktime=None, poolid=None):
100105
scriptSig = script_BIP34_coinbase_height(tmpl["height"])
101106
if poolid is not None:
102107
scriptSig = CScript(b"" + scriptSig + CScriptOp.encode_op_pushdata(poolid))
@@ -125,8 +130,14 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None, poolid=None):
125130
block.vtx[0].wit.vtxinwit = [cbwit]
126131
block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce))))
127132

128-
signme, spendme = signet_txs(block, signet_spk_bin)
133+
block.vtx[0].rehash()
134+
block.hashMerkleRoot = block.calc_merkle_root()
135+
136+
return block
129137

138+
def generate_psbt(block, signet_spk):
139+
signet_spk_bin = bytes.fromhex(signet_spk)
140+
signme, spendme = signet_txs(block, signet_spk_bin)
130141
psbt = PSBT()
131142
psbt.g = PSBTMap( {PSBT_GLOBAL_UNSIGNED_TX: signme.serialize(),
132143
PSBT_SIGNET_BLOCK: block.serialize()
@@ -175,12 +186,16 @@ def get_reward_addr_spk(args, height):
175186
def do_genpsbt(args):
176187
poolid = get_poolid(args)
177188
tmpl = json.load(sys.stdin)
189+
signet_spk = tmpl["signet_challenge"]
178190
_, reward_spk = get_reward_addr_spk(args, tmpl["height"])
179-
psbt = generate_psbt(tmpl, reward_spk, poolid=poolid)
191+
block = new_block(tmpl, reward_spk, poolid=poolid)
192+
psbt = generate_psbt(block, signet_spk)
180193
print(psbt)
181194

182195
def do_solvepsbt(args):
183-
block, signet_solution = decode_psbt(sys.stdin.read())
196+
psbt = decode_challenge_psbt(sys.stdin.read())
197+
block = get_block_from_psbt(psbt)
198+
signet_solution = get_solution_from_psbt(psbt, emptyok=True)
184199
block = finish_block(block, signet_solution, args.grind_cmd)
185200
print(block.serialize().hex())
186201

@@ -223,6 +238,21 @@ def seconds_to_hms(s):
223238
out = "-" + out
224239
return out
225240

241+
def trivial_challenge(spkhex):
242+
"""
243+
BIP325 allows omitting the signet commitment when scriptSig and
244+
scriptWitness are both empty. This is the case for trivial
245+
challenges such as OP_TRUE or a single data push.
246+
"""
247+
spk = bytes.fromhex(spkhex)
248+
if len(spk) == 1 and 0x51 <= spk[0] <= 0x60:
249+
# OP_TRUE/OP_1...OP_16
250+
return True
251+
elif 2 <= len(spk) <= 76 and spk[0] + 1 == len(spk):
252+
# Single fixed push of 1-75 bytes
253+
return True
254+
return False
255+
226256
class Generate:
227257
INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
228258

@@ -323,14 +353,22 @@ class Generate:
323353
return tmpl
324354

325355
def mine(self, bcli, grind_cmd, tmpl, reward_spk):
326-
psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time, poolid=self.poolid)
327-
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
328-
psbt_signed = json.loads(bcli("-stdin", "walletprocesspsbt", input=input_stream))
329-
if not psbt_signed.get("complete",False):
330-
logging.debug("Generated PSBT: %s" % (psbt,))
331-
sys.stderr.write("PSBT signing failed\n")
332-
return None
333-
block, signet_solution = decode_psbt(psbt_signed["psbt"])
356+
block = new_block(tmpl, reward_spk, blocktime=self.mine_time, poolid=self.poolid)
357+
358+
signet_spk = tmpl["signet_challenge"]
359+
if trivial_challenge(signet_spk):
360+
signet_solution = None
361+
else:
362+
psbt = generate_psbt(block, signet_spk)
363+
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
364+
psbt_signed = json.loads(bcli("-stdin", "walletprocesspsbt", input=input_stream))
365+
if not psbt_signed.get("complete",False):
366+
logging.debug("Generated PSBT: %s" % (psbt,))
367+
sys.stderr.write("PSBT signing failed\n")
368+
return None
369+
psbt = decode_challenge_psbt(psbt_signed["psbt"])
370+
signet_solution = get_solution_from_psbt(psbt)
371+
334372
return finish_block(block, signet_solution, grind_cmd)
335373

336374
def do_generate(args):

test/functional/test_framework/blocktools.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@
8181
DIFF_4_TARGET = int(DIFF_1_TARGET / 4)
8282
assert_equal(uint256_from_compact(DIFF_4_N_BITS), DIFF_4_TARGET)
8383

84+
# From BIP325
85+
SIGNET_HEADER = b"\xec\xc7\xda\xa2"
86+
8487
def nbits_str(nbits):
8588
return f"{nbits:08x}"
8689

test/functional/tool_signet_miner.py

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test signet miner tool"""
66

7+
import json
78
import os.path
89
import shlex
910
import subprocess
1011
import sys
1112
import time
1213

13-
from test_framework.blocktools import DIFF_1_N_BITS
14+
from test_framework.blocktools import DIFF_1_N_BITS, SIGNET_HEADER
1415
from test_framework.key import ECKey
15-
from test_framework.script_util import key_to_p2wpkh_script
16+
from test_framework.script_util import CScript, key_to_p2wpkh_script
1617
from test_framework.test_framework import BitcoinTestFramework
1718
from test_framework.util import (
1819
assert_equal,
@@ -23,31 +24,49 @@
2324

2425
CHALLENGE_PRIVATE_KEY = (42).to_bytes(32, 'big')
2526

27+
def get_segwit_commitment(node):
28+
coinbase = node.getblock(node.getbestblockhash(), 2)['tx'][0]
29+
commitment = coinbase['vout'][1]['scriptPubKey']['hex']
30+
assert_equal(commitment[0:12], '6a24aa21a9ed')
31+
return commitment
32+
33+
def get_signet_commitment(segwit_commitment):
34+
for el in CScript.fromhex(segwit_commitment):
35+
if isinstance(el, bytes) and el[0:4] == SIGNET_HEADER:
36+
return el[4:].hex()
37+
return None
2638

2739
class SignetMinerTest(BitcoinTestFramework):
2840
def set_test_params(self):
2941
self.chain = "signet"
3042
self.setup_clean_chain = True
31-
self.num_nodes = 1
43+
self.num_nodes = 4
3244

3345
# generate and specify signet challenge (simple p2wpkh script)
3446
privkey = ECKey()
3547
privkey.set(CHALLENGE_PRIVATE_KEY, True)
3648
pubkey = privkey.get_pubkey().get_bytes()
3749
challenge = key_to_p2wpkh_script(pubkey)
38-
self.extra_args = [[f'-signetchallenge={challenge.hex()}']]
50+
51+
self.extra_args = [
52+
[f'-signetchallenge={challenge.hex()}'],
53+
["-signetchallenge=51"], # OP_TRUE
54+
["-signetchallenge=60"], # OP_16
55+
["-signetchallenge=202cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"], # sha256("hello")
56+
]
3957

4058
def skip_test_if_missing_module(self):
4159
self.skip_if_no_cli()
4260
self.skip_if_no_wallet()
4361
self.skip_if_no_bitcoin_util()
4462

45-
def run_test(self):
46-
node = self.nodes[0]
47-
# import private key needed for signing block
48-
wallet_importprivkey(node, bytes_to_wif(CHALLENGE_PRIVATE_KEY), "now")
63+
def setup_network(self):
64+
self.setup_nodes()
65+
# Nodes with different signet networks are not connected
4966

50-
# generate block with signet miner tool
67+
# generate block with signet miner tool
68+
def mine_block(self, node):
69+
n_blocks = node.getblockcount()
5170
base_dir = self.config["environment"]["SRCDIR"]
5271
signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
5372
rpc_argv = node.binaries.rpc_argv() + [f"-datadir={node.cli.datadir}"]
@@ -63,7 +82,73 @@ def run_test(self):
6382
f'--set-block-time={int(time.time())}',
6483
'--poolnum=99',
6584
], check=True, stderr=subprocess.STDOUT)
66-
assert_equal(node.getblockcount(), 1)
85+
assert_equal(node.getblockcount(), n_blocks + 1)
86+
87+
# generate block using the signet miner tool genpsbt and solvepsbt commands
88+
def mine_block_manual(self, node, *, sign):
89+
n_blocks = node.getblockcount()
90+
base_dir = self.config["environment"]["SRCDIR"]
91+
signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
92+
rpc_argv = node.binaries.rpc_argv() + [f"-datadir={node.cli.datadir}"]
93+
util_argv = node.binaries.util_argv() + ["grind"]
94+
base_cmd = [
95+
sys.executable,
96+
signet_miner_path,
97+
f'--cli={shlex.join(rpc_argv)}',
98+
]
99+
100+
template = node.getblocktemplate(dict(rules=["signet","segwit"]))
101+
genpsbt = subprocess.run(base_cmd + [
102+
'genpsbt',
103+
f'--address={node.getnewaddress()}',
104+
'--poolnum=98',
105+
], check=True, input=json.dumps(template).encode('utf8'), capture_output=True)
106+
psbt = genpsbt.stdout.decode('utf8').strip()
107+
if sign:
108+
self.log.debug("Sign the PSBT")
109+
res = node.walletprocesspsbt(psbt=psbt, sign=True, sighashtype='ALL')
110+
assert res['complete']
111+
psbt = res['psbt']
112+
solvepsbt = subprocess.run(base_cmd + [
113+
'solvepsbt',
114+
f'--grind-cmd={shlex.join(util_argv)}',
115+
], check=True, input=psbt.encode('utf8'), capture_output=True)
116+
node.submitblock(solvepsbt.stdout.decode('utf8').strip())
117+
assert_equal(node.getblockcount(), n_blocks + 1)
118+
119+
def run_test(self):
120+
self.log.info("Signet node with single signature challenge")
121+
node = self.nodes[0]
122+
# import private key needed for signing block
123+
wallet_importprivkey(node, bytes_to_wif(CHALLENGE_PRIVATE_KEY), 0)
124+
self.mine_block(node)
125+
# MUST include signet commitment
126+
assert get_signet_commitment(get_segwit_commitment(node))
127+
128+
self.log.info("Mine manually using genpsbt and solvepsbt")
129+
self.mine_block_manual(node, sign=True)
130+
assert get_signet_commitment(get_segwit_commitment(node))
131+
132+
node = self.nodes[1]
133+
self.log.info("Signet node with trivial challenge (OP_TRUE)")
134+
self.mine_block(node)
135+
# MAY omit signet commitment (BIP 325). Do so for better compatibility
136+
# with signet unaware mining software and hardware.
137+
assert get_signet_commitment(get_segwit_commitment(node)) is None
138+
139+
node = self.nodes[2]
140+
self.log.info("Signet node with trivial challenge (OP_16)")
141+
self.mine_block(node)
142+
assert get_signet_commitment(get_segwit_commitment(node)) is None
143+
144+
node = self.nodes[3]
145+
self.log.info("Signet node with trivial challenge (push sha256 hash)")
146+
self.mine_block(node)
147+
assert get_signet_commitment(get_segwit_commitment(node)) is None
148+
149+
self.log.info("Manual mining with a trivial challenge doesn't require a PSBT")
150+
self.mine_block_manual(node, sign=False)
151+
assert get_signet_commitment(get_segwit_commitment(node)) is None
67152

68153

69154
if __name__ == "__main__":

0 commit comments

Comments
 (0)