aboutsummaryrefslogtreecommitdiff
path: root/contrib/signet/miner
blob: 012bd6cc3149075d56c2de338e05f40527a42fea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
#!/usr/bin/env python3
# Copyright (c) 2020 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

import argparse
import base64
import json
import logging
import math
import os.path
import re
import struct
import sys
import time
import subprocess

from io import BytesIO

PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)

from test_framework.blocktools import WITNESS_COMMITMENT_HEADER, script_BIP34_coinbase_height # noqa: E402
from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, from_hex, deser_string, hash256, ser_compact_size, ser_string, ser_uint256, tx_from_hex, uint256_from_str # noqa: E402
from test_framework.script import CScriptOp # noqa: E402

logging.basicConfig(
    format='%(asctime)s %(levelname)s %(message)s',
    level=logging.INFO,
    datefmt='%Y-%m-%d %H:%M:%S')

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

# #### some helpers that could go into test_framework

# like from_hex, but without the hex part
def FromBinary(cls, stream):
    """deserialize a binary stream (or bytes object) into an object"""
    # handle bytes object by turning it into a stream
    was_bytes = isinstance(stream, bytes)
    if was_bytes:
        stream = BytesIO(stream)
    obj = cls()
    obj.deserialize(stream)
    if was_bytes:
        assert len(stream.read()) == 0
    return obj

class PSBTMap:
    """Class for serializing and deserializing PSBT maps"""

    def __init__(self, map=None):
        self.map = map if map is not None else {}

    def deserialize(self, f):
        m = {}
        while True:
            k = deser_string(f)
            if len(k) == 0:
                break
            v = deser_string(f)
            if len(k) == 1:
                k = k[0]
            assert k not in m
            m[k] = v
        self.map = m

    def serialize(self):
        m = b""
        for k,v in self.map.items():
            if isinstance(k, int) and 0 <= k and k <= 255:
                k = bytes([k])
            m += ser_compact_size(len(k)) + k
            m += ser_compact_size(len(v)) + v
        m += b"\x00"
        return m

class PSBT:
    """Class for serializing and deserializing PSBTs"""

    def __init__(self):
        self.g = PSBTMap()
        self.i = []
        self.o = []
        self.tx = None

    def deserialize(self, f):
        assert f.read(5) == b"psbt\xff"
        self.g = FromBinary(PSBTMap, f)
        assert 0 in self.g.map
        self.tx = FromBinary(CTransaction, self.g.map[0])
        self.i = [FromBinary(PSBTMap, f) for _ in self.tx.vin]
        self.o = [FromBinary(PSBTMap, f) for _ in self.tx.vout]
        return self

    def serialize(self):
        assert isinstance(self.g, PSBTMap)
        assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
        assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
        assert 0 in self.g.map
        tx = FromBinary(CTransaction, self.g.map[0])
        assert len(tx.vin) == len(self.i)
        assert len(tx.vout) == len(self.o)

        psbt = [x.serialize() for x in [self.g] + self.i + self.o]
        return b"psbt\xff" + b"".join(psbt)

    def to_base64(self):
        return base64.b64encode(self.serialize()).decode("utf8")

    @classmethod
    def from_base64(cls, b64psbt):
        return FromBinary(cls, base64.b64decode(b64psbt))

# #####

def create_coinbase(height, value, spk):
    cb = CTransaction()
    cb.vin = [CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)]
    cb.vout = [CTxOut(value, spk)]
    return cb

def get_witness_script(witness_root, witness_nonce):
    commitment = uint256_from_str(hash256(ser_uint256(witness_root) + ser_uint256(witness_nonce)))
    return b"\x6a" + CScriptOp.encode_op_pushdata(WITNESS_COMMITMENT_HEADER + ser_uint256(commitment))

def signet_txs(block, challenge):
    # assumes signet solution has not been added yet so does not need
    # to be removed

    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("<i", block.nVersion)
    sd += ser_uint256(block.hashPrevBlock)
    sd += ser_uint256(mroot)
    sd += struct.pack("<I", block.nTime)

    to_spend = CTransaction()
    to_spend.nVersion = 0
    to_spend.nLockTime = 0
    to_spend.vin = [CTxIn(COutPoint(0, 0xFFFFFFFF), b"\x00" + CScriptOp.encode_op_pushdata(sd), 0)]
    to_spend.vout = [CTxOut(0, challenge)]
    to_spend.rehash()

    spend = CTransaction()
    spend.nVersion = 0
    spend.nLockTime = 0
    spend.vin = [CTxIn(COutPoint(to_spend.sha256, 0), b"", 0)]
    spend.vout = [CTxOut(0, b"\x6a")]

    return spend, to_spend

