From 09f55604a743cdcd4976fb85c2bda8e66a929079 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 11:12:51 +1000 Subject: [PATCH 01/10] signet/miner: drop get_reward_address function --- contrib/signet/miner | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 4216ada5fa633..a5976dbf38a18 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -139,35 +139,26 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None): 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) + if args.address is not None: + reward_addr = args.address + elif '*' not in args.descriptor: + reward_addr = args.address = json.loads(args.bcli("deriveaddresses", args.descriptor))[0] + else: + remove = [k for k in args.derived_addresses.keys() if k+20 <= height] + for k in remove: + del args.derived_addresses[k] + if height not in args.derived_addresses: + addrs = json.loads(args.bcli("deriveaddresses", args.descriptor, "[%d,%d]" % (height, height+20))) + for k, a in enumerate(addrs): + args.derived_addresses[height+k] = a + reward_addr = args.derived_addresses[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 From be91615324e8413526c083c51552c77db498b772 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 11:13:53 +1000 Subject: [PATCH 02/10] signet/miner: drop do_createpsbt function --- contrib/signet/miner | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index a5976dbf38a18..e7ec0a1d13ef0 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -72,17 +72,6 @@ def signet_txs(block, challenge): return spend, to_spend -def do_createpsbt(block, signme, spendme): - psbt = PSBT() - psbt.g = PSBTMap( {PSBT_GLOBAL_UNSIGNED_TX: signme.serialize(), - PSBT_SIGNET_BLOCK: block.serialize() - } ) - psbt.i = [ PSBTMap( {PSBT_IN_NON_WITNESS_UTXO: spendme.serialize(), - PSBT_IN_SIGHASH_TYPE: bytes([1,0,0,0])}) - ] - psbt.o = [ PSBTMap() ] - return psbt.to_base64() - def do_decode_psbt(b64psbt): psbt = PSBT.from_base64(b64psbt) @@ -137,7 +126,15 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None): signme, spendme = signet_txs(block, signet_spk_bin) - return do_createpsbt(block, signme, spendme) + psbt = PSBT() + psbt.g = PSBTMap( {PSBT_GLOBAL_UNSIGNED_TX: signme.serialize(), + PSBT_SIGNET_BLOCK: block.serialize() + } ) + psbt.i = [ PSBTMap( {PSBT_IN_NON_WITNESS_UTXO: spendme.serialize(), + PSBT_IN_SIGHASH_TYPE: bytes([1,0,0,0])}) + ] + psbt.o = [ PSBTMap() ] + return psbt.to_base64() def get_reward_addr_spk(args, height): assert args.address is not None or args.descriptor is not None From 517df644fc5ebcc68c0b8c0de1a360284a7ef451 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 11:16:25 +1000 Subject: [PATCH 03/10] signet/miner: drop create_coinbase function --- contrib/signet/miner | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index e7ec0a1d13ef0..84b28b6699c41 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -32,12 +32,6 @@ 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(r"^(\d+)(-(\d+))?/(\d+)$") -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 signet_txs(block, challenge): # assumes signet solution has not been added yet so does not need # to be removed @@ -103,7 +97,9 @@ 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 = CTransaction() + cbtx.vin = [CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(tmpl["height"]), 0xffffffff)] + cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)] cbtx.vin[0].nSequence = 2**32-2 cbtx.rehash() From eab0bed83f790f5993469c0d4d8726ed9318840d Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 11:18:03 +1000 Subject: [PATCH 04/10] signet/miner: rename do_decode_psbt to decode_psbt --- contrib/signet/miner | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 84b28b6699c41..a3190838f1c42 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -66,7 +66,7 @@ def signet_txs(block, challenge): return spend, to_spend -def do_decode_psbt(b64psbt): +def decode_psbt(b64psbt): psbt = PSBT.from_base64(b64psbt) assert len(psbt.tx.vin) == 1 @@ -166,7 +166,7 @@ def do_genpsbt(args): print(psbt) def do_solvepsbt(args): - block, signet_solution = do_decode_psbt(sys.stdin.read()) + block, signet_solution = decode_psbt(sys.stdin.read()) block = finish_block(block, signet_solution, args.grind_cmd) print(block.serialize().hex()) @@ -398,7 +398,7 @@ def do_generate(args): logging.debug("Generated PSBT: %s" % (psbt,)) sys.stderr.write("PSBT signing failed\n") return 1 - block, signet_solution = do_decode_psbt(psbt_signed["psbt"]) + block, signet_solution = decode_psbt(psbt_signed["psbt"]) block = finish_block(block, signet_solution, args.grind_cmd) # submit block From 855e94802e8f7138878622178e7231cf4e858d81 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 11:25:01 +1000 Subject: [PATCH 05/10] signet/miner: move next_block_* functions into new Generator class --- contrib/signet/miner | 72 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index a3190838f1c42..6aefc56ea2c69 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -209,36 +209,46 @@ def seconds_to_hms(s): out = "-" + out return out -def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson, max_interval): - # 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 - +class Generate: 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 + def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800): + if multiminer is None: + multiminer = (0, 1, 1) + (self.multi_low, self.multi_high, self.multi_period) = multiminer + self.ultimate_target = ultimate_target + self.poisson = poisson + self.max_interval = max_interval - if do_poisson: - det_rand = int(last_hash[-8:], 16) * 2**-32 - this_interval_variance = -math.log1p(-det_rand) - else: - this_interval_variance = 1 + def next_block_delta(self, last_nbits, last_hash): + # 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 - this_interval = avg_interval * this_interval_variance - this_interval = max(1, min(this_interval, max_interval)) + current_target = nbits_to_target(last_nbits) + retarget_factor = self.ultimate_target / current_target + retarget_factor = max(0.25, min(retarget_factor, 4.0)) - return this_interval + avg_interval = self.INTERVAL * retarget_factor + + if self.poisson: + det_rand = int(last_hash[-8:], 16) * 2**-32 + this_interval_variance = -math.log1p(-det_rand) + else: + this_interval_variance = 1 -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] + this_interval = avg_interval * this_interval_variance + this_interval = max(1, min(this_interval, self.max_interval)) + + return this_interval + + def next_block_is_mine(self, last_hash): + det_rand = int(last_hash[-16:-8], 16) + return self.multi_low <= (det_rand % self.multi_period) < self.multi_high def do_generate(args): if args.max_blocks is not None: @@ -298,6 +308,8 @@ def do_generate(args): ultimate_target = nbits_to_target(int(args.nbits,16)) + gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval) + mined_blocks = 0 bestheader = {"hash": None} lastheader = None @@ -312,9 +324,9 @@ def do_generate(args): 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, args.max_interval) + next_delta = gen.next_block_delta(int(bestheader["bits"], 16), bestheader["hash"]) next_delta += bestheader["time"] - time.time() - next_is_mine = next_block_is_mine(bestheader["hash"], my_blocks) + next_is_mine = gen.next_block_is_mine(bestheader["hash"]) 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"] @@ -326,17 +338,17 @@ def do_generate(args): 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, args.max_interval) + time_delta = gen.next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"]) 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, args.max_interval) + time_delta = gen.next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"]) mine_time = bestheader["time"] + time_delta - is_mine = next_block_is_mine(bci["bestblockhash"], my_blocks) + is_mine = gen.next_block_is_mine(bci["bestblockhash"]) action_time = mine_time if not is_mine: @@ -407,9 +419,9 @@ def do_generate(args): # report bstr = "block" if is_mine else "backup block" - next_delta = next_block_delta(block.nBits, block.hash, ultimate_target, args.poisson, args.max_interval) + next_delta = gen.next_block_delta(block.nBits, block.hash) next_delta += block.nTime - time.time() - next_is_mine = next_block_is_mine(block.hash, my_blocks) + next_is_mine = gen.next_block_is_mine(block.hash) 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")) From 792fdee7d720c91b83952159cd57a2cd14afacae Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 13:15:19 +1000 Subject: [PATCH 06/10] signet/miner: add Generate.next_block_time function --- contrib/signet/miner | 107 +++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 6aefc56ea2c69..f80f8aecfe070 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -213,13 +213,17 @@ class Generate: INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug - def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800): + def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800, + standby_delay=0, backup_delay=0, set_block_time=None): if multiminer is None: multiminer = (0, 1, 1) (self.multi_low, self.multi_high, self.multi_period) = multiminer self.ultimate_target = ultimate_target self.poisson = poisson self.max_interval = max_interval + self.standby_delay = standby_delay + self.backup_delay = backup_delay + self.set_block_time = set_block_time def next_block_delta(self, last_nbits, last_hash): # strategy: @@ -250,6 +254,42 @@ class Generate: det_rand = int(last_hash[-16:-8], 16) return self.multi_low <= (det_rand % self.multi_period) < self.multi_high + def next_block_time(self, now, bestheader, is_first_block): + if self.set_block_time is not None: + logging.debug("Setting start time to %d", self.set_block_time) + self.mine_time = self.set_block_time + self.action_time = now + self.is_mine = True + elif bestheader["height"] == 0: + time_delta = self.INTERVAL * 100 # plenty of time to mine 100 blocks + logging.info("Backdating time for first block to %d minutes ago" % (time_delta/60)) + self.mine_time = now - time_delta + self.action_time = now + self.is_mine = True + else: + time_delta = self.next_block_delta(int(bestheader["bits"], 16), bestheader["hash"]) + self.mine_time = bestheader["time"] + time_delta + + self.is_mine = self.next_block_is_mine(bestheader["hash"]) + + self.action_time = self.mine_time + if not self.is_mine: + self.action_time += self.backup_delay + + if self.standby_delay > 0: + self.action_time += self.standby_delay + elif is_first_block: + # for non-standby, always mine immediately on startup, + # even if the next block shouldn't be ours + self.action_time = now + + # don't want fractional times so round down + self.mine_time = int(self.mine_time) + self.action_time = int(self.action_time) + + # can't mine a block 2h in the future; 1h55m for some safety + self.action_time = max(self.action_time, self.mine_time - 6900) + def do_generate(args): if args.max_blocks is not None: if args.ongoing: @@ -308,7 +348,8 @@ def do_generate(args): ultimate_target = nbits_to_target(int(args.nbits,16)) - gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval) + gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval, + standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time) mined_blocks = 0 bestheader = {"hash": None} @@ -332,52 +373,18 @@ def do_generate(args): # 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 = gen.next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"]) - 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 = gen.next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"]) - mine_time = bestheader["time"] + time_delta - - is_mine = gen.next_block_is_mine(bci["bestblockhash"]) - - 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) + gen.next_block_time(now, bestheader, (mined_blocks == 0)) # 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: + if now < gen.action_time: + sleep_for = min(gen.action_time - now, 60) + if gen.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)) + minestr = "mine" if gen.is_mine else "backup" + logging.debug("Sleeping for %s, next block due in %s (%s)" % (seconds_to_hms(sleep_for), seconds_to_hms(gen.mine_time - now), minestr)) time.sleep(sleep_for) continue @@ -390,20 +397,20 @@ def do_generate(args): 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) + if tmpl["mintime"] > gen.mine_time: + logging.info("Updating block time from %d to %d", gen.mine_time, tmpl["mintime"]) + gen.mine_time = tmpl["mintime"] + if gen.mine_time > now: + logging.error("GBT mintime is in the future: %d is %d seconds later than %d", gen.mine_time, (gen.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) + logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(gen.mine_time-bestheader["time"]), gen.mine_time, gen.is_mine) mined_blocks += 1 - psbt = generate_psbt(tmpl, reward_spk, blocktime=mine_time) + psbt = generate_psbt(tmpl, reward_spk, blocktime=gen.mine_time) input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream)) if not psbt_signed.get("complete",False): @@ -417,7 +424,7 @@ def do_generate(args): r = args.bcli("-stdin", "submitblock", input=block.serialize().hex().encode('utf8')) # report - bstr = "block" if is_mine else "backup block" + bstr = "block" if gen.is_mine else "backup block" next_delta = gen.next_block_delta(block.nBits, block.hash) next_delta += block.nTime - time.time() From 7038de737e991bc62ded58d80e54fc3b4c7362d5 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 13:19:22 +1000 Subject: [PATCH 07/10] signet/miner: add Generate.gbt function --- contrib/signet/miner | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index f80f8aecfe070..02187476c53c4 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -290,6 +290,22 @@ class Generate: # can't mine a block 2h in the future; 1h55m for some safety self.action_time = max(self.action_time, self.mine_time - 6900) + def gbt(self, bcli, bestblockhash, now): + tmpl = json.loads(bcli("getblocktemplate", '{"rules":["signet","segwit"]}')) + if tmpl["previousblockhash"] != bestblockhash: + logging.warning("GBT based off unexpected block (%s not %s), retrying", tmpl["previousblockhash"], bci["bestblockhash"]) + time.sleep(1) + return None + + if tmpl["mintime"] > self.mine_time: + logging.info("Updating block time from %d to %d", self.mine_time, tmpl["mintime"]) + self.mine_time = tmpl["mintime"] + if self.mine_time > now: + logging.error("GBT mintime is in the future: %d is %d seconds later than %d", self.mine_time, (self.mine_time-now), now) + return None + + return tmpl + def do_generate(args): if args.max_blocks is not None: if args.ongoing: @@ -389,21 +405,12 @@ def do_generate(args): 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) + tmpl = gen.gbt(args.bcli, bci["bestblockhash"], now) + if tmpl is None: continue logging.debug("GBT template: %s", tmpl) - if tmpl["mintime"] > gen.mine_time: - logging.info("Updating block time from %d to %d", gen.mine_time, tmpl["mintime"]) - gen.mine_time = tmpl["mintime"] - if gen.mine_time > now: - logging.error("GBT mintime is in the future: %d is %d seconds later than %d", gen.mine_time, (gen.mine_time-now), now) - return 1 - # address for reward reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"]) From 14e10e7c774839bdd3360c117dd86b4b95b427fe Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 1 Oct 2022 13:24:22 +1000 Subject: [PATCH 08/10] signet/miner: add Generate.mine function --- contrib/signet/miner | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 02187476c53c4..2f7a883503a59 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -306,6 +306,17 @@ class Generate: return tmpl + def mine(self, bcli, grind_cmd, tmpl, reward_spk): + psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time) + input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') + psbt_signed = json.loads(bcli("-stdin", "walletprocesspsbt", input=input_stream)) + if not psbt_signed.get("complete",False): + logging.debug("Generated PSBT: %s" % (psbt,)) + sys.stderr.write("PSBT signing failed\n") + return None + block, signet_solution = decode_psbt(psbt_signed["psbt"]) + return finish_block(block, signet_solution, grind_cmd) + def do_generate(args): if args.max_blocks is not None: if args.ongoing: @@ -417,15 +428,9 @@ def do_generate(args): # mine block logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(gen.mine_time-bestheader["time"]), gen.mine_time, gen.is_mine) mined_blocks += 1 - psbt = generate_psbt(tmpl, reward_spk, blocktime=gen.mine_time) - input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') - psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream)) - if not psbt_signed.get("complete",False): - logging.debug("Generated PSBT: %s" % (psbt,)) - sys.stderr.write("PSBT signing failed\n") + block = gen.mine(args.bcli, args.grind_cmd, tmpl, reward_spk) + if block is None: return 1 - block, signet_solution = 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')) From 213a003dc4b06e62b50660d35c67b78432c9510e Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Wed, 6 Sep 2023 01:01:44 +1000 Subject: [PATCH 09/10] signet/miner: add support for a poolnum/poolid tag in mined blocks --- contrib/signet/miner | 38 +++++++++++++++++++++++----- test/functional/tool_signet_miner.py | 1 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 2f7a883503a59..7d9140bf82bbe 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -21,7 +21,7 @@ sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL) from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height # noqa: E402 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 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 -from test_framework.script import CScriptOp # noqa: E402 +from test_framework.script import CScript, CScriptOp # noqa: E402 logging.basicConfig( format='%(asctime)s %(levelname)s %(message)s', @@ -93,12 +93,16 @@ def finish_block(block, signet_solution, grind_cmd): block.rehash() return block -def generate_psbt(tmpl, reward_spk, *, blocktime=None): +def generate_psbt(tmpl, reward_spk, *, blocktime=None, poolid=None): signet_spk = tmpl["signet_challenge"] signet_spk_bin = bytes.fromhex(signet_spk) + scriptSig = script_BIP34_coinbase_height(tmpl["height"]) + if poolid is not None: + scriptSig = CScript(b"" + scriptSig + CScriptOp.encode_op_pushdata(poolid)) + cbtx = CTransaction() - cbtx.vin = [CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(tmpl["height"]), 0xffffffff)] + cbtx.vin = [CTxIn(COutPoint(0, 0xffffffff), scriptSig, 0xffffffff)] cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)] cbtx.vin[0].nSequence = 2**32-2 cbtx.rehash() @@ -132,6 +136,17 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None): psbt.o = [ PSBTMap() ] return psbt.to_base64() +def get_poolid(args): + if args.poolid is not None: + if args.poolnum is not None: + logging.error("Can only specify one of --poolid and --poolnum") + raise Exception("bad arguments") + return args.poolid.encode('utf8') + elif args.poolnum is not None: + return b"/signet:%d/" % (args.poolnum) + else: + return None + def get_reward_addr_spk(args, height): assert args.address is not None or args.descriptor is not None @@ -160,9 +175,10 @@ def get_reward_addr_spk(args, height): return reward_addr, reward_spk def do_genpsbt(args): + poolid = get_poolid(args) tmpl = json.load(sys.stdin) _, reward_spk = get_reward_addr_spk(args, tmpl["height"]) - psbt = generate_psbt(tmpl, reward_spk) + psbt = generate_psbt(tmpl, reward_spk, poolid=poolid) print(psbt) def do_solvepsbt(args): @@ -214,7 +230,8 @@ class Generate: def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800, - standby_delay=0, backup_delay=0, set_block_time=None): + standby_delay=0, backup_delay=0, set_block_time=None, + poolid=None): if multiminer is None: multiminer = (0, 1, 1) (self.multi_low, self.multi_high, self.multi_period) = multiminer @@ -224,6 +241,7 @@ class Generate: self.standby_delay = standby_delay self.backup_delay = backup_delay self.set_block_time = set_block_time + self.poolid = poolid def next_block_delta(self, last_nbits, last_hash): # strategy: @@ -307,7 +325,7 @@ class Generate: return tmpl def mine(self, bcli, grind_cmd, tmpl, reward_spk): - psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time) + psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time, poolid=self.poolid) input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') psbt_signed = json.loads(bcli("-stdin", "walletprocesspsbt", input=input_stream)) if not psbt_signed.get("complete",False): @@ -373,10 +391,12 @@ def do_generate(args): logging.error("--max-interval must be at least 960 (16 minutes)") return 1 + poolid = get_poolid(args) + ultimate_target = nbits_to_target(int(args.nbits,16)) gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval, - standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time) + standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid) mined_blocks = 0 bestheader = {"hash": None} @@ -501,6 +521,8 @@ def main(): 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) + genpsbt.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine") + genpsbt.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)") solvepsbt = cmds.add_parser("solvepsbt", help="Solve a signed block PSBT") solvepsbt.set_defaults(fn=do_solvepsbt) @@ -517,6 +539,8 @@ def main(): 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)") generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)") + generate.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine") + generate.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)") calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty") calibrate.set_defaults(fn=do_calibrate) diff --git a/test/functional/tool_signet_miner.py b/test/functional/tool_signet_miner.py index bdefb92ae6216..67fb5c9f94f34 100755 --- a/test/functional/tool_signet_miner.py +++ b/test/functional/tool_signet_miner.py @@ -57,6 +57,7 @@ def run_test(self): f'--grind-cmd={self.options.bitcoinutil} grind', '--nbits=1d00ffff', f'--set-block-time={int(time.time())}', + '--poolnum=99', ], check=True, stderr=subprocess.STDOUT) assert_equal(node.getblockcount(), 1) From a0aa4c7476fdf8907fbc525bfe0f1762cb71ac1d Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 10 Aug 2024 02:11:26 +1000 Subject: [PATCH 10/10] signet/miner: Use argparse exclusive groups Let argparse take care of making arguments make sense in more cases. Co-Authored-By: Ava Chow --- contrib/signet/miner | 53 +++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index 7d9140bf82bbe..3c90fe96a1d8b 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -138,9 +138,6 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None, poolid=None): def get_poolid(args): if args.poolid is not None: - if args.poolnum is not None: - logging.error("Can only specify one of --poolid and --poolnum") - raise Exception("bad arguments") return args.poolid.encode('utf8') elif args.poolnum is not None: return b"/signet:%d/" % (args.poolnum) @@ -336,12 +333,11 @@ class Generate: return finish_block(block, signet_solution, grind_cmd) 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.set_block_time is not None: + max_blocks = 1 + elif args.max_blocks is not None: if args.max_blocks < 1: - logging.error("N must be a positive integer") + logging.error("--max_blocks must specify a positive integer") return 1 max_blocks = args.max_blocks elif args.ongoing: @@ -349,17 +345,11 @@ def do_generate(args): 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)) @@ -521,35 +511,38 @@ def main(): 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) - genpsbt.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine") - genpsbt.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)") 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)") + howmany = generate.add_mutually_exclusive_group() + howmany.add_argument("--ongoing", action="store_true", help="Keep mining blocks") + howmany.add_argument("--max-blocks", default=None, type=int, help="Max blocks to mine (default=1)") + howmany.add_argument("--set-block-time", default=None, type=int, help="Set block time (unix timestamp); implies --max-blocks=1") + nbit_target = generate.add_mutually_exclusive_group() + nbit_target.add_argument("--nbits", default=None, type=str, help="Target nBits (specify difficulty)") + nbit_target.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)") generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)") - generate.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine") - generate.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)") 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) + calibrate_by = calibrate.add_mutually_exclusive_group() + calibrate_by.add_argument("--nbits", type=str, default=None) + calibrate_by.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") + payto = sp.add_mutually_exclusive_group(required=True) + payto.add_argument("--address", default=None, type=str, help="Address for block reward payment") + payto.add_argument("--descriptor", default=None, type=str, help="Descriptor for block reward payment") + pool = sp.add_mutually_exclusive_group() + pool.add_argument("--poolnum", default=None, type=int, help="Identify blocks that you mine") + pool.add_argument("--poolid", default=None, type=str, help="Identify blocks that you mine (eg: /signet:1/)") 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") @@ -559,12 +552,6 @@ def main(): 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: