Skip to content

Commit 41728b5

Browse files
committed
Extend signetchallenge to set target block spacing
Inspired by #27446, this commit implements the proposal detailed in the comment #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. 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/ )
1 parent 5b8046a commit 41728b5

File tree

10 files changed

+320
-19
lines changed

10 files changed

+320
-19
lines changed

contrib/signet/miner

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,10 @@ def seconds_to_hms(s):
223223
return out
224224

225225
class Generate:
226-
INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
227-
228226

229227
def __init__(self, multiminer=None, ultimate_target=None, poisson=False, max_interval=1800,
230228
standby_delay=0, backup_delay=0, set_block_time=None,
231-
poolid=None):
229+
poolid=None, target_spacing=600):
232230
if multiminer is None:
233231
multiminer = (0, 1, 1)
234232
(self.multi_low, self.multi_high, self.multi_period) = multiminer
@@ -240,6 +238,10 @@ class Generate:
240238
self.set_block_time = set_block_time
241239
self.poolid = poolid
242240

241+
# Set INTERVAL. If target_spacing=600 (the default), it is 10 minutes,
242+
# adjusted for the off-by-one bug.
243+
self.INTERVAL = target_spacing * 2016 / 2015
244+
243245
def next_block_delta(self, last_nbits, last_hash):
244246
# strategy:
245247
# 1) work out how far off our desired target we are
@@ -377,16 +379,17 @@ def do_generate(args):
377379
return 1
378380
my_blocks = (start-1, stop, total)
379381

380-
if args.max_interval < 960:
381-
logging.error("--max-interval must be at least 960 (16 minutes)")
382+
max_interval_limit = args.target_spacing * 16 / 10
383+
if args.max_interval < max_interval_limit:
384+
logging.error("--max-interval must be at least %d (%f minutes)" % (max_interval_limit, max_interval_limit/60))
382385
return 1
383386

384387
poolid = get_poolid(args)
385388

386389
ultimate_target = nbits_to_target(int(args.nbits,16))
387390

388391
gen = Generate(multiminer=my_blocks, ultimate_target=ultimate_target, poisson=args.poisson, max_interval=args.max_interval,
389-
standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid)
392+
standby_delay=args.standby_delay, backup_delay=args.backup_delay, set_block_time=args.set_block_time, poolid=poolid, target_spacing=args.target_spacing)
390393

391394
mined_blocks = 0
392395
bestheader = {"hash": None}
@@ -529,6 +532,7 @@ def main():
529532
generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
530533
generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")
531534
generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)")
535+
generate.add_argument("--target-spacing", default=600, type=int, help="Target interval between blocks (seconds), property of the network (default 600)")
532536

533537
calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
534538
calibrate.set_defaults(fn=do_calibrate)

src/chainparams.cpp

Lines changed: 81 additions & 1 deletion
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,8 +22,83 @@
2122
#include <stdexcept>
2223
#include <vector>
2324

25+
2426
using util::SplitString;
2527

