0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-08 10:31:50 -05: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/ )
This commit is contained in:
Boris Nagaev 2024-01-31 16:15:57 -03:00
parent 228aba2c4d
commit 7416b18392
No known key found for this signature in database
10 changed files with 320 additions and 19 deletions

View file

@ -223,12 +223,10 @@ def seconds_to_hms(s):
return out
class Generate:
INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800,
standby_delay=0, backup_delay=0, set_block_time=None,
poolid=None):
poolid=None, target_spacing=600):
if multiminer is None:
multiminer = (0, 1, 1)
(self.multi_low, self.multi_high, self.multi_period) = multiminer
@ -240,6 +238,10 @@ class Generate:
self.set_block_time = set_block_time
self.poolid = poolid
# 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
def next_block_delta(self, last_nbits, last_hash):
# strategy:
# 1) work out how far off our desired target we are
@ -377,8 +379,9 @@ def do_generate(args):
return 1
my_blocks = (start-1, stop, total)
if args.max_interval < 960:
logging.error("--max-interval must be at least 960 (16 minutes)")
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))
return 1
poolid = get_poolid(args)
@ -386,7 +389,7 @@ def do_generate(args):
ultimate_target = nbits_to_target(int(args.nbits,16))
gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval,
standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid)
standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid, target_spacing=args.target_spacing)
mined_blocks = 0
bestheader = {"hash": None}
@ -529,6 +532,7 @@ def main():
generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")
generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)")
generate.add_argument("--target-spacing", default=600, type=int, help="Target interval between blocks (seconds), property of the network (default 600)")
calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
calibrate.set_defaults(fn=do_calibrate)

View file

@ -10,6 +10,7 @@
#include <consensus/params.h>
#include <deploymentinfo.h>
#include <logging.h>
#include <script/script.h>
#include <tinyformat.h>
#include <util/chaintype.h>
#include <util/strencodings.h>
@ -21,8 +22,83 @@
#include <stdexcept>
#include <vector>
using util::SplitString;
void ParseWrappedSignetChallenge(const std::vector<uint8_t>& wrappedChallenge, std::vector<uint8_t>& outParams, std::vector<uint8_t>& outChallenge) {
if (wrappedChallenge.empty() || wrappedChallenge[0] != OP_RETURN) {
// Not a wrapped challenge.
outChallenge = wrappedChallenge;
return;
}
std::vector<uint8_t> params;
std::vector<uint8_t> challenge;
const CScript script(wrappedChallenge.begin(), wrappedChallenge.end());
CScript::const_iterator it = script.begin(), itend = script.end();
int i;
for (i = 0; it != itend; i++) {
if (i > 2) {
throw std::runtime_error("too many operations in wrapped challenge, must be 3.");
}
std::vector<unsigned char> push_data;
opcodetype opcode;
if (!script.GetOp(it, opcode, push_data)) {
throw std::runtime_error(strprintf("failed to parse operation %d in wrapped challenge script.", i));
}
if (i == 0) {
// OP_RETURN.
continue;
}
if (opcode != OP_PUSHDATA1 && opcode != OP_PUSHDATA2 && opcode != OP_PUSHDATA4) {
throw std::runtime_error(strprintf("operation %d of wrapped challenge script must be a PUSHDATA opcode, got 0x%02x.", i, opcode));
}
if (i == 1) {
params.swap(push_data);
} else if (i == 2) {
challenge.swap(push_data);
}
}
if (i != 3) {
throw std::runtime_error(strprintf("too few operations in wrapped challenge, must be 3, got %d.", i));
}
outParams.swap(params);
outChallenge.swap(challenge);
}
void ParseSignetParams(const std::vector<uint8_t>& params, CChainParams::SigNetOptions& options) {
if (params.empty()) {
return;
}
// The format of params is extendable in case more fields are added in the future.
// It is encoded as a concatenation of (field_id, value) tuples, protobuf style.
// Currently there is only one field defined: pow_target_spacing, whose field_id
// is 0x01 and the lendth of encoding is 8 (int64_t). So valid values of params are:
// - empty string (checked in if block above),
// - 0x01 followed by 8 bytes of pow_target_spacing (9 bytes in total).
// If length is not 0 and not 9, the value can not be parsed.
if (params.size() != 1 + 8) {
throw std::runtime_error(strprintf("signet params must have length %d, got %d.", 1+8, params.size()));
}
if (params[0] != 0x01) {
throw std::runtime_error(strprintf("signet params[0] must be 0x01, got 0x%02x.", params[0]));
}
// Parse little-endian 64 bit number to uint8_t.
const uint8_t* bytes = &params[1];
const uint64_t value = uint64_t(bytes[0]) | uint64_t(bytes[1])<<8 | uint64_t(bytes[2])<<16 | uint64_t(bytes[3])<<24 |
uint64_t(bytes[4])<<32 | uint64_t(bytes[5])<<40 | uint64_t(bytes[6])<<48 | uint64_t(bytes[7])<<56;
auto pow_target_spacing = int64_t(value);
if (pow_target_spacing <= 0) {
throw std::runtime_error("signet param pow_target_spacing <= 0.");
}
options.pow_target_spacing = pow_target_spacing;
}
void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& options)
{
if (args.IsArgSet("-signetseednode")) {
@ -37,7 +113,11 @@ void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& option
if (!val) {
throw std::runtime_error(strprintf("-signetchallenge must be hex, not '%s'.", signet_challenge[0]));
}
options.challenge.emplace(*val);
std::vector<unsigned char> params;
std::vector<unsigned char> challenge;
ParseWrappedSignetChallenge(*val, params, challenge);
ParseSignetParams(params, options);
options.challenge.emplace(challenge);
}
}

