Skip to content

Commit 557f7db

Browse files
committed
Extend signetchallenge to set target block spacing
Inspired by bitcoin#27446, this commit implements the proposal detailed in the comment bitcoin#27446 (comment). 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. This change is backward-compatible, as per the old rules, such a signet challenge would always evaluate to false, rendering it unused. 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, the value of -signetchallenge was changed from OP_TRUE to the wrapped challenge, setting spacing to 30 seconds and having the same 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/ )
1 parent 4d7d7fd commit 557f7db

10 files changed

+288
-12
lines changed

contrib/signet/miner

+10-8
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,15 @@ def seconds_to_hms(s):
225225
out = "-" + out
226226
return out
227227

228-
def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson, max_interval):
228+
def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson, max_interval, target_spacing):
229229
# strategy:
230230
# 1) work out how far off our desired target we are
231231
# 2) cap it to a factor of 4 since that's the best we can do in a single retarget period
232232
# 3) use that to work out the desired average interval in this retarget period
233233
# 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
234234
# 5) cap the resulting interval between 1 second and 1 hour to avoid extremes
235235

236-
INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
236+
INTERVAL = float(target_spacing)*2016/2015 # 10 minutes, adjusted for the off-by-one bug
237237

238238
current_target = nbits_to_target(last_nbits)
239239
retarget_factor = ultimate_target / current_target
@@ -308,8 +308,9 @@ def do_generate(args):
308308
return 1
309309
my_blocks = (start-1, stop, total)
310310

311-
if args.max_interval < 960:
312-
logging.error("--max-interval must be at least 960 (16 minutes)")
311+
max_interval_limit = args.target_spacing * 16 / 10
312+
if args.max_interval < max_interval_limit:
313+
logging.error("--max-interval must be at least %d (%f minutes)" % (max_interval_limit, max_interval_limit/60))
313314
return 1
314315

315316
ultimate_target = nbits_to_target(int(args.nbits,16))
@@ -328,7 +329,7 @@ def do_generate(args):
328329
if lastheader is None:
329330
lastheader = bestheader["hash"]
330331
elif bestheader["hash"] != lastheader:
331-
next_delta = next_block_delta(int(bestheader["bits"], 16), bestheader["hash"], ultimate_target, args.poisson, args.max_interval)
332+
next_delta = next_block_delta(int(bestheader["bits"], 16), bestheader["hash"], ultimate_target, args.poisson, args.max_interval, args.target_spacing)
332333
next_delta += bestheader["time"] - time.time()
333334
next_is_mine = next_block_is_mine(bestheader["hash"], my_blocks)
334335
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"))
@@ -342,14 +343,14 @@ def do_generate(args):
342343
action_time = now
343344
is_mine = True
344345
elif bestheader["height"] == 0:
345-
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson, args.max_interval)
346+
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson, args.max_interval, args.target_spacing)
346347
time_delta *= 100 # 100 blocks
347348
logging.info("Backdating time for first block to %d minutes ago" % (time_delta/60))
348349
mine_time = now - time_delta
349350
action_time = now
350351
is_mine = True
351352
else:
352-
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson, args.max_interval)
353+
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson, args.max_interval, args.target_spacing)
353354
mine_time = bestheader["time"] + time_delta
354355

355356
is_mine = next_block_is_mine(bci["bestblockhash"], my_blocks)
@@ -423,7 +424,7 @@ def do_generate(args):
423424
# report
424425
bstr = "block" if is_mine else "backup block"
425426

426-
next_delta = next_block_delta(block.nBits, block.hash, ultimate_target, args.poisson, args.max_interval)
427+
next_delta = next_block_delta(block.nBits, block.hash, ultimate_target, args.poisson, args.max_interval, args.target_spacing)
427428
next_delta += block.nTime - time.time()
428429
next_is_mine = next_block_is_mine(block.hash, my_blocks)
429430

@@ -502,6 +503,7 @@ def main():
502503
generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
503504
generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")
504505
generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)")
506+
generate.add_argument("--target-spacing", default=600, type=int, help="Target interval between blocks (seconds), property of the network (default 600)")
505507