def do_createpsbt(block, signme, spendme):
    psbt = PSBT()
    psbt.g = PSBTMap( {0: signme.serialize(),
                       PSBT_SIGNET_BLOCK: block.serialize()
                     } )
    psbt.i = [ PSBTMap( {0: spendme.serialize(),
                         3: bytes([1,0,0,0])})
             ]
    psbt.o = [ PSBTMap() ]
    return psbt.to_base64()

def do_decode_psbt(b64psbt):
    psbt = PSBT.from_base64(b64psbt)

    assert len(psbt.tx.vin) == 1
    assert len(psbt.tx.vout) == 1
    assert PSBT_SIGNET_BLOCK in psbt.g.map

    scriptSig = psbt.i[0].map.get(7, b"")
    scriptWitness = psbt.i[0].map.get(8, b"\x00")

    return FromBinary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness

def finish_block(block, signet_solution, grind_cmd):
    block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
    block.vtx[0].rehash()
    block.hashMerkleRoot = block.calc_merkle_root()
    if grind_cmd is None:
        block.solve()
    else:
        headhex = CBlockHeader.serialize(block).hex()
        cmd = grind_cmd.split(" ") + [headhex]
        newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
        newhead = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
        block.nNonce = newhead.nNonce
        block.rehash()
    return block

def generate_psbt(tmpl, reward_spk, *, blocktime=None):
    signet_spk = tmpl["signet_challenge"]
    signet_spk_bin = bytes.fromhex(signet_spk)

    cbtx = create_coinbase(height=tmpl["height"], value=tmpl["coinbasevalue"], spk=reward_spk)
    cbtx.vin[0].nSequence = 2**32-2
    cbtx.rehash()

    block = CBlock()
    block.nVersion = tmpl["version"]
    block.hashPrevBlock = int(tmpl["previousblockhash"], 16)
    block.nTime = tmpl["curtime"] if blocktime is None else blocktime
    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, get_witness_script(witroot, witnonce)))

    signme, spendme = signet_txs(block, signet_spk_bin)

    return do_createpsbt(block, signme, spendme)

def get_reward_address(args, height):
    if args.address is not None:
        return args.address

    if '*' not in args.descriptor:
        addr = json.loads(args.bcli("deriveaddresses", args.descriptor))[0]
        args.address = addr
        return addr

    remove = [k for k in args.derived_addresses.keys() if k+20 <= height]
    for k in remove:
        del args.derived_addresses[k]

    addr = args.derived_addresses.get(height, None)
    if addr is None:
        addrs = json.loads(args.bcli("deriveaddresses", args.descriptor, "[%d,%d]" % (height, height+20)))
        addr = addrs[0]
        for k, a in enumerate(addrs):
            args.derived_addresses[height+k] = a

    return addr

