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