506508
calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
507509
calibrate.set_defaults(fn=do_calibrate)

src/Makefile.test.include

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ BITCOIN_TESTS =\
8282
test/blockmanager_tests.cpp \
8383
test/bloom_tests.cpp \
8484
test/bswap_tests.cpp \
85+
test/chainparams_tests.cpp \
8586
test/checkqueue_tests.cpp \
8687
test/coins_tests.cpp \
8788
test/coinstatsindex_tests.cpp \

src/chainparams.cpp

+72-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <consensus/params.h>
1111
#include <deploymentinfo.h>
1212
#include <logging.h>
13+
#include <script/script.h>
1314
#include <tinyformat.h>
1415
#include <util/chaintype.h>
1516
#include <util/strencodings.h>
@@ -21,6 +22,72 @@
2122
#include <stdexcept>
2223
#include <vector>
2324

25+
void ParseWrappedSignetChallenge(const std::vector<uint8_t>& wrappedChallenge, std::vector<uint8_t>& outParams, std::vector<uint8_t>& outChallenge) {
26+
if (wrappedChallenge.empty() || wrappedChallenge[0] != OP_RETURN) {
27+
// Not a wrapped challenge.
28+
outChallenge = wrappedChallenge;
29+
return;
30+
}
31+
32+
std::vector<uint8_t> params;
33+
std::vector<uint8_t> challenge;
34+
35+
const CScript script(wrappedChallenge.begin(), wrappedChallenge.end());
36+
CScript::const_iterator it = script.begin(), itend = script.end();
37+
int i;
38+
for (i = 0; it != itend; i++) {
39+
if (i > 2) {
40+
throw std::runtime_error("too many operations in wrapped challenge, must be 3.");
41+
}
42+
std::vector<unsigned char> push_data;
43+
opcodetype opcode;
44+
if (!script.GetOp(it, opcode, push_data)) {
45+
throw std::runtime_error(strprintf("failed to parse operation %d in wrapped challenge script.", i));
46+
}
47+
if (i == 0) {
48+
// OP_RETURN.
49+
continue;
50+
}
51+
if (opcode != OP_PUSHDATA1 && opcode != OP_PUSHDATA2 && opcode != OP_PUSHDATA4) {
52+
throw std::runtime_error(strprintf("operation %d of wrapped challenge script must be a PUSHDATA opcode, got 0x%02x.", i, opcode));
53+
}
54+
if (i == 1) {
55+
params.swap(push_data);
56+
} else if (i == 2) {
57+
challenge.swap(push_data);
58+
}
59+
}
60+
if (i != 3) {
61+
throw std::runtime_error(strprintf("too few operations in wrapped challenge, must be 3, got %d.", i));
62+
}
63+
64+
outParams.swap(params);
65+
outChallenge.swap(challenge);
66+
}
67+
68+
void ParseSignetParams(const std::vector<uint8_t>& params, CChainParams::SigNetOptions& options) {
69+
if (params.empty()) {
70+
return;
71+
}
72+
73+
if (params.size() != 1 + 8) {
74+
throw std::runtime_error(strprintf("signet params must have length %d, got %d.", 1+8, params.size()));
75+
}
76+
if (params[0] != 0x01) {
77+
throw std::runtime_error(strprintf("signet params[0] must be 0x01, got 0x%02x.", params[0]));
78+
}
79+
// Parse little-endian 64 bit number to uint8_t.
80+
const uint8_t* bytes = &params[1];
81+
const uint64_t value = uint64_t(bytes[0]) | uint64_t(bytes[1])<<8 | uint64_t(bytes[2])<<16 | uint64_t(bytes[3])<<24 |
82+
uint64_t(bytes[4])<<32 | uint64_t(bytes[5])<<40 | uint64_t(bytes[6])<<48 | uint64_t(bytes[7])<<56;
83+
auto pow_target_spacing = int64_t(value);
84+
if (pow_target_spacing <= 0) {
85+
throw std::runtime_error("signet param pow_target_spacing <= 0.");
86+
}
87+
88+
options.pow_target_spacing = pow_target_spacing;
89+
}
90+
2491
void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& options)
2592
{
2693
if (args.IsArgSet("-signetseednode")) {
@@ -35,7 +102,11 @@ void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& option
35102
if (!val) {
36103
throw std::runtime_error(strprintf("-signetchallenge must be hex, not '%s'.", signet_challenge[0]));
37104
}
38-
options.challenge.emplace(*val);
105+
std::vector<unsigned char> params;
106+
std::vector<unsigned char> challenge;
107+
ParseWrappedSignetChallenge(*val, params, challenge);
108+
ParseSignetParams(params, options);
109+
options.challenge.emplace(challenge);
39110
}
40111
}
41112

