2020-09-10 08:37:55 +10:00
#!/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 json
import logging
import math
2022-03-13 21:44:28 +01:00
import os
2020-09-10 08:37:55 +10:00
import re
import struct
import sys
import time
import subprocess
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)
2022-07-03 20:07:44 +02:00
from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height # noqa: E402
2022-07-16 02:03:39 +02:00
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
2022-07-16 02:48:04 +02:00
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
2023-09-06 01:01:44 +10:00
from test_framework.script import CScript, CScriptOp # noqa: E402
2020-09-10 08:37:55 +10:00
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
2023-11-15 15:55:20 +01:00
RE_MULTIMINER = re.compile(r"^(\d+)(-(\d+))?/(\d+)$")
2020-09-10 08:37:55 +10:00
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""
scripted-diff: test: Use int.to_bytes over struct packing
-BEGIN VERIFY SCRIPT-
sed -i --regexp-extended 's!struct.pack\(.<?B., (.*)\)!\1.to_bytes(1, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<I., (.*)\)!\1.to_bytes(4, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<H., (.*)\)!\1.to_bytes(2, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<i., (.*)\)!\1.to_bytes(4, "little", signed=True)!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<q., (.*)\)!\1.to_bytes(8, "little", signed=True)!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.>H., (.*)\)!\1.to_bytes(2, "big")!g' $( git grep -l struct.pack )
-END VERIFY SCRIPT-
2024-02-07 11:26:05 +01:00
sd += block.nVersion.to_bytes(4, "little", signed=True)
2020-09-10 08:37:55 +10:00
sd += ser_uint256(block.hashPrevBlock)
sd += ser_uint256(mroot)
scripted-diff: test: Use int.to_bytes over struct packing
-BEGIN VERIFY SCRIPT-
sed -i --regexp-extended 's!struct.pack\(.<?B., (.*)\)!\1.to_bytes(1, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<I., (.*)\)!\1.to_bytes(4, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<H., (.*)\)!\1.to_bytes(2, "little")!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<i., (.*)\)!\1.to_bytes(4, "little", signed=True)!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.<q., (.*)\)!\1.to_bytes(8, "little", signed=True)!g' $( git grep -l struct.pack )
sed -i --regexp-extended 's!struct.pack\(.>H., (.*)\)!\1.to_bytes(2, "big")!g' $( git grep -l struct.pack )
-END VERIFY SCRIPT-
2024-02-07 11:26:05 +01:00
sd += block.nTime.to_bytes(4, "little")
2020-09-10 08:37:55 +10:00
to_spend = CTransaction()
2024-01-26 15:27:13 -05:00
to_spend.version = 0
2020-09-10 08:37:55 +10:00
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()
2024-01-26 15:27:13 -05:00
spend.version = 0
2020-09-10 08:37:55 +10:00
spend.nLockTime = 0
spend.vin = [CTxIn(COutPoint(to_spend.sha256, 0), b"", 0)]
spend.vout = [CTxOut(0, b"\x6a")]
return spend, to_spend
2022-10-01 11:18:03 +10:00
def decode_psbt(b64psbt):
2020-09-10 08:37:55 +10:00
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
2022-07-16 02:48:04 +02:00
scriptSig = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTSIG, b"")
scriptWitness = psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS, b"\x00")
2020-09-10 08:37:55 +10:00
2022-07-16 01:47:08 +02:00
return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
2020-09-10 08:37:55 +10:00
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()
2021-06-20 14:01:36 +02:00
newhead = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
2020-09-10 08:37:55 +10:00
block.nNonce = newhead.nNonce
block.rehash()
return block
2023-09-06 01:01:44 +10:00
def generate_psbt(tmpl, reward_spk, *, blocktime=None, poolid=None):
2020-09-10 08:37:55 +10:00
signet_spk = tmpl["signet_challenge"]
2021-08-16 19:30:44 +01:00
signet_spk_bin = bytes.fromhex(signet_spk)
2020-09-10 08:37:55 +10:00
2023-09-06 01:01:44 +10:00
scriptSig = script_BIP34_coinbase_height(tmpl["height"])
if poolid is not None:
scriptSig = CScript(b"" + scriptSig + CScriptOp.encode_op_pushdata(poolid))
2022-10-01 11:16:25 +10:00
cbtx = CTransaction()
2023-09-06 01:01:44 +10:00
cbtx.vin = [CTxIn(COutPoint(0, 0xffffffff), scriptSig, 0xffffffff)]
2022-10-01 11:16:25 +10:00
cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)]
2020-09-10 08:37:55 +10:00
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
2021-06-15 23:02:28 +02:00
block.vtx = [cbtx] + [tx_from_hex(t["data"]) for t in tmpl["transactions"]]
2020-09-10 08:37:55 +10:00
witnonce = 0
witroot = block.calc_witness_merkle_root()
cbwit = CTxInWitness()
cbwit.scriptWitness.stack = [ser_uint256(witnonce)]
block.vtx[0].wit.vtxinwit = [cbwit]
2022-07-03 20:07:44 +02:00
block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce))))
2020-09-10 08:37:55 +10:00
signme, spendme = signet_txs(block, signet_spk_bin)
2022-10-01 11:13:53 +10:00
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()
2020-09-10 08:37:55 +10:00
2023-09-06 01:01:44 +10:00
def get_poolid(args):
if args.poolid is not None:
return args.poolid.encode('utf8')
elif args.poolnum is not None:
return b"/signet:%d/" % (args.poolnum)
else:
return None
2020-09-10 08:37:55 +10:00
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
2022-10-01 11:12:51 +10:00
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]
2021-08-16 19:30:44 +01:00
reward_spk = bytes.fromhex(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"])
2020-09-10 08:37:55 +10:00
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):
2023-09-06 01:01:44 +10:00
poolid = get_poolid(args)
2020-09-10 08:37:55 +10:00
tmpl = json.load(sys.stdin)
_, reward_spk = get_reward_addr_spk(args, tmpl["height"])
2023-09-06 01:01:44 +10:00
psbt = generate_psbt(tmpl, reward_spk, poolid=poolid)
2020-09-10 08:37:55 +10:00
print(psbt)
def do_solvepsbt(args):
2022-10-01 11:18:03 +10:00
block, signet_solution = decode_psbt(sys.stdin.read())
2020-09-10 08:37:55 +10:00
block = finish_block(block, signet_solution, args.grind_cmd)
2021-06-16 00:32:18 +02:00
print(block.serialize().hex())
2020-09-10 08:37:55 +10:00
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
2022-10-01 11:25:01 +10:00
class Generate:
2020-09-10 08:37:55 +10:00
2022-10-01 13:15:19 +10:00
def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800,
2023-09-06 01:01:44 +10:00
standby_delay=0, backup_delay=0, set_block_time=None,
Extend signetchallenge to set target block spacing
Inspired by https://github.com/bitcoin/bitcoin/pull/27446, this commit
implements the proposal detailed in the comment
https://github.com/bitcoin/bitcoin/pull/27446#issuecomment-1516600820.
Rationale.
Introduce the ability to configure a custom target time between blocks in a
custom Bitcoin signet network. This enhancement enables users to create a signet
that is more conducive to testing. The change enhances the flexibility of signet,
rendering it more versatile for various testing scenarios. For instance, I am
currently working on setting up a signet with a 30-second block time. However,
this caused numerous difficulty adjustments, resulting in an inconsistent
network state. Regtest isn't a viable alternative for me in this context since
we prefer defaults to utilize our custom signet when configured, without
impeding local regtest development.
Implementation.
If the challenge format is "OP_RETURN PUSHDATA<params> PUSHDATA<actual
challenge>", the actual challenge from the second data push is used as the
signet challenge, and the parameters from the first push are used to configure
the network. Otherwise the challenge is used as is.
Under the previous rules, such a signet challenge would always evaluate to
false, suggesting that it is likely not in use by anyone. Updating bitcoind to a
version that includes this change will not cause any disruptions - existing
signet challenges will retain their original meaning without alteration.
The only parameter currently available is "target_spacing" (default 600
seconds). To set it, place "0x01<target_spacing as uint64_t, little endian>" in
the params. Empty params are also valid. If other network parameters are added
in the future, they should use "0x02<option 2 value>", "0x03<option 3 value>",
etc., following the protobuf style.
Two public functions were added to chainparams.h:
- ParseWrappedSignetChallenge: Extracts signet params and signet challenge
from a wrapped signet challenge.
- ParseSignetParams: Parses <params> bytes of the first data push.
Function ReadSigNetArgs calls ParseWrappedSignetChallenge and ParseSignetParams
to implement the new meaning of signetchallenge.
The description of the flag -signetchallenge was updated to reflect the changes.
A new unit tests file, chainparams_tests.cpp, was added, containing tests for
ParseWrappedSignetChallenge and ParseSignetParams.
The test signet_parse_tests from the file validation_tests.cpp was modified to
ensure proper understanding of the new logic.
In the functional test feature_signet.py, a test case was added with the value
of -signetchallenge set to the wrapped challenge, setting spacing to 30 seconds
and having the actual challenge OP_TRUE.
The Signet miner was updated, introducing a new option --target-spacing with a
default of 600 seconds. It must be set to the value used by the network.
Example.
I tested this commit against Mutinynet, a signet running on a custom fork of
Bitcoin Core, implementing 30s target spacing. I successfully synchronized the
blockchain using the following config:
signet=1
[signet]
signetchallenge=6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
The content of this wrapped challenge:
6a OP_RETURN
4c OP_PUSHDATA1
09 (length of signet params = 9)
011e00000000000000 (signet params: 0x01, pow_target_spacing=30)
4c OP_PUSHDATA1
25 (length of challenge = 37)
512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
(original Mutinynet challenge, can be found here:
https://blog.mutinywallet.com/mutinynet/ )
2024-01-31 16:15:57 -03:00
poolid=None, target_spacing=600):
2022-10-01 11:25:01 +10:00
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
2022-10-01 13:15:19 +10:00
self.standby_delay = standby_delay
self.backup_delay = backup_delay
self.set_block_time = set_block_time
2023-09-06 01:01:44 +10:00
self.poolid = poolid
2020-09-10 08:37:55 +10:00
Extend signetchallenge to set target block spacing
Inspired by https://github.com/bitcoin/bitcoin/pull/27446, this commit
implements the proposal detailed in the comment
https://github.com/bitcoin/bitcoin/pull/27446#issuecomment-1516600820.
Rationale.
Introduce the ability to configure a custom target time between blocks in a
custom Bitcoin signet network. This enhancement enables users to create a signet
that is more conducive to testing. The change enhances the flexibility of signet,
rendering it more versatile for various testing scenarios. For instance, I am
currently working on setting up a signet with a 30-second block time. However,
this caused numerous difficulty adjustments, resulting in an inconsistent
network state. Regtest isn't a viable alternative for me in this context since
we prefer defaults to utilize our custom signet when configured, without
impeding local regtest development.
Implementation.
If the challenge format is "OP_RETURN PUSHDATA<params> PUSHDATA<actual
challenge>", the actual challenge from the second data push is used as the
signet challenge, and the parameters from the first push are used to configure
the network. Otherwise the challenge is used as is.
Under the previous rules, such a signet challenge would always evaluate to
false, suggesting that it is likely not in use by anyone. Updating bitcoind to a
version that includes this change will not cause any disruptions - existing
signet challenges will retain their original meaning without alteration.
The only parameter currently available is "target_spacing" (default 600
seconds). To set it, place "0x01<target_spacing as uint64_t, little endian>" in
the params. Empty params are also valid. If other network parameters are added
in the future, they should use "0x02<option 2 value>", "0x03<option 3 value>",
etc., following the protobuf style.
Two public functions were added to chainparams.h:
- ParseWrappedSignetChallenge: Extracts signet params and signet challenge
from a wrapped signet challenge.
- ParseSignetParams: Parses <params> bytes of the first data push.
Function ReadSigNetArgs calls ParseWrappedSignetChallenge and ParseSignetParams
to implement the new meaning of signetchallenge.
The description of the flag -signetchallenge was updated to reflect the changes.
A new unit tests file, chainparams_tests.cpp, was added, containing tests for
ParseWrappedSignetChallenge and ParseSignetParams.
The test signet_parse_tests from the file validation_tests.cpp was modified to
ensure proper understanding of the new logic.
In the functional test feature_signet.py, a test case was added with the value
of -signetchallenge set to the wrapped challenge, setting spacing to 30 seconds
and having the actual challenge OP_TRUE.
The Signet miner was updated, introducing a new option --target-spacing with a
default of 600 seconds. It must be set to the value used by the network.
Example.
I tested this commit against Mutinynet, a signet running on a custom fork of
Bitcoin Core, implementing 30s target spacing. I successfully synchronized the
blockchain using the following config:
signet=1
[signet]
signetchallenge=6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
The content of this wrapped challenge:
6a OP_RETURN
4c OP_PUSHDATA1
09 (length of signet params = 9)
011e00000000000000 (signet params: 0x01, pow_target_spacing=30)
4c OP_PUSHDATA1
25 (length of challenge = 37)
512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
(original Mutinynet challenge, can be found here:
https://blog.mutinywallet.com/mutinynet/ )
2024-01-31 16:15:57 -03:00
# Set INTERVAL. If target_spacing=600 (the default), it is 10 minutes,
# adjusted for the off-by-one bug.
self.INTERVAL = target_spacing * 2016 / 2015
2022-10-01 11:25:01 +10:00
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
2020-09-10 08:37:55 +10:00
2022-10-01 11:25:01 +10:00
current_target = nbits_to_target(last_nbits)
retarget_factor = self.ultimate_target / current_target
retarget_factor = max(0.25, min(retarget_factor, 4.0))
2020-09-10 08:37:55 +10:00
2022-10-01 11:25:01 +10:00
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
2020-09-10 08:37:55 +10:00
2022-10-01 11:25:01 +10:00
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
2020-09-10 08:37:55 +10:00
2022-10-01 13:15:19 +10:00
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
2020-09-10 08:37:55 +10:00
2022-10-01 13:15:19 +10:00
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
2020-09-10 08:37:55 +10:00
2022-10-01 13:15:19 +10:00
# don't want fractional times so round down
self.mine_time = int(self.mine_time)
self.action_time = int(self.action_time)
2020-09-10 08:37:55 +10:00
2022-10-01 13:15:19 +10:00
# can't mine a block 2h in the future; 1h55m for some safety
self.action_time = max(self.action_time, self.mine_time - 6900)
2022-10-01 13:19:22 +10:00
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
2022-10-01 13:24:22 +10:00
def mine(self, bcli, grind_cmd, tmpl, reward_spk):
2023-09-06 01:01:44 +10:00
psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time, poolid=self.poolid)
2022-10-01 13:24:22 +10:00
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)
2020-09-10 08:37:55 +10:00
def do_generate(args):
2024-08-10 02:11:26 +10:00
if args.set_block_time is not None:
max_blocks = 1
elif args.max_blocks is not None:
2020-09-10 08:37:55 +10:00
if args.max_blocks < 1:
2024-08-10 02:11:26 +10:00
logging.error("--max_blocks must specify a positive integer")
2020-09-10 08:37:55 +10:00
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 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:
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)
Extend signetchallenge to set target block spacing
Inspired by https://github.com/bitcoin/bitcoin/pull/27446, this commit
implements the proposal detailed in the comment
https://github.com/bitcoin/bitcoin/pull/27446#issuecomment-1516600820.
Rationale.
Introduce the ability to configure a custom target time between blocks in a
custom Bitcoin signet network. This enhancement enables users to create a signet
that is more conducive to testing. The change enhances the flexibility of signet,
rendering it more versatile for various testing scenarios. For instance, I am
currently working on setting up a signet with a 30-second block time. However,
this caused numerous difficulty adjustments, resulting in an inconsistent
network state. Regtest isn't a viable alternative for me in this context since
we prefer defaults to utilize our custom signet when configured, without
impeding local regtest development.
Implementation.
If the challenge format is "OP_RETURN PUSHDATA<params> PUSHDATA<actual
challenge>", the actual challenge from the second data push is used as the
signet challenge, and the parameters from the first push are used to configure
the network. Otherwise the challenge is used as is.
Under the previous rules, such a signet challenge would always evaluate to
false, suggesting that it is likely not in use by anyone. Updating bitcoind to a
version that includes this change will not cause any disruptions - existing
signet challenges will retain their original meaning without alteration.
The only parameter currently available is "target_spacing" (default 600
seconds). To set it, place "0x01<target_spacing as uint64_t, little endian>" in
the params. Empty params are also valid. If other network parameters are added
in the future, they should use "0x02<option 2 value>", "0x03<option 3 value>",
etc., following the protobuf style.
Two public functions were added to chainparams.h:
- ParseWrappedSignetChallenge: Extracts signet params and signet challenge
from a wrapped signet challenge.
- ParseSignetParams: Parses <params> bytes of the first data push.
Function ReadSigNetArgs calls ParseWrappedSignetChallenge and ParseSignetParams
to implement the new meaning of signetchallenge.
The description of the flag -signetchallenge was updated to reflect the changes.
A new unit tests file, chainparams_tests.cpp, was added, containing tests for
ParseWrappedSignetChallenge and ParseSignetParams.
The test signet_parse_tests from the file validation_tests.cpp was modified to
ensure proper understanding of the new logic.
In the functional test feature_signet.py, a test case was added with the value
of -signetchallenge set to the wrapped challenge, setting spacing to 30 seconds
and having the actual challenge OP_TRUE.
The Signet miner was updated, introducing a new option --target-spacing with a
default of 600 seconds. It must be set to the value used by the network.
Example.
I tested this commit against Mutinynet, a signet running on a custom fork of
Bitcoin Core, implementing 30s target spacing. I successfully synchronized the
blockchain using the following config:
signet=1
[signet]
signetchallenge=6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
The content of this wrapped challenge:
6a OP_RETURN
4c OP_PUSHDATA1
09 (length of signet params = 9)
011e00000000000000 (signet params: 0x01, pow_target_spacing=30)
4c OP_PUSHDATA1
25 (length of challenge = 37)
512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
(original Mutinynet challenge, can be found here:
https://blog.mutinywallet.com/mutinynet/ )
2024-01-31 16:15:57 -03:00
max_interval_limit = args.target_spacing * 16 / 10
if args.max_interval < max_interval_limit:
logging.error("--max-interval must be at least %d (%f minutes)" % (max_interval_limit, max_interval_limit/60))
2022-09-29 14:02:58 +10:00
return 1
2023-09-06 01:01:44 +10:00
poolid = get_poolid(args)
2020-09-10 08:37:55 +10:00
ultimate_target = nbits_to_target(int(args.nbits,16))
2022-10-01 13:15:19 +10:00
gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval,
Extend signetchallenge to set target block spacing
Inspired by https://github.com/bitcoin/bitcoin/pull/27446, this commit
implements the proposal detailed in the comment
https://github.com/bitcoin/bitcoin/pull/27446#issuecomment-1516600820.
Rationale.
Introduce the ability to configure a custom target time between blocks in a
custom Bitcoin signet network. This enhancement enables users to create a signet
that is more conducive to testing. The change enhances the flexibility of signet,
rendering it more versatile for various testing scenarios. For instance, I am
currently working on setting up a signet with a 30-second block time. However,
this caused numerous difficulty adjustments, resulting in an inconsistent
network state. Regtest isn't a viable alternative for me in this context since
we prefer defaults to utilize our custom signet when configured, without
impeding local regtest development.
Implementation.
If the challenge format is "OP_RETURN PUSHDATA<params> PUSHDATA<actual
challenge>", the actual challenge from the second data push is used as the
signet challenge, and the parameters from the first push are used to configure
the network. Otherwise the challenge is used as is.
Under the previous rules, such a signet challenge would always evaluate to
false, suggesting that it is likely not in use by anyone. Updating bitcoind to a
version that includes this change will not cause any disruptions - existing
signet challenges will retain their original meaning without alteration.
The only parameter currently available is "target_spacing" (default 600
seconds). To set it, place "0x01<target_spacing as uint64_t, little endian>" in
the params. Empty params are also valid. If other network parameters are added
in the future, they should use "0x02<option 2 value>", "0x03<option 3 value>",
etc., following the protobuf style.
Two public functions were added to chainparams.h:
- ParseWrappedSignetChallenge: Extracts signet params and signet challenge
from a wrapped signet challenge.
- ParseSignetParams: Parses <params> bytes of the first data push.
Function ReadSigNetArgs calls ParseWrappedSignetChallenge and ParseSignetParams
to implement the new meaning of signetchallenge.
The description of the flag -signetchallenge was updated to reflect the changes.
A new unit tests file, chainparams_tests.cpp, was added, containing tests for
ParseWrappedSignetChallenge and ParseSignetParams.
The test signet_parse_tests from the file validation_tests.cpp was modified to
ensure proper understanding of the new logic.
In the functional test feature_signet.py, a test case was added with the value
of -signetchallenge set to the wrapped challenge, setting spacing to 30 seconds
and having the actual challenge OP_TRUE.
The Signet miner was updated, introducing a new option --target-spacing with a
default of 600 seconds. It must be set to the value used by the network.
Example.
I tested this commit against Mutinynet, a signet running on a custom fork of
Bitcoin Core, implementing 30s target spacing. I successfully synchronized the
blockchain using the following config:
signet=1
[signet]
signetchallenge=6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
The content of this wrapped challenge:
6a OP_RETURN
4c OP_PUSHDATA1
09 (length of signet params = 9)
011e00000000000000 (signet params: 0x01, pow_target_spacing=30)
4c OP_PUSHDATA1
25 (length of challenge = 37)
512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
(original Mutinynet challenge, can be found here:
https://blog.mutinywallet.com/mutinynet/ )
2024-01-31 16:15:57 -03:00
standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid, target_spacing=args.target_spacing)
2022-10-01 11:25:01 +10:00
2020-09-10 08:37:55 +10:00
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:
2022-10-01 11:25:01 +10:00
next_delta = gen.next_block_delta(int(bestheader["bits"], 16), bestheader["hash"])
2020-09-10 08:37:55 +10:00
next_delta += bestheader["time"] - time.time()
2022-10-01 11:25:01 +10:00
next_is_mine = gen.next_block_is_mine(bestheader["hash"])
2020-09-10 08:37:55 +10:00
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()
2022-10-01 13:15:19 +10:00
gen.next_block_time(now, bestheader, (mined_blocks == 0))
2020-09-10 08:37:55 +10:00
# ready to go? otherwise sleep and check for new block
2022-10-01 13:15:19 +10:00
if now < gen.action_time:
sleep_for = min(gen.action_time - now, 60)
if gen.mine_time < now:
2020-09-10 08:37:55 +10:00
# 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)
2022-10-01 13:15:19 +10:00
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))
2020-09-10 08:37:55 +10:00
time.sleep(sleep_for)
continue
# gbt
2022-10-01 13:19:22 +10:00
tmpl = gen.gbt(args.bcli, bci["bestblockhash"], now)
if tmpl is None:
2020-09-10 08:37:55 +10:00
continue
logging.debug("GBT template: %s", tmpl)
# address for reward
reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"])
# mine block
2022-10-01 13:15:19 +10:00
logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(gen.mine_time-bestheader["time"]), gen.mine_time, gen.is_mine)
2020-09-10 08:37:55 +10:00
mined_blocks += 1
2022-10-01 13:24:22 +10:00
block = gen.mine(args.bcli, args.grind_cmd, tmpl, reward_spk)
if block is None:
2020-09-10 08:37:55 +10:00
return 1
# submit block
2021-06-16 00:32:18 +02:00
r = args.bcli("-stdin", "submitblock", input=block.serialize().hex().encode('utf8'))
2020-09-10 08:37:55 +10:00
# report
2022-10-01 13:15:19 +10:00
bstr = "block" if gen.is_mine else "backup block"
2020-09-10 08:37:55 +10:00
2022-10-01 11:25:01 +10:00
next_delta = gen.next_block_delta(block.nBits, block.hash)
2020-09-10 08:37:55 +10:00
next_delta += block.nTime - time.time()
2022-10-01 11:25:01 +10:00
next_is_mine = gen.next_block_is_mine(block.hash)
2020-09-10 08:37:55 +10:00
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:
2021-01-13 11:55:52 +10:00
sys.stderr.write("Must specify 8 hex digits for --nbits\n")
2020-09-10 08:37:55 +10:00
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)
2024-08-10 02:11:26 +10:00
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)")
2020-09-10 08:37:55 +10:00
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)")
2022-09-29 14:02:58 +10:00
generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)")
Extend signetchallenge to set target block spacing
Inspired by https://github.com/bitcoin/bitcoin/pull/27446, this commit
implements the proposal detailed in the comment
https://github.com/bitcoin/bitcoin/pull/27446#issuecomment-1516600820.
Rationale.
Introduce the ability to configure a custom target time between blocks in a
custom Bitcoin signet network. This enhancement enables users to create a signet
that is more conducive to testing. The change enhances the flexibility of signet,
rendering it more versatile for various testing scenarios. For instance, I am
currently working on setting up a signet with a 30-second block time. However,
this caused numerous difficulty adjustments, resulting in an inconsistent
network state. Regtest isn't a viable alternative for me in this context since
we prefer defaults to utilize our custom signet when configured, without
impeding local regtest development.
Implementation.
If the challenge format is "OP_RETURN PUSHDATA<params> PUSHDATA<actual
challenge>", the actual challenge from the second data push is used as the
signet challenge, and the parameters from the first push are used to configure
the network. Otherwise the challenge is used as is.
Under the previous rules, such a signet challenge would always evaluate to
false, suggesting that it is likely not in use by anyone. Updating bitcoind to a
version that includes this change will not cause any disruptions - existing
signet challenges will retain their original meaning without alteration.
The only parameter currently available is "target_spacing" (default 600
seconds). To set it, place "0x01<target_spacing as uint64_t, little endian>" in
the params. Empty params are also valid. If other network parameters are added
in the future, they should use "0x02<option 2 value>", "0x03<option 3 value>",
etc., following the protobuf style.
Two public functions were added to chainparams.h:
- ParseWrappedSignetChallenge: Extracts signet params and signet challenge
from a wrapped signet challenge.
- ParseSignetParams: Parses <params> bytes of the first data push.
Function ReadSigNetArgs calls ParseWrappedSignetChallenge and ParseSignetParams
to implement the new meaning of signetchallenge.
The description of the flag -signetchallenge was updated to reflect the changes.
A new unit tests file, chainparams_tests.cpp, was added, containing tests for
ParseWrappedSignetChallenge and ParseSignetParams.
The test signet_parse_tests from the file validation_tests.cpp was modified to
ensure proper understanding of the new logic.
In the functional test feature_signet.py, a test case was added with the value
of -signetchallenge set to the wrapped challenge, setting spacing to 30 seconds
and having the actual challenge OP_TRUE.
The Signet miner was updated, introducing a new option --target-spacing with a
default of 600 seconds. It must be set to the value used by the network.
Example.
I tested this commit against Mutinynet, a signet running on a custom fork of
Bitcoin Core, implementing 30s target spacing. I successfully synchronized the
blockchain using the following config:
signet=1
[signet]
signetchallenge=6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
addnode=45.79.52.207:38333
dnsseed=0
The content of this wrapped challenge:
6a OP_RETURN
4c OP_PUSHDATA1
09 (length of signet params = 9)
011e00000000000000 (signet params: 0x01, pow_target_spacing=30)
4c OP_PUSHDATA1
25 (length of challenge = 37)
512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae
(original Mutinynet challenge, can be found here:
https://blog.mutinywallet.com/mutinynet/ )
2024-01-31 16:15:57 -03:00
generate.add_argument("--target-spacing", default=600, type=int, help="Target interval between blocks (seconds), property of the network (default 600)")
2020-09-10 08:37:55 +10:00
calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
calibrate.set_defaults(fn=do_calibrate)
2024-08-10 02:11:26 +10:00
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)
2020-09-10 08:37:55 +10:00
for sp in [genpsbt, generate]:
2024-08-10 02:11:26 +10:00
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/)")
2020-09-10 08:37:55 +10:00
for sp in [solvepsbt, generate, calibrate]:
2021-01-13 11:56:06 +10:00
sp.add_argument("--grind-cmd", default=None, type=str, required=(sp==calibrate), help="Command to grind a block header for proof-of-work")
2020-09-10 08:37:55 +10:00
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"):
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()