View file

@ -28,4 +28,26 @@ const CChainParams &Params();
*/
void SelectParams(const ChainType chain);
/**
* Extracts signet params and signet challenge from wrapped signet challenge.
* Format of wrapped signet challenge is:
* If the challenge is in the form "OP_RETURN PUSHDATA<params> PUSHDATA<actual challenge>",
* If the input challenge does not start with OP_RETURN,
* sets outParams="" and outChallenge=input.
* If the input challenge starts with OP_RETURN, but does not satisfy the format,
* throws an exception.
*/
void ParseWrappedSignetChallenge(const std::vector<uint8_t>& wrappedChallenge, std::vector<uint8_t>& outParams, std::vector<uint8_t>& outChallenge);
/**
* Parses signet options.
* The format currently supports only setting pow_target_spacing, but
* can be extended in the future.
* Possible values:
* - Empty (then do nothing)
* - 0x01 (pow_target_spacing as int64_t little endian) => set pow_target_spacing.
* If the format is wrong, throws an exception.
*/
void ParseSignetParams(const std::vector<uint8_t>& params, CChainParams::SigNetOptions& options);
#endif // BITCOIN_CHAINPARAMS_H

View file

@ -21,7 +21,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signet", "Use the signet chain. Equivalent to -chain=signet. Note that the network is defined by the -signetchallenge parameter", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge); in case -signetchallenge is in the form of 'OP_RETURN PUSHDATA<params> PUSHDATA<actual challenge>', then <actual challenge> is used as a challenge and <params> is used to set parameters of signet; currently the only supported parameter is target spacing, the format of <params> to set it is 01<8 bytes value of target spacing, seconds, little endian>", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signetseednode", "Specify a seed node for the signet network, in the hostname[:port] format, e.g. sig.net:1234 (may be used multiple times to specify multiple seed nodes; defaults to the global default signet test network seed node(s))", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS);
}

View file

@ -461,7 +461,7 @@ public:
consensus.CSVHeight = 1;
consensus.SegwitHeight = 1;
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
consensus.nPowTargetSpacing = 10 * 60;
consensus.nPowTargetSpacing = options.pow_target_spacing;
consensus.fPowAllowMinDifficultyBlocks = false;
consensus.enforce_BIP94 = false;
consensus.fPowNoRetargeting = false;

View file