def get_reward_addr_spk(args, height):
    assert args.address is not None or args.descriptor is not None

    if hasattr(args, "reward_spk"):
        return args.address, args.reward_spk

    reward_addr = get_reward_address(args, height)
    reward_spk = bytes.fromhex(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"])
    if args.address is not None:
        # will always be the same, so cache
        args.reward_spk = reward_spk

    return reward_addr, reward_spk

def do_genpsbt(args):
    tmpl = json.load(sys.stdin)
    _, reward_spk = get_reward_addr_spk(args, tmpl["height"])
    psbt = generate_psbt(tmpl, reward_spk)
    print(psbt)

def do_solvepsbt(args):
    block, signet_solution = do_decode_psbt(sys.stdin.read())
    block = finish_block(block, signet_solution, args.grind_cmd)
    print(block.serialize().hex())

def nbits_to_target(nbits):
    shift = (nbits >> 24) & 0xff
    return (nbits & 0x00ffffff) * 2**(8*(shift - 3))

def target_to_nbits(target):
    tstr = "{0:x}".format(target)
    if len(tstr) < 6:
        tstr = ("000000"+tstr)[-6:]
    if len(tstr) % 2 != 0:
        tstr = "0" + tstr
    if int(tstr[0],16) >= 0x8:
        # avoid "negative"
        tstr = "00" + tstr
    fix = int(tstr[:6], 16)
    sz = len(tstr)//2
    if tstr[6:] != "0"*(sz*2-6):
        fix += 1

    return int("%02x%06x" % (sz,fix), 16)

def seconds_to_hms(s):
    if s == 0:
        return "0s"
    neg = (s < 0)
    if neg:
        s = -s
    out = ""
    if s % 60 > 0:
        out = "%ds" % (s % 60)
    s //= 60
    if s % 60 > 0:
        out = "%dm%s" % (s % 60, out)
    s //= 60
    if s > 0:
        out = "%dh%s" % (s, out)
    if neg:
        out = "-" + out
    return out

def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson):
    # strategy:
    #  1) work out how far off our desired target we are
    #  2) cap it to a factor of 4 since that's the best we can do in a single retarget period
    #  3) use that to work out the desired average interval in this retarget period
    #  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
    #  5) cap the resulting interval between 1 second and 1 hour to avoid extremes

    INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug

    current_target = nbits_to_target(last_nbits)
    retarget_factor = ultimate_target / current_target
    retarget_factor = max(0.25, min(retarget_factor, 4.0))

    avg_interval = INTERVAL * retarget_factor

    if do_poisson:
        det_rand = int(last_hash[-8:], 16) * 2**-32
        this_interval_variance = -math.log1p(-det_rand)
    else:
        this_interval_variance = 1

    this_interval = avg_interval * this_interval_variance
    this_interval = max(1, min(this_interval, 3600))

    return this_interval

def next_block_is_mine(last_hash, my_blocks):
    det_rand = int(last_hash[-16:-8], 16)
    return my_blocks[0] <= (det_rand % my_blocks[2]) < my_blocks[1]

