1 #!/usr/bin/env python3
2 # Copyright (c) 2020-present The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6 import argparse
7 import json
8 import logging
9 import math
10 import os
11 import re
12 import shlex
13 import sys
14 import time
15 import subprocess
16
17 PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
18 PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
19 sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
20
21 from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height, SIGNET_HEADER # noqa: E402
22 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
23 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
24 from test_framework.script import CScript, CScriptOp # noqa: E402
25
26 logging.basicConfig(
27 format='%(asctime)s %(levelname)s %(message)s',
28 level=logging.INFO,
29 datefmt='%Y-%m-%d %H:%M:%S')
30
31 PSBT_SIGNET_BLOCK = b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed
32 RE_MULTIMINER = re.compile(r"^(\d+)(-(\d+))?/(\d+)$")
33
34 def signet_txs(block, challenge):
35 # assumes signet solution has not been added yet so does not need
36 # to be removed
37
38 txs = block.vtx[:]
39 txs[0] = CTransaction(txs[0])
40 txs[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER)
41 hashes = []
42 for tx in txs:
43 hashes.append(ser_uint256(tx.txid_int))
44 mroot = block.get_merkle_root(hashes)
45
46 sd = b""
47 sd += block.nVersion.to_bytes(4, "little", signed=True)
48 sd += ser_uint256(block.hashPrevBlock)
49 sd += ser_uint256(mroot)
50 sd += block.nTime.to_bytes(4, "little")
51
52 to_spend = CTransaction()
53 to_spend.version = 0
54 to_spend.nLockTime = 0
55 to_spend.vin = [CTxIn(COutPoint(0, 0xFFFFFFFF), b"\x00" + CScriptOp.encode_op_pushdata(sd), 0)]
56 to_spend.vout = [CTxOut(0, challenge)]
57
58 spend = CTransaction()
59 spend.version = 0
60 spend.nLockTime = 0
61 spend.vin = [CTxIn(COutPoint(to_spend.txid_int, 0), b"", 0)]
62 spend.vout = [CTxOut(0, b"\x6a")]
63
64 return spend, to_spend
65
66 def decode_challenge_psbt(b64psbt):
67 psbt = PSBT.from_base64(b64psbt)
68
69 assert len(psbt.i) == 1
70 assert len(psbt.o) == 1
71 assert PSBT_SIGNET_BLOCK in psbt.g.map
72 return psbt
73
74 def get_block_from_psbt(psbt):
75 return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK])
76
77 def get_solution_from_psbt(psbt, emptyok=False):
78 scriptSig = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTSIG, b"")
79 scriptWitness = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS, b"\x00")
80 if emptyok and len(scriptSig) == 0 and scriptWitness == b"\x00":
81 return None
82 return ser_string(scriptSig) + scriptWitness
83
84 def finish_block(block, signet_solution, grind_cmd):
85 if signet_solution is None:
86 pass # Don't need to add a signet commitment if there's no signet signature needed
87 else:
88 block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
89 block.hashMerkleRoot = block.calc_merkle_root()
90 if grind_cmd is None:
91 block.solve()
92 else:
93 headhex = CBlockHeader.serialize(block).hex()
94 cmd = shlex.split(grind_cmd) + [headhex]
95 newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
96 newhead = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
97 block.nNonce = newhead.nNonce
98 return block
99
100 def new_block(tmpl, reward_spk, *, blocktime=None, poolid=None):
101 scriptSig = script_BIP34_coinbase_height(tmpl["height"])
102 if poolid is not None:
103 scriptSig = CScript(b"" + scriptSig + CScriptOp.encode_op_pushdata(poolid))
104
105 cbtx = CTransaction()
106 cbtx.nLockTime = tmpl["height"] - 1
107 cbtx.vin = [CTxIn(COutPoint(0, 0xffffffff), scriptSig, MAX_SEQUENCE_NONFINAL)]
108 cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)]
109 cbtx.vin[0].nSequence = 2**32-2
110
111 block = CBlock()
112 block.nVersion = tmpl["version"]
113 block.hashPrevBlock = int(tmpl["previousblockhash"], 16)
114 block.nTime = tmpl["curtime"] if blocktime is None else blocktime
115 if block.nTime < tmpl["mintime"]:
116 block.nTime = tmpl["mintime"]
117 block.nBits = int(tmpl["bits"], 16)
118 block.nNonce = 0
119 block.vtx = [cbtx] + [tx_from_hex(t["data"]) for t in tmpl["transactions"]]
120
121 witnonce = 0
122 witroot = block.calc_witness_merkle_root()
123 cbwit = CTxInWitness()
124 cbwit.scriptWitness.stack = [ser_uint256(witnonce)]
125 block.vtx[0].wit.vtxinwit = [cbwit]
126 block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce))))
127
128 block.hashMerkleRoot = block.calc_merkle_root()
129
130 return block
131
132 def generate_psbt(block, signet_spk):
133 signet_spk_bin = bytes.fromhex(signet_spk)
134 signme, spendme = signet_txs(block, signet_spk_bin)
135 psbt = PSBT()
136 psbt.g = PSBTMap( {PSBT_GLOBAL_UNSIGNED_TX: signme.serialize(),
137 PSBT_SIGNET_BLOCK: block.serialize()
138 } )
139 psbt.i = [ PSBTMap( {PSBT_IN_NON_WITNESS_UTXO: spendme.serialize(),
140 PSBT_IN_SIGHASH_TYPE: bytes([1,0,0,0])})
141 ]
142 psbt.o = [ PSBTMap() ]
143 return psbt.to_base64()
144
145 def get_poolid(args):
146 if args.poolid is not None:
147 return args.poolid.encode('utf8')
148 elif args.poolnum is not None:
149 return b"/signet:%d/" % (args.poolnum)
150 else:
151 return None
152
153 def get_reward_addr_spk(args, height):
154 assert args.address is not None or args.descriptor is not None
155
156 if hasattr(args, "reward_spk"):
157 return args.address, args.reward_spk
158
159 if args.address is not None:
160 reward_addr = args.address
161 elif '*' not in args.descriptor:
162 reward_addr = args.address = json.loads(args.bcli("deriveaddresses", args.descriptor))[0]
163 else:
164 remove = [k for k in args.derived_addresses.keys() if k+20 <= height]
165 for k in remove:
166 del args.derived_addresses[k]
167 if height not in args.derived_addresses:
168 addrs = json.loads(args.bcli("deriveaddresses", args.descriptor, "[%d,%d]" % (height, height+20)))
169 for k, a in enumerate(addrs):
170 args.derived_addresses[height+k] = a
171 reward_addr = args.derived_addresses[height]
172
173 reward_spk = bytes.fromhex(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"])
174 if args.address is not None:
175 # will always be the same, so cache
176 args.reward_spk = reward_spk
177
178 return reward_addr, reward_spk
179
180 def do_genpsbt(args):
181 poolid = get_poolid(args)
182 tmpl = json.load(sys.stdin)
183 signet_spk = tmpl["signet_challenge"]
184 _, reward_spk = get_reward_addr_spk(args, tmpl["height"])
185 block = new_block(tmpl, reward_spk, poolid=poolid)
186 psbt = generate_psbt(block, signet_spk)
187 print(psbt)
188
189 def do_solvepsbt(args):
190 psbt = decode_challenge_psbt(sys.stdin.read())
191 block = get_block_from_psbt(psbt)
192 signet_solution = get_solution_from_psbt(psbt, emptyok=True)
193 block = finish_block(block, signet_solution, args.grind_cmd)
194 print(block.serialize().hex())
195
196 def nbits_to_target(nbits):
197 shift = (nbits >> 24) & 0xff
198 return (nbits & 0x00ffffff) * 2**(8*(shift - 3))
199
200 def target_to_nbits(target):
201 tstr = "{0:x}".format(target)
202 if len(tstr) < 6:
203 tstr = ("000000"+tstr)[-6:]
204 if len(tstr) % 2 != 0:
205 tstr = "0" + tstr
206 if int(tstr[0],16) >= 0x8:
207 # avoid "negative"
208 tstr = "00" + tstr
209 fix = int(tstr[:6], 16)
210 sz = len(tstr)//2
211 if tstr[6:] != "0"*(sz*2-6):
212 fix += 1
213
214 return int("%02x%06x" % (sz,fix), 16)
215
216 def seconds_to_hms(s):
217 if s == 0:
218 return "0s"
219 neg = (s < 0)
220 if neg:
221 s = -s
222 out = ""
223 if s % 60 > 0:
224 out = "%ds" % (s % 60)
225 s //= 60
226 if s % 60 > 0:
227 out = "%dm%s" % (s % 60, out)
228 s //= 60
229 if s > 0:
230 out = "%dh%s" % (s, out)
231 if neg:
232 out = "-" + out
233 return out
234
235 def trivial_challenge(spkhex):
236 """
237 BIP325 allows omitting the signet commitment when scriptSig and
238 scriptWitness are both empty. This is the case for trivial
239 challenges such as OP_TRUE or a single data push.
240 """
241 spk = bytes.fromhex(spkhex)
242 if len(spk) == 1 and 0x51 <= spk[0] <= 0x60:
243 # OP_TRUE/OP_1...OP_16
244 return True
245 elif 2 <= len(spk) <= 76 and spk[0] + 1 == len(spk):
246 # Single fixed push of 1-75 bytes
247 return True
248 return False
249
250 class Generate:
251 INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
252
253
254 def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800,
255 standby_delay=0, backup_delay=0, set_block_time=None,
256 poolid=None):
257 if multiminer is None:
258 multiminer = (0, 1, 1)
259 (self.multi_low, self.multi_high, self.multi_period) = multiminer
260 self.ultimate_target = ultimate_target
261 self.poisson = poisson
262 self.max_interval = max_interval
263 self.standby_delay = standby_delay
264 self.backup_delay = backup_delay
265 self.set_block_time = set_block_time
266 self.poolid = poolid
267
268 def next_block_delta(self, last_nbits, last_hash):
269 # strategy:
270 # 1) work out how far off our desired target we are
271 # 2) cap it to a factor of 4 since that's the best we can do in a single retarget period
272 # 3) use that to work out the desired average interval in this retarget period
273 # 4) if doing poisson, use the last hash to pick a uniformly random number in [0,1), and work out a random multiplier to vary the average by
274 # 5) cap the resulting interval between 1 second and 1 hour to avoid extremes
275
276 current_target = nbits_to_target(last_nbits)
277 retarget_factor = self.ultimate_target / current_target
278 retarget_factor = max(0.25, min(retarget_factor, 4.0))
279
280 avg_interval = self.INTERVAL * retarget_factor
281
282 if self.poisson:
283 det_rand = int(last_hash[-8:], 16) * 2**-32
284 this_interval_variance = -math.log1p(-det_rand)
285 else:
286 this_interval_variance = 1
287
288 this_interval = avg_interval * this_interval_variance
289 this_interval = max(1, min(this_interval, self.max_interval))
290
291 return this_interval
292
293 def next_block_is_mine(self, last_hash):
294 det_rand = int(last_hash[-16:-8], 16)
295 return self.multi_low <= (det_rand % self.multi_period) < self.multi_high
296
297 def next_block_time(self, now, bestheader, is_first_block):
298 if self.set_block_time is not None:
299 logging.debug("Setting start time to %d", self.set_block_time)
300 self.mine_time = self.set_block_time
301 self.action_time = now
302 self.is_mine = True
303 elif bestheader["height"] == 0:
304 time_delta = self.INTERVAL * 100 # plenty of time to mine 100 blocks
305 logging.info("Backdating time for first block to %d minutes ago" % (time_delta/60))
306 self.mine_time = now - time_delta
307 self.action_time = now
308 self.is_mine = True
309 else:
310 time_delta = self.next_block_delta(int(bestheader["bits"], 16), bestheader["hash"])
311 self.mine_time = bestheader["time"] + time_delta
312
313 self.is_mine = self.next_block_is_mine(bestheader["hash"])
314
315 self.action_time = self.mine_time
316 if not self.is_mine:
317 self.action_time += self.backup_delay
318
319 if self.standby_delay > 0:
320 self.action_time += self.standby_delay
321 elif is_first_block:
322 # for non-standby, always mine immediately on startup,
323 # even if the next block shouldn't be ours
324 self.action_time = now
325
326 # don't want fractional times so round down
327 self.mine_time = int(self.mine_time)
328 self.action_time = int(self.action_time)
329
330 # can't mine a block 2h in the future; 1h55m for some safety
331 self.action_time = max(self.action_time, self.mine_time - 6900)
332
333 def gbt(self, bcli, bestblockhash, now):
334 tmpl = json.loads(bcli("getblocktemplate", '{"rules":["signet","segwit"]}'))
335 if tmpl["previousblockhash"] != bestblockhash:
336 logging.warning("GBT based off unexpected block (%s not %s), retrying", tmpl["previousblockhash"], bestblockhash)
337 time.sleep(1)
338 return None
339
340 if tmpl["mintime"] > self.mine_time:
341 logging.info("Updating block time from %d to %d", self.mine_time, tmpl["mintime"])
342 self.mine_time = tmpl["mintime"]
343 if self.mine_time > now:
344 logging.error("GBT mintime is in the future: %d is %d seconds later than %d", self.mine_time, (self.mine_time-now), now)
345 return None
346
347 return tmpl
348
349 def mine(self, bcli, grind_cmd, tmpl, reward_spk):
350 block = new_block(tmpl, reward_spk, blocktime=self.mine_time, poolid=self.poolid)
351
352 signet_spk = tmpl["signet_challenge"]
353 if trivial_challenge(signet_spk):
354 signet_solution = None
355 else:
356 psbt = generate_psbt(block, signet_spk)
357 input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
358 psbt_signed = json.loads(bcli("-stdin", "walletprocesspsbt", input=input_stream))
359 if not psbt_signed.get("complete",False):
360 logging.debug("Generated PSBT: %s" % (psbt,))
361 sys.stderr.write("PSBT signing failed\n")
362 return None
363 psbt = decode_challenge_psbt(psbt_signed["psbt"])
364 signet_solution = get_solution_from_psbt(psbt)
365
366 return finish_block(block, signet_solution, grind_cmd)
367
368 def do_generate(args):
369 if args.set_block_time is not None:
370 max_blocks = 1
371 elif args.max_blocks is not None:
372 if args.max_blocks < 1:
373 logging.error("--max_blocks must specify a positive integer")
374 return 1
375 max_blocks = args.max_blocks
376 elif args.ongoing:
377 max_blocks = None
378 else:
379 max_blocks = 1
380
381 if args.set_block_time is not None and args.set_block_time < 0:
382 args.set_block_time = time.time()
383 logging.info("Treating negative block time as current time (%d)" % (args.set_block_time))
384
385 if args.min_nbits:
386 args.nbits = "1e0377ae"
387 logging.info("Using nbits=%s" % (args.nbits))
388
389 if args.set_block_time is None:
390 if args.nbits is None or len(args.nbits) != 8:
391 logging.error("Must specify --nbits (use calibrate command to determine value)")
392 return 1
393
394 if args.multiminer is None:
395 my_blocks = (0,1,1)
396 else:
397 if not args.ongoing:
398 logging.error("Cannot specify --multiminer without --ongoing")
399 return 1
400 m = RE_MULTIMINER.match(args.multiminer)
401 if m is None:
402 logging.error("--multiminer argument must be k/m or j-k/m")
403 return 1
404 start,_,stop,total = m.groups()
405 if stop is None:
406 stop = start
407 start, stop, total = map(int, (start, stop, total))
408 if stop < start or start <= 0 or total < stop or total == 0:
409 logging.error("Inconsistent values for --multiminer")
410 return 1
411 my_blocks = (start-1, stop, total)
412
413 if args.max_interval < 960:
414 logging.error("--max-interval must be at least 960 (16 minutes)")
415 return 1
416
417 poolid = get_poolid(args)
418
419 ultimate_target = nbits_to_target(int(args.nbits,16))
420
421 gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval,
422 standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid)
423
424 mined_blocks = 0
425 bestheader = {"hash": None}
426 lastheader = None
427 while max_blocks is None or mined_blocks < max_blocks:
428
429 # current status?
430 bci = json.loads(args.bcli("getblockchaininfo"))
431
432 if bestheader["hash"] != bci["bestblockhash"]:
433 bestheader = json.loads(args.bcli("getblockheader", bci["bestblockhash"]))
434
435 if lastheader is None:
436 lastheader = bestheader["hash"]
437 elif bestheader["hash"] != lastheader:
438 next_delta = gen.next_block_delta(int(bestheader["bits"], 16), bestheader["hash"])
439 next_delta += bestheader["time"] - time.time()
440 next_is_mine = gen.next_block_is_mine(bestheader["hash"])
441 logging.info("Received new block at height %d; next in %s (%s)", bestheader["height"], seconds_to_hms(next_delta), ("mine" if next_is_mine else "backup"))
442 lastheader = bestheader["hash"]
443
444 # when is the next block due to be mined?
445 now = time.time()
446 gen.next_block_time(now, bestheader, (mined_blocks == 0))
447
448 # ready to go? otherwise sleep and check for new block
449 if now < gen.action_time:
450 sleep_for = min(gen.action_time - now, 60)
451 if gen.mine_time < now:
452 # someone else might have mined the block,
453 # so check frequently, so we don't end up late
454 # mining the next block if it's ours
455 sleep_for = min(20, sleep_for)
456 minestr = "mine" if gen.is_mine else "backup"
457 logging.debug("Sleeping for %s, next block due in %s (%s)" % (seconds_to_hms(sleep_for), seconds_to_hms(gen.mine_time - now), minestr))
458 time.sleep(sleep_for)
459 continue
460
461 # gbt
462 tmpl = gen.gbt(args.bcli, bci["bestblockhash"], now)
463 if tmpl is None:
464 continue
465
466 logging.debug("GBT template: %s", tmpl)
467
468 # address for reward
469 reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"])
470
471 # mine block
472 logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(gen.mine_time-bestheader["time"]), gen.mine_time, gen.is_mine)
473 mined_blocks += 1
474 block = gen.mine(args.bcli, args.grind_cmd, tmpl, reward_spk)
475 if block is None:
476 return 1
477
478 # submit block
479 r = args.bcli("-stdin", "submitblock", input=block.serialize().hex().encode('utf8'))
480
481 # report
482 bstr = "block" if gen.is_mine else "backup block"
483
484 next_delta = gen.next_block_delta(block.nBits, block.hash_hex)
485 next_delta += block.nTime - time.time()
486 next_is_mine = gen.next_block_is_mine(block.hash_hex)
487
488 logging.debug("Block hash %s payout to %s", block.hash_hex, reward_addr)
489 logging.info("Mined %s at height %d; next in %s (%s)", bstr, tmpl["height"], seconds_to_hms(next_delta), ("mine" if next_is_mine else "backup"))
490 if r != "":
491 logging.warning("submitblock returned %s for height %d hash %s", r, tmpl["height"], block.hash_hex)
492 lastheader = block.hash_hex
493
494 def do_calibrate(args):
495 if args.nbits is not None and args.seconds is not None:
496 sys.stderr.write("Can only specify one of --nbits or --seconds\n")
497 return 1
498 if args.nbits is not None and len(args.nbits) != 8:
499 sys.stderr.write("Must specify 8 hex digits for --nbits\n")
500 return 1
501
502 TRIALS = 600 # gets variance down pretty low
503 TRIAL_BITS = 0x1e3ea75f # takes about 5m to do 600 trials
504
505 header = CBlockHeader()
506 header.nBits = TRIAL_BITS
507 targ = nbits_to_target(header.nBits)
508
509 start = time.time()
510 count = 0
511 for i in range(TRIALS):
512 header.nTime = i
513 header.nNonce = 0
514 headhex = header.serialize().hex()
515 cmd = shlex.split(args.grind_cmd) + [headhex]
516 newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
517
518 avg = (time.time() - start) * 1.0 / TRIALS
519
520 if args.nbits is not None:
521 want_targ = nbits_to_target(int(args.nbits,16))
522 want_time = avg*targ/want_targ
523 else:
524 want_time = args.seconds if args.seconds is not None else 25
525 want_targ = int(targ*(avg/want_time))
526
527 print("nbits=%08x for %ds average mining time" % (target_to_nbits(want_targ), want_time))
528 return 0
529
530 def bitcoin_cli(basecmd, args, **kwargs):
531 cmd = basecmd + ["-signet"] + args
532 logging.debug("Calling bitcoin-cli: %r", cmd)
533 out = subprocess.run(cmd, stdout=subprocess.PIPE, **kwargs, check=True).stdout
534 if isinstance(out, bytes):
535 out = out.decode('utf8')
536 return out.strip()
537
538 def main():
539 parser = argparse.ArgumentParser()
540 parser.add_argument("--cli", default="bitcoin-cli", type=str, help="bitcoin-cli command")
541 parser.add_argument("--debug", action="store_true", help="Print debugging info")
542 parser.add_argument("--quiet", action="store_true", help="Only print warnings/errors")
543
544 cmds = parser.add_subparsers(help="sub-commands")
545 genpsbt = cmds.add_parser("genpsbt", help="Generate a block PSBT for signing")
546 genpsbt.set_defaults(fn=do_genpsbt)
547
548 solvepsbt = cmds.add_parser("solvepsbt", help="Solve a signed block PSBT")
549 solvepsbt.set_defaults(fn=do_solvepsbt)
550
551 generate = cmds.add_parser("generate", help="Mine blocks")
552 generate.set_defaults(fn=do_generate)
553 howmany = generate.add_mutually_exclusive_group()
554 howmany.add_argument("--ongoing", action="store_true", help="Keep mining blocks")
555 howmany.add_argument("--max-blocks", default=None, type=int, help="Max blocks to mine (default=1)")
556 howmany.add_argument("--set-block-time", default=None, type=int, help="Set block time (unix timestamp); implies --max-blocks=1")
557 nbit_target = generate.add_mutually_exclusive_group()
558 nbit_target.add_argument("--nbits", default=None, type=str, help="Target nBits (specify difficulty)")
559 nbit_target.add_argument("--min-nbits", action="store_true", help="Target minimum nBits (use min difficulty)")
560 generate.add_argument("--poisson", action="store_true", help="Simulate randomised block times")
561 generate.add_argument("--multiminer", default=None, type=str, help="Specify which set of blocks to mine (eg: 1-40/100 for the first 40%%, 2/3 for the second 3rd)")
562 generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
563 generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")
564 generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)")
565
566 calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
567 calibrate.set_defaults(fn=do_calibrate)
568 calibrate_by = calibrate.add_mutually_exclusive_group()
569 calibrate_by.add_argument("--nbits", type=str, default=None)
570 calibrate_by.add_argument("--seconds", type=int, default=None)
571
572 for sp in [genpsbt, generate]:
573 payto = sp.add_mutually_exclusive_group(required=True)
574 payto.add_argument("--address", default=None, type=str, help="Address for block reward payment")
575 payto.add_argument("--descriptor", default=None, type=str, help="Descriptor for block reward payment")
576 pool = sp.add_mutually_exclusive_group()
577 pool.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine")
578 pool.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)")
579
580 for sp in [solvepsbt, generate, calibrate]:
581 sp.add_argument("--grind-cmd", default=None, type=str, required=(sp==calibrate), help="Command to grind a block header for proof-of-work")
582
583 args = parser.parse_args(sys.argv[1:])
584
585 args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(shlex.split(args.cli), list(a), input=input, **kwargs)
586
587 if hasattr(args, "address") and hasattr(args, "descriptor"):
588 args.derived_addresses = {}
589
590 if args.debug:
591 logging.getLogger().setLevel(logging.DEBUG)
592 elif args.quiet:
593 logging.getLogger().setLevel(logging.WARNING)
594 else:
595 logging.getLogger().setLevel(logging.INFO)
596
597 if hasattr(args, "fn"):
598 return args.fn(args)
599 else:
600 logging.error("Must specify command")
601 return 1
602
603 if __name__ == "__main__":
604 main()
605