28+
void ParseWrappedSignetChallenge(const std::vector<uint8_t>& wrappedChallenge, std::vector<uint8_t>& outParams, std::vector<uint8_t>& outChallenge) {
29+
if (wrappedChallenge.empty() || wrappedChallenge[0] != OP_RETURN) {
30+
// Not a wrapped challenge.
31+
outChallenge = wrappedChallenge;
32+
return;
33+
}
34+
35+
std::vector<uint8_t> params;
36+
std::vector<uint8_t> challenge;
37+
38+
const CScript script(wrappedChallenge.begin(), wrappedChallenge.end());
39+
CScript::const_iterator it = script.begin(), itend = script.end();
40+
int i;
41+
for (i = 0; it != itend; i++) {
42+
if (i > 2) {
43+
throw std::runtime_error("too many operations in wrapped challenge, must be 3.");
44+
}
45+
std::vector<unsigned char> push_data;
46+
opcodetype opcode;
47+
if (!script.GetOp(it, opcode, push_data)) {
48+
throw std::runtime_error(strprintf("failed to parse operation %d in wrapped challenge script.", i));
49+
}
50+
if (i == 0) {
51+
// OP_RETURN.
52+
continue;
53+
}
54+
if (opcode != OP_PUSHDATA1 && opcode != OP_PUSHDATA2 && opcode != OP_PUSHDATA4) {
55+
throw std::runtime_error(strprintf("operation %d of wrapped challenge script must be a PUSHDATA opcode, got 0x%02x.", i, opcode));
56+
}
57+
if (i == 1) {
58+
params.swap(push_data);
59+
} else if (i == 2) {
60+
challenge.swap(push_data);
61+
}
62+
}
63+
if (i != 3) {
64+
throw std::runtime_error(strprintf("too few operations in wrapped challenge, must be 3, got %d.", i));
65+
}
66+
67+
outParams.swap(params);
68+
outChallenge.swap(challenge);
69+
}
70+
71+
void ParseSignetParams(const std::vector<uint8_t>& params, CChainParams::SigNetOptions& options) {
72+
if (params.empty()) {
73+
return;
74+
}
75+
76+
// The format of params is extendable in case more fields are added in the future.
77+
// It is encoded as a concatenation of (field_id, value) tuples, protobuf style.
78+
// Currently there is only one field defined: pow_target_spacing, whose field_id
79+
// is 0x01 and the length of encoding is 8 (int64_t). So valid values of params are:
80+
// - empty string (checked in if block above),
81+
// - 0x01 followed by 8 bytes of pow_target_spacing (9 bytes in total).
82+
// If length is not 0 and not 9, the value can not be parsed.
83+
84+
if (params.size() != 1 + 8) {
85+
throw std::runtime_error(strprintf("signet params must have length %d, got %d.", 1+8, params.size()));
86+
}
87+
if (params[0] != 0x01) {
88+
throw std::runtime_error(strprintf("signet params[0] must be 0x01, got 0x%02x.", params[0]));
89+
}
90+
// Parse little-endian 64 bit number to uint64_t.
91+
const uint8_t* bytes = &params[1];
92+
const uint64_t value = uint64_t(bytes[0]) | uint64_t(bytes[1])<<8 | uint64_t(bytes[2])<<16 | uint64_t(bytes[3])<<24 |
93+
uint64_t(bytes[4])<<32 | uint64_t(bytes[5])<<40 | uint64_t(bytes[6])<<48 | uint64_t(bytes[7])<<56;
94+
auto pow_target_spacing = int64_t(value);
95+
if (pow_target_spacing <= 0) {
96+
throw std::runtime_error("signet param pow_target_spacing <= 0.");
97+
}
98+
99+
options.pow_target_spacing = pow_target_spacing;
100+
}
101+
26102
void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& options)
27103
{
28104
if (!args.GetArgs("-signetseednode").empty()) {
@@ -37,7 +113,11 @@ void ReadSigNetArgs(const ArgsManager& args, CChainParams::SigNetOptions& option
37113
if (!val) {
38114
throw std::runtime_error(strprintf("-signetchallenge must be hex, not '%s'.", signet_challenge[0]));
39115
}
40-
options.challenge.emplace(*val);
116+
std::vector<unsigned char> params;
117+
std::vector<unsigned char> challenge;
118+
ParseWrappedSignetChallenge(*val, params, challenge);
119+
ParseSignetParams(params, options);
120+
options.challenge.emplace(challenge);
41121
}
42122
}
43123

src/chainparams.h

Lines changed: 22 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
2121
argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
2222
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);
2323
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);
24-
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);
24+
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);
2525
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);
2626
}
2727

src/kernel/chainparams.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ class SigNetParams : public CChainParams {
442442
consensus.CSVHeight = 1;
443443
consensus.SegwitHeight = 1;
444444
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
445-
consensus.nPowTargetSpacing = 10 * 60;
445+
consensus.nPowTargetSpacing = options.pow_target_spacing;
446446
consensus.fPowAllowMinDifficultyBlocks = false;
447447
consensus.enforce_BIP94 = false;
448448
consensus.fPowNoRetargeting = false;

src/kernel/chainparams.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class CChainParams
124124
struct SigNetOptions {
125125
std::optional<std::vector<uint8_t>> challenge{};
126126
std::optional<std::vector<std::string>> seeds{};
127+
int64_t pow_target_spacing{10 * 60};
127128
};
128129

129130
/**

src/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_executable(test_bitcoin
2525
blockmanager_tests.cpp
2626
bloom_tests.cpp
2727
bswap_tests.cpp
28+
chainparams_tests.cpp
2829
chainstate_write_tests.cpp
2930
checkqueue_tests.cpp
3031
cluster_linearize_tests.cpp

src/test/chainparams_tests.cpp

Lines changed: 170 additions & 0 deletions
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)