src/chainparams.h

+22
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,26 @@ const CChainParams &Params();
2828
*/
2929
void SelectParams(const ChainType chain);
3030

31+
/**
32+
* Extracts signet params and signet challenge from wrapped signet challenge.
33+
* Format of wrapped signet challenge is:
34+
* If the challenge is in the form "OP_RETURN PUSHDATA<params> PUSHDATA<actual challenge>",
35+
* If the input challenge does not start with OP_RETURN,
36+
* sets outParams="" and outChallenge=input.
37+
* If the input challenge starts with OP_RETURN, but does not satisfy the format,
38+
* throws an exception.
39+
*/
40+
void ParseWrappedSignetChallenge(const std::vector<uint8_t>& wrappedChallenge, std::vector<uint8_t>& outParams, std::vector<uint8_t>& outChallenge);
41+
42+
/**
43+
* Parses signet options.
44+
* The format currently supports only setting pow_target_spacing, but
45+
* can be extended in the future.
46+
* Possible values:
47+
* - Empty (then do nothing)
48+
* - 0x01 (pow_target_spacing as int64_t little endian) => set pow_target_spacing.
49+
* If the format is wrong, throws an exception.
50+
*/
51+
void ParseSignetParams(const std::vector<uint8_t>& params, CChainParams::SigNetOptions& options);
52+
3153
#endif // BITCOIN_CHAINPARAMS_H

src/chainparamsbase.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
2020
argsman.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
2121
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);
2222
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);
23-
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);
23+
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);
2424
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);
2525
}
2626

