mirror of https://github.com/bitcoin/bitcoin
Merge #19937: signet mining utility
pull/20911/head595a34dbea
contrib/signet: Document miner script in README.md (Anthony Towns)ff7dbdc08a
contrib/signet: Add script for generating a signet chain (Anthony Towns)13762bcc96
Add bitcoin-util command line utility (Anthony Towns)95d5d5e625
rpc: allow getblocktemplate for test chains when unconnected or in IBD (Anthony Towns)81c54dec20
rpc: update getblocktemplate with signet rule, include signet_challenge (Anthony Towns) Pull request description: Adds `contrib/signet/miner` for mining signet blocks. Adds `bitcoin-util` cli utility, with the idea being it can provide bitcoin related functionality that does not rely on the ability to access a running node. Only subcommand currently is "grind" which takes a hex-encoded header and grinds its nonce until its nBits is satisfied. Updates `getblocktemplate` to include `signet_challenge` field, and makes `getblocktemplate` require the signet rule when invoked on the signet change. Removes connectivity and IBD checks from `getblocktemplate` when applied to a test chain (regtest, testnet, signet). ACKs for top commit: laanwj: code review ACK595a34dbea
Tree-SHA512: 8d43297710fdc1edc58acd9b53e1bd1671e5724f7097b40ab73653715dc8becc70534c4496cbba9290f4dd6538a7a3d5830eb85f83391ea31a3bb5b9d3378cc3
commit
7b975639ef
@ -0,0 +1,639 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2020 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os.path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
|
||||
PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
|
||||
PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
|
||||
sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
|
||||
|
||||
from test_framework.blocktools import WITNESS_COMMITMENT_HEADER, script_BIP34_coinbase_height # noqa: E402
|
||||
from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, FromHex, ToHex, deser_string, hash256, ser_compact_size, ser_string, ser_uint256, 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 FromHex, 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 = FromHex(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 = unhexlify(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] + [FromHex(CTransaction(), 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 = unhexlify(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(ToHex(block))
|
||||
|
||||
def nbits_to_target(nbits):
|
||||
shift = (nbits >> 24) & 0xff
|
||||
return (nbits & 0x00ffffff) * 2**(8*(shift - 3))
|
||||
|
||||
def target_to_nbits(target):
|
||||
tstr = "{0:x}".format(target)
|
||||
if len(tstr) < 6:
|
||||
tstr = ("000000"+tstr)[-6:]
|
||||
if len(tstr) % 2 != 0:
|
||||
tstr = "0" + tstr
|
||||
if int(tstr[0],16) >= 0x8:
|
||||
# avoid "negative"
|
||||
tstr = "00" + tstr
|
||||
fix = int(tstr[:6], 16)
|
||||
sz = len(tstr)//2
|
||||
if tstr[6:] != "0"*(sz*2-6):
|
||||
fix += 1
|
||||
|
||||
return int("%02x%06x" % (sz,fix), 16)
|
||||
|
||||
def seconds_to_hms(s):
|
||||
if s == 0:
|
||||
return "0s"
|
||||
neg = (s < 0)
|
||||
if neg:
|
||||
s = -s
|
||||
out = ""
|
||||
if s % 60 > 0:
|
||||
out = "%ds" % (s % 60)
|
||||
s //= 60
|
||||
if s % 60 > 0:
|
||||
out = "%dm%s" % (s % 60, out)
|
||||
s //= 60
|
||||
if s > 0:
|
||||
out = "%dh%s" % (s, out)
|
||||
if neg:
|
||||
out = "-" + out
|
||||
return out
|
||||
|
||||
def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson):
|
||||
# strategy:
|
||||
# 1) work out how far off our desired target we are
|
||||
# 2) cap it to a factor of 4 since that's the best we can do in a single retarget period
|
||||
# 3) use that to work out the desired average interval in this retarget period
|
||||
# 4) if doing poisson, use the last hash to pick a uniformly random number in [0,1), and work out a random multiplier to vary the average by
|
||||
# 5) cap the resulting interval between 1 second and 1 hour to avoid extremes
|
||||
|
||||
INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
|
||||
|
||||
current_target = nbits_to_target(last_nbits)
|
||||
retarget_factor = ultimate_target / current_target
|
||||
retarget_factor = max(0.25, min(retarget_factor, 4.0))
|
||||
|
||||
avg_interval = INTERVAL * retarget_factor
|
||||
|
||||
if do_poisson:
|
||||
det_rand = int(last_hash[-8:], 16) * 2**-32
|
||||
this_interval_variance = -math.log1p(-det_rand)
|
||||
else:
|
||||
this_interval_variance = 1
|
||||
|
||||
this_interval = avg_interval * this_interval_variance
|
||||
this_interval = max(1, min(this_interval, 3600))
|
||||
|
||||
return this_interval
|
||||
|
||||
def next_block_is_mine(last_hash, my_blocks):
|
||||
det_rand = int(last_hash[-16:-8], 16)
|
||||
return my_blocks[0] <= (det_rand % my_blocks[2]) < my_blocks[1]
|
||||
|
||||
def do_generate(args):
|
||||
if args.max_blocks is not None:
|
||||
if args.ongoing:
|
||||
logging.error("Cannot specify both --ongoing and --max-blocks")
|
||||
return 1
|
||||
if args.max_blocks < 1:
|
||||
logging.error("N must be a positive integer")
|
||||
return 1
|
||||
max_blocks = args.max_blocks
|
||||
elif args.ongoing:
|
||||
max_blocks = None
|
||||
else:
|
||||
max_blocks = 1
|
||||
|
||||
if args.set_block_time is not None and max_blocks != 1:
|
||||
logging.error("Cannot specify --ongoing or --max-blocks > 1 when using --set-block-time")
|
||||
return 1
|
||||
if args.set_block_time is not None and args.set_block_time < 0:
|
||||
args.set_block_time = time.time()
|
||||
logging.info("Treating negative block time as current time (%d)" % (args.set_block_time))
|
||||
|
||||
if args.min_nbits:
|
||||
if args.nbits is not None:
|
||||
logging.error("Cannot specify --nbits and --min-nbits")
|
||||
return 1
|
||||
args.nbits = "1e0377ae"
|
||||
logging.info("Using nbits=%s" % (args.nbits))
|
||||
|
||||
if args.set_block_time is None:
|
||||
if args.nbits is None or len(args.nbits) != 8:
|
||||
logging.error("Must specify --nbits (use calibrate command to determine value)")
|
||||
return 1
|
||||
|
||||
if args.multiminer is None:
|
||||
my_blocks = (0,1,1)
|
||||
else:
|
||||
if not args.ongoing:
|
||||
logging.error("Cannot specify --multiminer without --ongoing")
|
||||
return 1
|
||||
m = RE_MULTIMINER.match(args.multiminer)
|
||||
if m is None:
|
||||
logging.error("--multiminer argument must be k/m or j-k/m")
|
||||
return 1
|
||||
start,_,stop,total = m.groups()
|
||||
if stop is None:
|
||||
stop = start
|
||||
start, stop, total = map(int, (start, stop, total))
|
||||
if stop < start or start <= 0 or total < stop or total == 0:
|
||||
logging.error("Inconsistent values for --multiminer")
|
||||
return 1
|
||||
my_blocks = (start-1, stop, total)
|
||||
|
||||
ultimate_target = nbits_to_target(int(args.nbits,16))
|
||||
|
||||
mined_blocks = 0
|
||||
bestheader = {"hash": None}
|
||||
lastheader = None
|
||||
while max_blocks is None or mined_blocks < max_blocks:
|
||||
|
||||
# current status?
|
||||
bci = json.loads(args.bcli("getblockchaininfo"))
|
||||
|
||||
if bestheader["hash"] != bci["bestblockhash"]:
|
||||
bestheader = json.loads(args.bcli("getblockheader", bci["bestblockhash"]))
|
||||
|
||||
if lastheader is None:
|
||||
lastheader = bestheader["hash"]
|
||||
elif bestheader["hash"] != lastheader:
|
||||
next_delta = next_block_delta(int(bestheader["bits"], 16), bestheader["hash"], ultimate_target, args.poisson)
|
||||
next_delta += bestheader["time"] - time.time()
|
||||
next_is_mine = next_block_is_mine(bestheader["hash"], my_blocks)
|
||||
logging.info("Received new block at height %d; next in %s (%s)", bestheader["height"], seconds_to_hms(next_delta), ("mine" if next_is_mine else "backup"))
|
||||
lastheader = bestheader["hash"]
|
||||
|
||||
# when is the next block due to be mined?
|
||||
now = time.time()
|
||||
if args.set_block_time is not None:
|
||||
logging.debug("Setting start time to %d", args.set_block_time)
|
||||
mine_time = args.set_block_time
|
||||
action_time = now
|
||||
is_mine = True
|
||||
elif bestheader["height"] == 0:
|
||||
logging.error("When mining first block in a new signet, must specify --set-block-time")
|
||||
return 1
|
||||
else:
|
||||
|
||||
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson)
|
||||
mine_time = bestheader["time"] + time_delta
|
||||
|
||||
is_mine = next_block_is_mine(bci["bestblockhash"], my_blocks)
|
||||
|
||||
action_time = mine_time
|
||||
if not is_mine:
|
||||
action_time += args.backup_delay
|
||||
|
||||
if args.standby_delay > 0:
|
||||
action_time += args.standby_delay
|
||||
elif mined_blocks == 0:
|
||||
# for non-standby, always mine immediately on startup,
|
||||
# even if the next block shouldn't be ours
|
||||
action_time = now
|
||||
|
||||
# don't want fractional times so round down
|
||||
mine_time = int(mine_time)
|
||||
action_time = int(action_time)
|
||||
|
||||
# can't mine a block 2h in the future; 1h55m for some safety
|
||||
action_time = max(action_time, mine_time - 6900)
|
||||
|
||||
# ready to go? otherwise sleep and check for new block
|
||||
if now < action_time:
|
||||
sleep_for = min(action_time - now, 60)
|
||||
if mine_time < now:
|
||||
# someone else might have mined the block,
|
||||
# so check frequently, so we don't end up late
|
||||
# mining the next block if it's ours
|
||||
sleep_for = min(20, sleep_for)
|
||||
minestr = "mine" if is_mine else "backup"
|
||||
logging.debug("Sleeping for %s, next block due in %s (%s)" % (seconds_to_hms(sleep_for), seconds_to_hms(mine_time - now), minestr))
|
||||
time.sleep(sleep_for)
|
||||
continue
|
||||
|
||||
# gbt
|
||||
tmpl = json.loads(args.bcli("getblocktemplate", '{"rules":["signet","segwit"]}'))
|
||||
if tmpl["previousblockhash"] != bci["bestblockhash"]:
|
||||
logging.warning("GBT based off unexpected block (%s not %s), retrying", tmpl["previousblockhash"], bci["bestblockhash"])
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
logging.debug("GBT template: %s", tmpl)
|
||||
|
||||
if tmpl["mintime"] > mine_time:
|
||||
logging.info("Updating block time from %d to %d", mine_time, tmpl["mintime"])
|
||||
mine_time = tmpl["mintime"]
|
||||
if mine_time > now:
|
||||
logging.error("GBT mintime is in the future: %d is %d seconds later than %d", mine_time, (mine_time-now), now)
|
||||
return 1
|
||||
|
||||
# address for reward
|
||||
reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"])
|
||||
|
||||
# mine block
|
||||
logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(mine_time-bestheader["time"]), mine_time, is_mine)
|
||||
mined_blocks += 1
|
||||
psbt = generate_psbt(tmpl, reward_spk, blocktime=mine_time)
|
||||
psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=psbt.encode('utf8')))
|
||||
if not psbt_signed.get("complete",False):
|
||||
logging.debug("Generated PSBT: %s" % (psbt,))
|
||||
sys.stderr.write("PSBT signing failed")
|
||||
return 1
|
||||
block, signet_solution = do_decode_psbt(psbt_signed["psbt"])
|
||||
block = finish_block(block, signet_solution, args.grind_cmd)
|
||||
|
||||
# submit block
|
||||
r = args.bcli("-stdin", "submitblock", input=ToHex(block).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")
|
||||
return 1
|
||||
|
||||
TRIALS = 600 # gets variance down pretty low
|
||||
TRIAL_BITS = 0x1e3ea75f # takes about 5m to do 600 trials
|
||||
#TRIAL_BITS = 0x1e7ea75f # XXX
|
||||
|
||||
header = CBlockHeader()
|
||||
header.nBits = TRIAL_BITS
|
||||
targ = nbits_to_target(header.nBits)
|
||||
|
||||
start = time.time()
|
||||
count = 0
|
||||
#CHECKS=[]
|
||||
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()
|
||||
#newhead = FromHex(CBlockHeader(), newheadhex.decode('utf8'))
|
||||
#count += newhead.nNonce
|
||||
#if (i+1) % 100 == 0:
|
||||
# CHECKS.append((i+1, count, time.time()-start))
|
||||
|
||||
#print("checks =", [c*1.0 / (b*targ*2**-256) for _,b,c in CHECKS])
|
||||
|
||||
avg = (time.time() - start) * 1.0 / TRIALS
|
||||
#exp_count = 2**256 / targ * TRIALS
|
||||
#print("avg =", avg, "count =", count, "exp_count =", exp_count)
|
||||
|
||||
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("--signcmd", default=None, type=str, help="Alternative signing command")
|
||||
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, help="Command to grind a block header for proof-of-work")
|
||||
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
|
||||
args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(args.cli.split(" "), list(a), input=input, **kwargs)
|
||||
|
||||
if hasattr(args, "address") and hasattr(args, "descriptor"):
|
||||
if args.address is None and args.descriptor is None:
|
||||
sys.stderr.write("Must specify --address or --descriptor\n")
|
||||
return 1
|
||||
elif args.address is not None and args.descriptor is not None:
|
||||
sys.stderr.write("Only specify one of --address or --descriptor\n")
|
||||
return 1
|
||||
args.derived_addresses = {}
|
||||
|
||||
if args.debug:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif args.quiet:
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
if hasattr(args, "fn"):
|
||||
return args.fn(args)
|
||||
else:
|
||||
logging.error("Must specify command")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
#include <windows.h> // needed for VERSIONINFO
|
||||
#include "clientversion.h" // holds the needed client version information
|
||||
|
||||
#define VER_PRODUCTVERSION CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR,CLIENT_VERSION_BUILD
|
||||
#define VER_PRODUCTVERSION_STR STRINGIZE(CLIENT_VERSION_MAJOR) "." STRINGIZE(CLIENT_VERSION_MINOR) "." STRINGIZE(CLIENT_VERSION_BUILD)
|
||||
#define VER_FILEVERSION VER_PRODUCTVERSION
|
||||
#define VER_FILEVERSION_STR VER_PRODUCTVERSION_STR
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VER_FILEVERSION
|
||||
PRODUCTVERSION VER_PRODUCTVERSION
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904E4" // U.S. English - multilingual (hex)
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Bitcoin"
|
||||
VALUE "FileDescription", "bitcoin-util (CLI Bitcoin utility)"
|
||||
VALUE "FileVersion", VER_FILEVERSION_STR
|
||||
VALUE "InternalName", "bitcoin-util"
|
||||
VALUE "LegalCopyright", COPYRIGHT_STR
|
||||
VALUE "LegalTrademarks1", "Distributed under the MIT software license, see the accompanying file COPYING or http://www.opensource.org/licenses/mit-license.php."
|
||||
VALUE "OriginalFilename", "bitcoin-util.exe"
|
||||
VALUE "ProductName", "bitcoin-util"
|
||||
VALUE "ProductVersion", VER_PRODUCTVERSION_STR
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0, 1252 // language neutral - multilingual (decimal)
|
||||
END
|
||||
END
|
@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2009-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.
|
||||
|
||||
#if defined(HAVE_CONFIG_H)
|
||||
#include <config/bitcoin-config.h>
|
||||
#endif
|
||||
|
||||
#include <arith_uint256.h>
|
||||
#include <clientversion.h>
|
||||
#include <coins.h>
|
||||
#include <consensus/consensus.h>
|
||||
#include <core_io.h>
|
||||
#include <key_io.h>
|
||||
#include <policy/rbf.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <script/script.h>
|
||||
#include <script/sign.h>
|
||||
#include <script/signingprovider.h>
|
||||
#include <univalue.h>
|
||||
#include <util/moneystr.h>
|
||||
#include <util/rbf.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdio.h>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
static const int CONTINUE_EXECUTION=-1;
|
||||
|
||||
const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr;
|
||||
|
||||
static void SetupBitcoinUtilArgs(ArgsManager &argsman)
|
||||
{
|
||||
SetupHelpOptions(argsman);
|
||||
|
||||
SetupChainParamsBaseOptions(argsman);
|
||||
}
|
||||
|
||||
// This function returns either one of EXIT_ codes when it's expected to stop the process or
|
||||
// CONTINUE_EXECUTION when it's expected to continue further.
|
||||
static int AppInitUtil(int argc, char* argv[])
|
||||
{
|
||||
SetupBitcoinUtilArgs(gArgs);
|
||||
std::string error;
|
||||
if (!gArgs.ParseParameters(argc, argv, error)) {
|
||||
tfm::format(std::cerr, "Error parsing command line arguments: %s\n", error);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Check for chain settings (Params() calls are only valid after this clause)
|
||||
try {
|
||||
SelectParams(gArgs.GetChainName());
|
||||
} catch (const std::exception& e) {
|
||||
tfm::format(std::cerr, "Error: %s\n", e.what());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (argc < 2 || HelpRequested(gArgs)) {
|
||||
// First part of help message is specific to this utility
|
||||
std::string strUsage = PACKAGE_NAME " bitcoin-util utility version " + FormatFullVersion() + "\n\n" +
|
||||
"Usage: bitcoin-util [options] [commands] Do stuff\n" +
|
||||
"\n";
|
||||
strUsage += gArgs.GetHelpMessage();
|
||||
|
||||
tfm::format(std::cout, "%s", strUsage);
|
||||
|
||||
if (argc < 2) {
|
||||
tfm::format(std::cerr, "Error: too few parameters\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
return CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
static void grind_task(uint32_t nBits, CBlockHeader& header_orig, uint32_t offset, uint32_t step, std::atomic<bool>& found)
|
||||
{
|
||||
arith_uint256 target;
|
||||
bool neg, over;
|
||||
target.SetCompact(nBits, &neg, &over);
|
||||
if (target == 0 || neg || over) return;
|
||||
CBlockHeader header = header_orig; // working copy
|
||||
header.nNonce = offset;
|
||||
|
||||
uint32_t finish = std::numeric_limits<uint32_t>::max() - step;
|
||||
finish = finish - (finish % step) + offset;
|
||||
|
||||
while (!found && header.nNonce < finish) {
|
||||
const uint32_t next = (finish - header.nNonce < 5000*step) ? finish : header.nNonce + 5000*step;
|
||||
do {
|
||||
if (UintToArith256(header.GetHash()) <= target) {
|
||||
if (!found.exchange(true)) {
|
||||
header_orig.nNonce = header.nNonce;
|
||||
}
|
||||
return;
|
||||
}
|
||||
header.nNonce += step;
|
||||
} while(header.nNonce != next);
|
||||
}
|
||||
}
|
||||
|
||||
static int Grind(int argc, char* argv[], std::string& strPrint)
|
||||
{
|
||||
if (argc != 1) {
|
||||
strPrint = "Must specify block header to grind";
|
||||
return 1;
|
||||
}
|
||||
|
||||
CBlockHeader header;
|
||||
if (!DecodeHexBlockHeader(header, argv[0])) {
|
||||
strPrint = "Could not decode block header";
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t nBits = header.nBits;
|
||||
std::atomic<bool> found{false};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
int n_tasks = std::max(1u, std::thread::hardware_concurrency());
|
||||
for (int i = 0; i < n_tasks; ++i) {
|
||||
threads.emplace_back( grind_task, nBits, std::ref(header), i, n_tasks, std::ref(found) );
|
||||
}
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
}
|
||||
if (!found) {
|
||||
strPrint = "Could not satisfy difficulty target";
|
||||
return 1;
|
||||
}
|
||||
|
||||
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ss << header;
|
||||
strPrint = HexStr(ss);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int CommandLineUtil(int argc, char* argv[])
|
||||
{
|
||||
if (argc <= 1) return 1;
|
||||
|
||||
std::string strPrint;
|
||||
int nRet = 0;
|
||||
|
||||
try {
|
||||
while (argc > 1 && IsSwitchChar(argv[1][0]) && (argv[1][1] != 0)) {
|
||||
--argc;
|
||||
++argv;
|
||||
}
|
||||
|
||||
char* command = argv[1];
|
||||
if (strcmp(command, "grind") == 0) {
|
||||
nRet = Grind(argc-2, argv+2, strPrint);
|
||||
} else {
|
||||
strPrint = strprintf("Unknown command %s", command);
|
||||
nRet = 1;
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
strPrint = std::string("error: ") + e.what();
|
||||
nRet = EXIT_FAILURE;
|
||||
}
|
||||
catch (...) {
|
||||
PrintExceptionContinue(nullptr, "CommandLineUtil()");
|
||||
throw;
|
||||
}
|
||||
|
||||
if (strPrint != "") {
|
||||
tfm::format(nRet == 0 ? std::cout : std::cerr, "%s\n", strPrint);
|
||||
}
|
||||
return nRet;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
SetupEnvironment();
|
||||
|
||||
try {
|
||||
int ret = AppInitUtil(argc, argv);
|
||||
if (ret != CONTINUE_EXECUTION)
|
||||
return ret;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
PrintExceptionContinue(&e, "AppInitUtil()");
|
||||
return EXIT_FAILURE;
|
||||
} catch (...) {
|
||||
PrintExceptionContinue(nullptr, "AppInitUtil()");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
int ret = EXIT_FAILURE;
|
||||
try {
|
||||
ret = CommandLineUtil(argc, argv);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
PrintExceptionContinue(&e, "CommandLineUtil()");
|
||||
} catch (...) {
|
||||
PrintExceptionContinue(nullptr, "CommandLineUtil()");
|
||||
}
|
||||
return ret;
|
||||
}
|
Loading…
Reference in new issue