def do_generate(args):
    if args.max_blocks is not None:
        if args.ongoing:
            logging.error("Cannot specify both --ongoing and --max-blocks")
            return 1
        if args.max_blocks < 1:
            logging.error("N must be a positive integer")
            return 1
        max_blocks = args.max_blocks
    elif args.ongoing:
        max_blocks = None
    else:
        max_blocks = 1

    if args.set_block_time is not None and max_blocks != 1:
        logging.error("Cannot specify --ongoing or --max-blocks > 1 when using --set-block-time")
        return 1
    if args.set_block_time is not None and args.set_block_time < 0:
        args.set_block_time = time.time()
        logging.info("Treating negative block time as current time (%d)" % (args.set_block_time))

    if args.min_nbits:
        if args.nbits is not None:
            logging.error("Cannot specify --nbits and --min-nbits")
            return 1
        args.nbits = "1e0377ae"
        logging.info("Using nbits=%s" % (args.nbits))

    if args.set_block_time is None:
        if args.nbits is None or len(args.nbits) != 8:
            logging.error("Must specify --nbits (use calibrate command to determine value)")
            return 1

    if args.multiminer is None:
       my_blocks = (0,1,1)
    else:
        if not args.ongoing:
            logging.error("Cannot specify --multiminer without --ongoing")
            return 1
        m = RE_MULTIMINER.match(args.multiminer)
        if m is None:
            logging.error("--multiminer argument must be k/m or j-k/m")
            return 1
        start,_,stop,total = m.groups()
        if stop is None:
            stop = start
        start, stop, total = map(int, (start, stop, total))
        if stop < start or start <= 0 or total < stop or total == 0:
            logging.error("Inconsistent values for --multiminer")
            return 1
        my_blocks = (start-1, stop, total)

    ultimate_target = nbits_to_target(int(args.nbits,16))

    mined_blocks = 0
    bestheader = {"hash": None}
    lastheader = None
    while max_blocks is None or mined_blocks < max_blocks:

        # current status?
        bci = json.loads(args.bcli("getblockchaininfo"))

        if bestheader["hash"] != bci["bestblockhash"]:
            bestheader = json.loads(args.bcli("getblockheader", bci["bestblockhash"]))

        if lastheader is None:
            lastheader = bestheader["hash"]
        elif bestheader["hash"] != lastheader:
            next_delta = next_block_delta(int(bestheader["bits"], 16), bestheader["hash"], ultimate_target, args.poisson)
            next_delta += bestheader["time"] - time.time()
            next_is_mine = next_block_is_mine(bestheader["hash"], my_blocks)
            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"))
            lastheader = bestheader["hash"]

        # when is the next block due to be mined?
        now = time.time()
        if args.set_block_time is not None:
            logging.debug("Setting start time to %d", args.set_block_time)
            mine_time = args.set_block_time
            action_time = now
            is_mine = True
        elif bestheader["height"] == 0:
            time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson)
            time_delta *= 100 # 100 blocks
            logging.info("Backdating time for first block to %d minutes ago" % (time_delta/60))
            mine_time = now - time_delta
            action_time = now
            is_mine = True
        else:
            time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson)
            mine_time = bestheader["time"] + time_delta

            is_mine = next_block_is_mine(bci["bestblockhash"], my_blocks)

            action_time = mine_time
            if not is_mine:
                action_time += args.backup_delay

            if args.standby_delay > 0:
                action_time += args.standby_delay
            elif mined_blocks == 0:
                # for non-standby, always mine immediately on startup,
                # even if the next block shouldn't be ours
                action_time = now

        # don't want fractional times so round down
        mine_time = int(mine_time)
        action_time = int(action_time)

        # can't mine a block 2h in the future; 1h55m for some safety
        action_time = max(action_time, mine_time - 6900)

        # ready to go? otherwise sleep and check for new block
        if now < action_time:
            sleep_for = min(action_time - now, 60)
            if mine_time < now:
                # someone else might have mined the block,
                # so check frequently, so we don't end up late
                # mining the next block if it's ours
                sleep_for = min(20, sleep_for)
            minestr = "mine" if is_mine else "backup"
            logging.debug("Sleeping for %s, next block due in %s (%s)" % (seconds_to_hms(sleep_for), seconds_to_hms(mine_time - now), minestr))
            time.sleep(sleep_for)
            continue

        # gbt
        tmpl = json.loads(args.bcli("getblocktemplate", '{"rules":["signet","segwit"]}'))
        if tmpl["previousblockhash"] != bci["bestblockhash"]:
            logging.warning("GBT based off unexpected block (%s not %s), retrying", tmpl["previousblockhash"], bci["bestblockhash"])
            time.sleep(1)
            continue

        logging.debug("GBT template: %s", tmpl)

        if tmpl["mintime"] > mine_time:
            logging.info("Updating block time from %d to %d", mine_time, tmpl["mintime"])
            mine_time = tmpl["mintime"]
            if mine_time > now:
                logging.error("GBT mintime is in the future: %d is %d seconds later than %d", mine_time, (mine_time-now), now)
                return 1

        # address for reward
        reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"])

        # mine block
        logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(mine_time-bestheader["time"]), mine_time, is_mine)
        mined_blocks += 1
        psbt = generate_psbt(tmpl, reward_spk, blocktime=mine_time)
        psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=psbt.encode('utf8')))
        if not psbt_signed.get("complete",False):
            logging.debug("Generated PSBT: %s" % (psbt,))
            sys.stderr.write("PSBT signing failed")
            return 1
        block, signet_solution = do_decode_psbt(psbt_signed["psbt"])
        block = finish_block(block, signet_solution, args.grind_cmd)

        # submit block
        r = args.bcli("-stdin", "submitblock", input=block.serialize().hex().encode('utf8'))

        # report
        bstr = "block" if is_mine else "backup block"

        next_delta = next_block_delta(block.nBits, block.hash, ultimate_target, args.poisson)
        next_delta += block.nTime - time.time()
        next_is_mine = next_block_is_mine(block.hash, my_blocks)

        logging.debug("Block hash %s payout to %s", block.hash, reward_addr)
        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"))
        if r != "":
            logging.warning("submitblock returned %s for height %d hash %s", r, tmpl["height"], block.hash)
        lastheader = block.hash