@ -137,6 +137,7 @@ public:
struct SigNetOptions {
std::optional<std::vector<uint8_t>> challenge{};
std::optional<std::vector<std::string>> seeds{};
int64_t pow_target_spacing{10 * 60};
};
/**

View file

@ -47,6 +47,7 @@ add_executable(test_bitcoin
blockmanager_tests.cpp
bloom_tests.cpp
bswap_tests.cpp
chainparams_tests.cpp
checkqueue_tests.cpp
cluster_linearize_tests.cpp
coins_tests.cpp

View file

@ -0,0 +1,170 @@
// Copyright (c) 2011-2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h>
#include <boost/test/unit_test.hpp>
#include <util/strencodings.h>
using namespace std::literals;
BOOST_AUTO_TEST_SUITE(chainparams_tests)
struct ParseWrappedSignetChallenge_TestCase
{
std::string wrappedChallengeHex;
std::string wantParamsHex;
std::string wantChallengeHex;
std::string wantError;
};
BOOST_AUTO_TEST_CASE(parse_wrapped_signet_challenge)
{
static const ParseWrappedSignetChallenge_TestCase cases[] = {
{
"512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae",
"",
"512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae",
"",
},
{
"6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
"011e00000000000000",
"512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
"",
},
{
"6a4c004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
"",
"512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
"",
},
{
"6a4c004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae4c00",
"",
"",
"too many operations in wrapped challenge, must be 3.",
},
{
"6a4c09011e00000000000000",
"",
"",
"too few operations in wrapped challenge, must be 3, got 2.",
},
{
"6a4c01",
"",
"",
"failed to parse operation 1 in wrapped challenge script.",
},
{
"6a4c004c25512102f7561d208dd9ae99bf497273",
"",
"",
"failed to parse operation 2 in wrapped challenge script.",
},
{
"6a6a4c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae4c00",
"",
"",
"operation 1 of wrapped challenge script must be a PUSHDATA opcode, got 0x6a.",
},
{
"6a4c09011e0000000000000051",
"",
"",
"operation 2 of wrapped challenge script must be a PUSHDATA opcode, got 0x51.",
},
};
for (unsigned int i=0; i<std::size(cases); i++)
{
const auto wrappedChallenge = ParseHex(cases[i].wrappedChallengeHex);
const auto wantParamsHex = cases[i].wantParamsHex;
const auto wantChallengeHex = cases[i].wantChallengeHex;
const auto wantError = cases[i].wantError;
std::vector<uint8_t> gotParams;
std::vector<uint8_t> gotChallenge;
std::string gotError;
try {
ParseWrappedSignetChallenge(wrappedChallenge, gotParams, gotChallenge);
} catch (const std::exception& e) {
gotError = e.what();
}
BOOST_CHECK_EQUAL(HexStr(gotParams), wantParamsHex);
BOOST_CHECK_EQUAL(HexStr(gotChallenge), wantChallengeHex);
BOOST_CHECK_EQUAL(gotError, wantError);
}
}
struct ParseSignetParams_TestCase
{
std::string paramsHex;
int64_t wantPowTargetSpacing;
std::string wantError;
};
BOOST_AUTO_TEST_CASE(parse_signet_params)
{
static const ParseSignetParams_TestCase cases[] = {
{
"",
600,
"",
},
{
"011e00000000000000",
30,
"",
},
{
"01e803000000000000",
1000,
"",
},
{
"015802000000000000",
600,
"",
},
{
"012502",
600,
"signet params must have length 9, got 3.",
},
{
"022502000000000000",
600,
"signet params[0] must be 0x01, got 0x02.",
},
{
"01ffffffffffffffff",
600,
"signet param pow_target_spacing <= 0.",
},
};
for (unsigned int i=0; i<std::size(cases); i++)
{
const auto params = ParseHex(cases[i].paramsHex);
const auto wantPowTargetSpacing = cases[i].wantPowTargetSpacing;
const auto wantError = cases[i].wantError;
CChainParams::SigNetOptions gotOptions;
std::string gotError;
try {
ParseSignetParams(params, gotOptions);
} catch (const std::exception& e) {
gotError = e.what();
}
BOOST_CHECK_EQUAL(gotOptions.pow_target_spacing, wantPowTargetSpacing);
BOOST_CHECK_EQUAL(gotError, wantError);
}
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -75,6 +75,15 @@ BOOST_AUTO_TEST_CASE(signet_parse_tests)
BOOST_CHECK(signet_params->GetConsensus().signet_challenge == std::vector<uint8_t>{OP_TRUE});
CScript challenge{OP_TRUE};
{
// Wrapped challenge case.
ArgsManager signet_argsman_custom_spacing;
signet_argsman_custom_spacing.ForceSetArg("-signetchallenge", "6a4c09011e000000000000004c0151"); // set challenge to OP_TRUE and spacing to 30 seconds
const auto signet_params_custom_spacing = CreateChainParams(signet_argsman_custom_spacing, ChainType::SIGNET);
BOOST_CHECK(signet_params_custom_spacing->GetConsensus().signet_challenge == std::vector<uint8_t>{OP_TRUE});
BOOST_CHECK(signet_params_custom_spacing->GetConsensus().nPowTargetSpacing == 30);
}
// empty block is invalid
BOOST_CHECK(!SignetTxs::Create(block, challenge));
BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus()));

View file

@ -25,7 +25,7 @@ signet_blocks = [
]
class SignetParams:
def __init__(self, challenge=None):
def __init__(self, challenge=None, internal_challenge=None):
if challenge is None:
self.challenge = SIGNET_DEFAULT_CHALLENGE
self.shared_args = []
@ -33,22 +33,28 @@ class SignetParams:
self.challenge = challenge
self.shared_args = [f"-signetchallenge={challenge}"]
if internal_challenge is None:
internal_challenge = self.challenge
self.internal_challenge = internal_challenge
class SignetBasicTest(BitcoinTestFramework):
def set_test_params(self):
self.chain = "signet"
self.num_nodes = 6
self.num_nodes = 8
self.setup_clean_chain = True
self.signets = [
SignetParams(challenge='51'), # OP_TRUE
SignetParams(), # default challenge
# default challenge as a 2-of-2, which means it should fail
SignetParams(challenge='522103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae')
SignetParams(challenge='522103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae'),
SignetParams(challenge='6a4c09011e000000000000004c0151', internal_challenge='51'), # OP_TRUE, target_spacing=30
]
self.extra_args = [
self.signets[0].shared_args, self.signets[0].shared_args,
self.signets[1].shared_args, self.signets[1].shared_args,
self.signets[2].shared_args, self.signets[2].shared_args,
self.signets[3].shared_args, self.signets[3].shared_args,
]
def setup_network(self):
@ -58,6 +64,7 @@ class SignetBasicTest(BitcoinTestFramework):
self.connect_nodes(0, 1)
self.connect_nodes(2, 3)
self.connect_nodes(4, 5)
self.connect_nodes(6, 7)
def run_test(self):
self.log.info("basic tests using OP_TRUE challenge")
@ -66,10 +73,11 @@ class SignetBasicTest(BitcoinTestFramework):
def check_getblockchaininfo(node_idx, signet_idx):
blockchain_info = self.nodes[node_idx].getblockchaininfo()
assert_equal(blockchain_info['chain'], 'signet')
assert_equal(blockchain_info['signet_challenge'], self.signets[signet_idx].challenge)
assert_equal(blockchain_info['signet_challenge'], self.signets[signet_idx].internal_challenge)
check_getblockchaininfo(node_idx=1, signet_idx=0)
check_getblockchaininfo(node_idx=2, signet_idx=1)
check_getblockchaininfo(node_idx=5, signet_idx=2)
check_getblockchaininfo(node_idx=6, signet_idx=3)
self.log.info('getmininginfo')
def check_getmininginfo(node_idx, signet_idx):
@ -80,20 +88,26 @@ class SignetBasicTest(BitcoinTestFramework):
assert 'currentblockweight' not in mining_info
assert_equal(mining_info['networkhashps'], Decimal('0'))
assert_equal(mining_info['pooledtx'], 0)
assert_equal(mining_info['signet_challenge'], self.signets[signet_idx].challenge)
assert_equal(mining_info['signet_challenge'], self.signets[signet_idx].internal_challenge)
check_getmininginfo(node_idx=0, signet_idx=0)
check_getmininginfo(node_idx=3, signet_idx=1)
check_getmininginfo(node_idx=4, signet_idx=2)
check_getmininginfo(node_idx=7, signet_idx=3)
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
self.log.info("pregenerated signet blocks check")
height = 0
for block in signet_blocks:
assert_equal(self.nodes[2].submitblock(block), None)
height += 1
assert_equal(self.nodes[2].getblockcount(), height)
# Verify that nodes accept blocks mined using the default signet challenge.
# This test includes one default signet node (2) and another node (6)
# configured with an extended signet challenge that uses the actual script
# OP_TRUE (accepts all blocks).
for node_idx in [2, 6]:
height = 0
for block in signet_blocks:
assert_equal(self.nodes[node_idx].submitblock(block), None)
height += 1
assert_equal(self.nodes[node_idx].getblockcount(), height)
self.log.info("pregenerated signet blocks check (incompatible solution)")