src/kernel/chainparams.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ class SigNetParams : public CChainParams {
341341
consensus.CSVHeight = 1;
342342
consensus.SegwitHeight = 1;
343343
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
344-
consensus.nPowTargetSpacing = 10 * 60;
344+
consensus.nPowTargetSpacing = options.pow_target_spacing;
345345
consensus.fPowAllowMinDifficultyBlocks = false;
346346
consensus.fPowNoRetargeting = false;
347347
consensus.nRuleChangeActivationThreshold = 1815; // 90% of 2016

src/kernel/chainparams.h

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class CChainParams
136136
struct SigNetOptions {
137137
std::optional<std::vector<uint8_t>> challenge{};
138138
std::optional<std::vector<std::string>> seeds{};
139+
int64_t pow_target_spacing{10 * 60};
139140
};
140141

141142
/**

src/test/chainparams_tests.cpp

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright (c) 2011-2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <chainparams.h>
6+
7+
#include <boost/test/unit_test.hpp>
8+
9+
#include <util/strencodings.h>
10+
11+
using namespace std::literals;
12+
13+
BOOST_AUTO_TEST_SUITE(chainparams_tests)
14+
15+
struct ParseWrappedSignetChallenge_TestCase
16+
{
17+
std::string wrappedChallengeHex;
18+
std::string wantParamsHex;
19+
std::string wantChallengeHex;
20+
std::string wantError;
21+
};
22+
23+
BOOST_AUTO_TEST_CASE(parse_wrapped_signet_challenge)
24+
{
25+
static const ParseWrappedSignetChallenge_TestCase cases[] = {
26+
{
27+
"512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae",
28+
"",
29+
"512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae",
30+
"",
31+
},
32+
{
33+
"6a4c09011e000000000000004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
34+
"011e00000000000000",
35+
"512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
36+
"",
37+
},
38+
{
39+
"6a4c004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
40+
"",
41+
"512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae",
42+
"",
43+
},
44+
{
45+
"6a4c004c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae4c00",
46+
"",
47+
"",
48+
"too many operations in wrapped challenge, must be 3.",
49+
},
50+
{
51+
"6a4c09011e00000000000000",
52+
"",
53+
"",
54+
"too few operations in wrapped challenge, must be 3, got 2.",
55+
},
56+
{
57+
"6a4c01",
58+
"",
59+
"",
60+
"failed to parse operation 1 in wrapped challenge script.",
61+
},
62+
{
63+
"6a4c004c25512102f7561d208dd9ae99bf497273",
64+
"",
65+
"",
66+
"failed to parse operation 2 in wrapped challenge script.",
67+
},
68+
{
69+
"6a6a4c25512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae4c00",
70+
"",
71+
"",
72+
"operation 1 of wrapped challenge script must be a PUSHDATA opcode, got 0x6a.",
73+
},
74+
{
75+
"6a4c09011e0000000000000051",
76+
"",
77+
"",
78+
"operation 2 of wrapped challenge script must be a PUSHDATA opcode, got 0x51.",
79+
},
80+
};
81+
82+
for (unsigned int i=0; i<std::size(cases); i++)
83+
{
84+
const auto wrappedChallenge = ParseHex(cases[i].wrappedChallengeHex);
85+
const auto wantParamsHex = cases[i].wantParamsHex;
86+
const auto wantChallengeHex = cases[i].wantChallengeHex;
87+
const auto wantError = cases[i].wantError;
88+
89+
std::vector<uint8_t> gotParams;
90+
std::vector<uint8_t> gotChallenge;
91+
std::string gotError;
92+
try {
93+
ParseWrappedSignetChallenge(wrappedChallenge, gotParams, gotChallenge);
94+
} catch (const std::exception& e) {
95+
gotError = e.what();
96+
}
97+
98+
BOOST_CHECK_EQUAL(HexStr(gotParams), wantParamsHex);
99+
BOOST_CHECK_EQUAL(HexStr(gotChallenge), wantChallengeHex);
100+
BOOST_CHECK_EQUAL(gotError, wantError);
101+
}
102+
}
103+
104+
struct ParseSignetParams_TestCase
105+
{
106+
std::string paramsHex;
107+
int64_t wantPowTargetSpacing;
108+
std::string wantError;
109+
};
110+
111+
BOOST_AUTO_TEST_CASE(parse_signet_params)
112+
{
113+
static const ParseSignetParams_TestCase cases[] = {
114+
{
115+
"",
116+
600,
117+
"",
118+
},
119+
{
120+
"011e00000000000000",
121+
30,
122+
"",
123+
},
124+
{
125+
"01e803000000000000",
126+
1000,
127+
"",
128+
},
129+
{
130+
"015802000000000000",
131+
600,
132+
"",
133+
},
134+
{
135+
"012502",
136+
600,
137+
"signet params must have length 9, got 3.",
138+
},
139+
{
140+
"022502000000000000",
141+
600,
142+
"signet params[0] must be 0x01, got 0x02.",
143+
},
144+
{
145+
"01ffffffffffffffff",
146+
600,
147+
"signet param pow_target_spacing <= 0.",
148+
},
149+
};
150+
151+
for (unsigned int i=0; i<std::size(cases); i++)
152+
{
153+
const auto params = ParseHex(cases[i].paramsHex);
154+
const auto wantPowTargetSpacing = cases[i].wantPowTargetSpacing;
155+
const auto wantError = cases[i].wantError;
156+
157+
CChainParams::SigNetOptions gotOptions;
158+
std::string gotError;
159+
try {
160+
ParseSignetParams(params, gotOptions);
161+
} catch (const std::exception& e) {
162+
gotError = e.what();
163+
}
164+
165+
BOOST_CHECK_EQUAL(gotOptions.pow_target_spacing, wantPowTargetSpacing);
166+
BOOST_CHECK_EQUAL(gotError, wantError);
167+
}
168+
}
169+
170+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)