miner raw

   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