ncantu f7f9442156 Fix Bitcoin miner PSBT signing after bitcoind restart
**Motivations:**
- Miner stopped producing blocks after bitcoind and mempool restart
- PSBT signing failed with "PSBT signing failed" error
- descriptorprocesspsbt failed with "Argument list too long" error due to large PSBT size (1.5MB)

**Root causes:**
- descriptorprocesspsbt called via command line exceeded argument length limit when PSBT is large
- walletprocesspsbt succeeded but could not mark PSBT as complete because it cannot sign artificial signet transactions (to_spend/spend) which are not real UTXOs in wallet
- After restart, the miner needed descriptorprocesspsbt to sign artificial signet transactions, but command line approach failed

**Correctifs:**
- Modified miner to use JSON-RPC HTTP API directly for descriptorprocesspsbt instead of command line
- PSBT is now passed via HTTP request body, avoiding command line argument length limit
- Added fallback to walletprocesspsbt if HTTP RPC fails
- Updated mine.sh to use -rpcwallet and -datadir correctly for wallet operations

**Evolutions:**
- Miner now uses HTTP JSON-RPC for large PSBTs, making it more robust for signet mining with many transactions
- Improved error handling with fallback mechanisms

**Pages affectées:**
- miner: Modified PSBT signing logic to use HTTP JSON-RPC for descriptorprocesspsbt
- mine.sh: Updated to use -rpcwallet and -datadir correctly
2026-01-27 23:30:19 +01:00

715 lines
30 KiB
Python

#!/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.
# Modified for quick blocks and run in leaner docker container(By Easepay Inc 2024)
# Standard library imports for path setup
import argparse
import base64
import json
import logging
import math
import os
import sys
import re
import struct
import time
import subprocess
from io import BytesIO
# Modify the Python path
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, "miner_imports"))
sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
# Custom module imports from test_framework
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):
this_interval = 0.000001
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)
# Use descriptorprocesspsbt instead of walletprocesspsbt for Bitcoin Core 30+
# descriptorprocesspsbt can sign PSBT with artificial UTXOs (to_spend/spend) used in Signet
# It doesn't require the wallet to recognize the UTXO, just the descriptor with private key
if hasattr(args, 'signer') and args.signer == 'descriptor':
# Get private key from descriptor or use provided one
privkey = args.descriptor if args.descriptor and not '*' in args.descriptor else None
if privkey is None and hasattr(args, 'privkey') and args.privkey:
privkey = args.privkey
if privkey:
# Extract WIF from descriptor if needed (pk(WIF) or wpkh(WIF))
if privkey.startswith('pk(') or privkey.startswith('wpkh('):
privkey = privkey.split('(')[1].split(')')[0].split('#')[0]
descriptor = "pk(%s)" % privkey
# Use descriptorprocesspsbt: PSBT, descriptors array (JSON), sighashtype, bip32derivs, finalize
descriptors_array = [descriptor]
psbt_signed = json.loads(args.bcli("descriptorprocesspsbt", psbt, json.dumps(descriptors_array), "ALL", "true", "true"))
else:
# Fallback to walletprocesspsbt if no descriptor/privkey provided
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
else:
# Use descriptorprocesspsbt by default (works with Bitcoin Core 30+ and descriptor wallets)
# This avoids the wallet UTXO recognition issue with artificial Signet transactions
try:
# Get privkey from environment or args
privkey = os.environ.get('PRIVKEY', '')
if not privkey and hasattr(args, 'privkey') and args.privkey:
privkey = args.privkey
if privkey:
# Try descriptorprocesspsbt first using JSON-RPC HTTP to avoid "Argument list too long"
# descriptorprocesspsbt is needed for signet artificial transactions
cli_base = args.cli.split(" ")
cli_node = [c for c in cli_base if not c.startswith("-rpcwallet")]
if not cli_node:
cli_node = ["bitcoin-cli"]
descriptor = "pk(%s)" % privkey
descriptors_array = [descriptor]
try:
# Use JSON-RPC HTTP directly to avoid command line length limit
import urllib.request
import urllib.parse
# Extract RPC credentials from bitcoin-cli config or use defaults
rpc_url = "http://127.0.0.1:38332/" # Default signet RPC port
rpc_user = "bitcoin"
rpc_pass = "bitcoin"
# Try to get RPC credentials from environment or config
if "RPCUSER" in os.environ:
rpc_user = os.environ["RPCUSER"]
if "RPCPASSWORD" in os.environ:
rpc_pass = os.environ["RPCPASSWORD"]
# Create JSON-RPC request
rpc_request = {
"method": "descriptorprocesspsbt",
"params": [psbt, descriptors_array, "ALL", True, True],
"id": 1,
"jsonrpc": "2.0"
}
# Encode request
data = json.dumps(rpc_request).encode('utf-8')
# Create HTTP request with basic auth
req = urllib.request.Request(rpc_url, data=data, headers={"Content-Type": "application/json"})
# Add basic auth
import base64
credentials = base64.b64encode(("%s:%s" % (rpc_user, rpc_pass)).encode()).decode()
req.add_header("Authorization", "Basic %s" % credentials)
# Send request
with urllib.request.urlopen(req, timeout=30) as response:
rpc_response = json.loads(response.read().decode('utf-8'))
if "result" in rpc_response:
psbt_signed = rpc_response["result"]
elif "error" in rpc_response:
raise Exception("RPC error: %s" % rpc_response["error"])
else:
psbt_signed = rpc_response
logging.debug("Used descriptorprocesspsbt via HTTP for signing with descriptor: %s" % descriptor)
except Exception as e:
# If descriptorprocesspsbt fails, try walletprocesspsbt as fallback
logging.debug("descriptorprocesspsbt via HTTP failed, trying walletprocesspsbt: %s" % str(e))
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
logging.debug("walletprocesspsbt completed, complete=%s" % psbt_signed.get("complete", False))
else:
# Fallback to walletprocesspsbt if no PRIVKEY available
logging.debug("No PRIVKEY found, falling back to walletprocesspsbt")
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
except Exception as e:
# If descriptorprocesspsbt fails, try walletprocesspsbt as fallback
logging.debug("descriptorprocesspsbt failed, trying walletprocesspsbt: %s" % str(e))
try:
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
logging.debug("walletprocesspsbt succeeded")
except Exception as e2:
logging.debug("walletprocesspsbt also failed: %s" % str(e2))
raise e
except Exception as e:
# If descriptorprocesspsbt fails, try walletprocesspsbt as fallback
logging.debug("descriptorprocesspsbt failed, trying walletprocesspsbt: %s" % str(e))
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")
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)
# Get PRIVKEY from environment or argument for descriptor signing
if not hasattr(args, 'privkey') or args.privkey is None:
args.privkey = os.environ.get('PRIVKEY', '')
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()