miner raw

   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