def do_calibrate(args):
    if args.nbits is not None and args.seconds is not None:
        sys.stderr.write("Can only specify one of --nbits or --seconds\n")
        return 1
    if args.nbits is not None and len(args.nbits) != 8:
        sys.stderr.write("Must specify 8 hex digits for --nbits\n")
        return 1

    TRIALS = 600 # gets variance down pretty low
    TRIAL_BITS = 0x1e3ea75f # takes about 5m to do 600 trials

    header = CBlockHeader()
    header.nBits = TRIAL_BITS
    targ = nbits_to_target(header.nBits)

    start = time.time()
    count = 0
    for i in range(TRIALS):
        header.nTime = i
        header.nNonce = 0
        headhex = header.serialize().hex()
        cmd = args.grind_cmd.split(" ") + [headhex]
        newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()

    avg = (time.time() - start) * 1.0 / TRIALS

    if args.nbits is not None:
        want_targ = nbits_to_target(int(args.nbits,16))
        want_time = avg*targ/want_targ
    else:
        want_time = args.seconds if args.seconds is not None else 25
        want_targ = int(targ*(avg/want_time))

    print("nbits=%08x for %ds average mining time" % (target_to_nbits(want_targ), want_time))
    return 0

def bitcoin_cli(basecmd, args, **kwargs):
    cmd = basecmd + ["-signet"] + args
    logging.debug("Calling bitcoin-cli: %r", cmd)
    out = subprocess.run(cmd, stdout=subprocess.PIPE, **kwargs, check=True).stdout
    if isinstance(out, bytes):
        out = out.decode('utf8')
    return out.strip()

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--cli", default="bitcoin-cli", type=str, help="bitcoin-cli command")
    parser.add_argument("--debug", action="store_true", help="Print debugging info")
    parser.add_argument("--quiet", action="store_true", help="Only print warnings/errors")

    cmds = parser.add_subparsers(help="sub-commands")
    genpsbt = cmds.add_parser("genpsbt", help="Generate a block PSBT for signing")
    genpsbt.set_defaults(fn=do_genpsbt)

    solvepsbt = cmds.add_parser("solvepsbt", help="Solve a signed block PSBT")
    solvepsbt.set_defaults(fn=do_solvepsbt)

    generate = cmds.add_parser("generate", help="Mine blocks")
    generate.set_defaults(fn=do_generate)
    generate.add_argument("--ongoing", action="store_true", help="Keep mining blocks")
    generate.add_argument("--max-blocks", default=None, type=int, help="Max blocks to mine (default=1)")
    generate.add_argument("--set-block-time", default=None, type=int, help="Set block time (unix timestamp)")
    generate.add_argument("--nbits", default=None, type=str, help="Target nBits (specify difficulty)")
    generate.add_argument("--min-nbits", action="store_true", help="Target minimum nBits (use min difficulty)")
    generate.add_argument("--poisson", action="store_true", help="Simulate randomised block times")
    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)")
    generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
    generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")

    calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
    calibrate.set_defaults(fn=do_calibrate)
    calibrate.add_argument("--nbits", type=str, default=None)
    calibrate.add_argument("--seconds", type=int, default=None)

    for sp in [genpsbt, generate]:
        sp.add_argument("--address", default=None, type=str, help="Address for block reward payment")
        sp.add_argument("--descriptor", default=None, type=str, help="Descriptor for block reward payment")

    for sp in [solvepsbt, generate, calibrate]:
        sp.add_argument("--grind-cmd", default=None, type=str, required=(sp==calibrate), help="Command to grind a block header for proof-of-work")

    args = parser.parse_args(sys.argv[1:])

    args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(args.cli.split(" "), list(a), input=input, **kwargs)

    if hasattr(args, "address") and hasattr(args, "descriptor"):
        if args.address is None and args.descriptor is None:
            sys.stderr.write("Must specify --address or --descriptor\n")
            return 1
        elif args.address is not None and args.descriptor is not None:
            sys.stderr.write("Only specify one of --address or --descriptor\n")
            return 1
        args.derived_addresses = {}

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
    elif args.quiet:
        logging.getLogger().setLevel(logging.WARNING)
    else:
        logging.getLogger().setLevel(logging.INFO)

    if hasattr(args, "fn"):
        return args.fn(args)
    else:
        logging.error("Must specify command")
        return 1

if __name__ == "__main__":
    main()