Skip to content

Commit 2c7a423

Browse files
committed
Merge bitcoin/bitcoin#30569: node: reduce unsafe uint256S usage
18d65d2 test: use uint256::FromUserHex for RANDOM_CTX_SEED (stickies-v) 6819e5a node: use uint256::FromUserHex for -assumevalid parsing (stickies-v) 2e58fdb util: remove unused IsHexNumber (stickies-v) 8a44d7d node: use uint256::FromUserHex for -minimumchainwork parsing (stickies-v) 70e2c87 refactor: add uint256::FromUserHex helper (stickies-v) 85b7cbf test: unittest chainstatemanager_args (stickies-v) Pull request description: Since fad2991, `uint256S` has been [deprecated](bitcoin/bitcoin@fad2991#diff-800776e2dda39116e889839f69409571a5d397de048a141da7e4003bc099e3e2R138) because it is less robust than the `base_blob::FromHex()` introduced in [the same PR](bitcoin/bitcoin#30482). Specifically, it tries to recover from length-mismatches, recover from untrimmed whitespace, 0x-prefix and garbage at the end, instead of simply requiring exactly 64 hex-only characters. _(see also #30532)_ This PR carves out the few `uint256S` callsites that may potentially prove a bit more controversial to change because they deal with user input and backwards incompatible behaviour change. The main behaviour change introduced in this PR is: - `-minimumchainwork` will raise an error when input is longer than 64 hex digits - `-assumevalid` will raise an error when input contains invalid hex characters, or when it is longer than 64 hex digits - test: the optional RANDOM_CTX_SEED env var will now cause tests to abort when it contains invalid hex characters, or when it is longer than 64 hex digits After this PR, the remaining work to remove `uint256S` completely is almost entirely mechanical and/or test related. I will open that PR once #30560 is merged because it builds on that. ACKs for top commit: hodlinator: re-ACK 18d65d2 l0rinc: ACK 18d65d2 achow101: ACK 18d65d2 ryanofsky: Code review ACK 18d65d2. Very nice change that cleans up the API, adds checking for invalid values, makes parsing of values more consistent, and adds test coverage. Tree-SHA512: ec118ea3d56e1dfbc4c79acdbfc797f65c4d2107b0ca9577c848b4ab9b7cb8d05db9f3c7fe8441a09291aca41bf671572431f4eddc59be8fb53abbea76bbbf86
2 parents 99b06b7 + 18d65d2 commit 2c7a423

File tree

11 files changed

+139
-52
lines changed

11 files changed

+139
-52
lines changed

src/node/chainstatemanager_args.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,20 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManage
3232
if (auto value{args.GetBoolArg("-checkpoints")}) opts.checkpoints_enabled = *value;
3333

3434
if (auto value{args.GetArg("-minimumchainwork")}) {
35-
if (!IsHexNumber(*value)) {
36-
return util::Error{strprintf(Untranslated("Invalid non-hex (%s) minimum chain work value specified"), *value)};
35+
if (auto min_work{uint256::FromUserHex(*value)}) {
36+
opts.minimum_chain_work = UintToArith256(*min_work);
37+
} else {
38+
return util::Error{strprintf(Untranslated("Invalid minimum work specified (%s), must be up to %d hex digits"), *value, uint256::size() * 2)};
3739
}
38-
opts.minimum_chain_work = UintToArith256(uint256S(*value));
3940
}
4041

41-
if (auto value{args.GetArg("-assumevalid")}) opts.assumed_valid_block = uint256S(*value);
42+
if (auto value{args.GetArg("-assumevalid")}) {
43+
if (auto block_hash{uint256::FromUserHex(*value)}) {
44+
opts.assumed_valid_block = *block_hash;
45+
} else {
46+
return util::Error{strprintf(Untranslated("Invalid assumevalid block hash specified (%s), must be up to %d hex digits (or 0 to disable)"), *value, uint256::size() * 2)};
47+
}
48+
}
4249

4350
if (auto value{args.GetIntArg("-maxtipage")}) opts.max_tip_age = std::chrono::seconds{*value};
4451

src/test/fuzz/hex.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ FUZZ_TARGET(hex)
2727
if (IsHex(random_hex_string)) {
2828
assert(ToLower(random_hex_string) == hex_data);
2929
}
30-
(void)IsHexNumber(random_hex_string);
3130
if (uint256::FromHex(random_hex_string)) {
3231
assert(random_hex_string.length() == 64);
3332
assert(Txid::FromHex(random_hex_string));
3433
assert(Wtxid::FromHex(random_hex_string));
34+
assert(uint256::FromUserHex(random_hex_string));
35+
}
36+
if (const auto result{uint256::FromUserHex(random_hex_string)}) {
37+
assert(uint256::FromHex(result->ToString()));
3538
}
3639
(void)uint256S(random_hex_string);
3740
try {

src/test/uint256_tests.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,35 @@ BOOST_AUTO_TEST_CASE(from_hex)
393393
TestFromHex<Wtxid>();
394394
}
395395

396+
BOOST_AUTO_TEST_CASE(from_user_hex)
397+
{
398+
BOOST_CHECK_EQUAL(uint256::FromUserHex("").value(), uint256::ZERO);
399+
BOOST_CHECK_EQUAL(uint256::FromUserHex("0x").value(), uint256::ZERO);
400+
BOOST_CHECK_EQUAL(uint256::FromUserHex("0").value(), uint256::ZERO);
401+
BOOST_CHECK_EQUAL(uint256::FromUserHex("00").value(), uint256::ZERO);
402+
BOOST_CHECK_EQUAL(uint256::FromUserHex("1").value(), uint256::ONE);
403+
BOOST_CHECK_EQUAL(uint256::FromUserHex("0x10").value(), uint256{0x10});
404+
BOOST_CHECK_EQUAL(uint256::FromUserHex("10").value(), uint256{0x10});
405+
BOOST_CHECK_EQUAL(uint256::FromUserHex("0xFf").value(), uint256{0xff});
406+
BOOST_CHECK_EQUAL(uint256::FromUserHex("Ff").value(), uint256{0xff});
407+
const std::string valid_hex_64{"0x0123456789abcdef0123456789abcdef0123456789ABDCEF0123456789ABCDEF"};
408+
BOOST_REQUIRE_EQUAL(valid_hex_64.size(), 2 + 64); // 0x prefix and 64 hex digits
409+
BOOST_CHECK_EQUAL(uint256::FromUserHex(valid_hex_64.substr(2)).value().ToString(), ToLower(valid_hex_64.substr(2)));
410+
BOOST_CHECK_EQUAL(uint256::FromUserHex(valid_hex_64.substr(0)).value().ToString(), ToLower(valid_hex_64.substr(2)));
411+
412+
BOOST_CHECK(!uint256::FromUserHex("0x0 ")); // no spaces at end,
413+
BOOST_CHECK(!uint256::FromUserHex(" 0x0")); // or beginning,
414+
BOOST_CHECK(!uint256::FromUserHex("0x 0")); // or middle,
415+
BOOST_CHECK(!uint256::FromUserHex(" ")); // etc.
416+
BOOST_CHECK(!uint256::FromUserHex("0x0ga")); // invalid character
417+
BOOST_CHECK(!uint256::FromUserHex("x0")); // broken prefix
418+
BOOST_CHECK(!uint256::FromUserHex("0x0x00")); // two prefixes not allowed
419+
BOOST_CHECK(!uint256::FromUserHex(valid_hex_64.substr(2) + "0")); // 1 hex digit too many
420+
BOOST_CHECK(!uint256::FromUserHex(valid_hex_64 + "a")); // 1 hex digit too many
421+
BOOST_CHECK(!uint256::FromUserHex(valid_hex_64 + " ")); // whitespace after max length
422+
BOOST_CHECK(!uint256::FromUserHex(valid_hex_64 + "z")); // invalid character after max length
423+
}
424+
396425
BOOST_AUTO_TEST_CASE( check_ONE )
397426
{
398427
uint256 one = uint256{"0000000000000000000000000000000000000000000000000000000000000001"};

src/test/util/random.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,39 @@
77
#include <logging.h>
88
#include <random.h>
99
#include <uint256.h>
10+
#include <util/check.h>
1011

1112
#include <cstdlib>
12-
#include <string>
13+
#include <iostream>
1314

1415
FastRandomContext g_insecure_rand_ctx;
1516

1617
extern void MakeRandDeterministicDANGEROUS(const uint256& seed) noexcept;
1718

1819
void SeedRandomForTest(SeedRand seedtype)
1920
{
20-
static const std::string RANDOM_CTX_SEED{"RANDOM_CTX_SEED"};
21+
constexpr auto RANDOM_CTX_SEED{"RANDOM_CTX_SEED"};
2122

2223
// Do this once, on the first call, regardless of seedtype, because once
2324
// MakeRandDeterministicDANGEROUS is called, the output of GetRandHash is
2425
// no longer truly random. It should be enough to get the seed once for the
2526
// process.
2627
static const uint256 ctx_seed = []() {
2728
// If RANDOM_CTX_SEED is set, use that as seed.
28-
const char* num = std::getenv(RANDOM_CTX_SEED.c_str());
29-
if (num) return uint256S(num);
29+
if (const char* num{std::getenv(RANDOM_CTX_SEED)}) {
30+
if (auto num_parsed{uint256::FromUserHex(num)}) {
31+
return *num_parsed;
32+
} else {
33+
std::cerr << RANDOM_CTX_SEED << " must consist of up to " << uint256::size() * 2 << " hex digits (\"0x\" prefix allowed), it was set to: '" << num << "'.\n";
34+
std::abort();
35+
}
36+
}
3037
// Otherwise use a (truly) random value.
3138
return GetRandHash();
3239
}();
3340

3441
const uint256& seed{seedtype == SeedRand::SEED ? ctx_seed : uint256::ZERO};
35-
LogPrintf("%s: Setting random seed for current tests to %s=%s\n", __func__, RANDOM_CTX_SEED, seed.GetHex());
42+
LogInfo("Setting random seed for current tests to %s=%s\n", RANDOM_CTX_SEED, seed.GetHex());
3643
MakeRandDeterministicDANGEROUS(seed);
3744
g_insecure_rand_ctx.Reseed(GetRandHash());
3845
}

src/test/util_tests.cpp

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -432,31 +432,6 @@ BOOST_AUTO_TEST_CASE(util_IsHex)
432432
BOOST_CHECK(!IsHex("0x0000"));
433433
}
434434

435-
BOOST_AUTO_TEST_CASE(util_IsHexNumber)
436-
{
437-
BOOST_CHECK(IsHexNumber("0x0"));
438-
BOOST_CHECK(IsHexNumber("0"));
439-
BOOST_CHECK(IsHexNumber("0x10"));
440-
BOOST_CHECK(IsHexNumber("10"));
441-
BOOST_CHECK(IsHexNumber("0xff"));
442-
BOOST_CHECK(IsHexNumber("ff"));
443-
BOOST_CHECK(IsHexNumber("0xFfa"));
444-
BOOST_CHECK(IsHexNumber("Ffa"));
445-
BOOST_CHECK(IsHexNumber("0x00112233445566778899aabbccddeeffAABBCCDDEEFF"));
446-
BOOST_CHECK(IsHexNumber("00112233445566778899aabbccddeeffAABBCCDDEEFF"));
447-
448-
BOOST_CHECK(!IsHexNumber("")); // empty string not allowed
449-
BOOST_CHECK(!IsHexNumber("0x")); // empty string after prefix not allowed
450-
BOOST_CHECK(!IsHexNumber("0x0 ")); // no spaces at end,
451-
BOOST_CHECK(!IsHexNumber(" 0x0")); // or beginning,
452-
BOOST_CHECK(!IsHexNumber("0x 0")); // or middle,
453-
BOOST_CHECK(!IsHexNumber(" ")); // etc.
454-
BOOST_CHECK(!IsHexNumber("0x0ga")); // invalid character
455-
BOOST_CHECK(!IsHexNumber("x0")); // broken prefix
456-
BOOST_CHECK(!IsHexNumber("0x0x00")); // two prefixes not allowed
457-
458-
}
459-
460435
BOOST_AUTO_TEST_CASE(util_seed_insecure_rand)
461436
{
462437
SeedRandomForTest(SeedRand::ZEROS);

src/test/validation_chainstatemanager_tests.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <chainparams.h>
66
#include <consensus/validation.h>
77
#include <kernel/disconnected_transactions.h>
8+
#include <node/chainstatemanager_args.h>
89
#include <node/kernel_notifications.h>
910
#include <node/utxo_snapshot.h>
1011
#include <random.h>
@@ -16,6 +17,8 @@
1617
#include <test/util/setup_common.h>
1718
#include <test/util/validation.h>
1819
#include <uint256.h>
20+
#include <util/result.h>
21+
#include <util/vector.h>
1922
#include <validation.h>
2023
#include <validationinterface.h>
2124

@@ -769,4 +772,59 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_hash_mismatch, Sna
769772
}
770773
}
771774

775+
/** Helper function to parse args into args_man and return the result of applying them to opts */
776+
template <typename Options>
777+
util::Result<Options> SetOptsFromArgs(ArgsManager& args_man, Options opts,
778+
const std::vector<const char*>& args)
779+
{
780+
const auto argv{Cat({"ignore"}, args)};
781+
std::string error{};
782+
if (!args_man.ParseParameters(argv.size(), argv.data(), error)) {
783+
return util::Error{Untranslated("ParseParameters failed with error: " + error)};
784+
}
785+
const auto result{node::ApplyArgsManOptions(args_man, opts)};
786+
if (!result) return util::Error{util::ErrorString(result)};
787+
return opts;
788+
}
789+
790+
BOOST_FIXTURE_TEST_CASE(chainstatemanager_args, BasicTestingSetup)
791+
{
792+
//! Try to apply the provided args to a ChainstateManager::Options
793+
auto get_opts = [&](const std::vector<const char*>& args) {
794+
static kernel::Notifications notifications{};
795+
static const ChainstateManager::Options options{
796+
.chainparams = ::Params(),
797+
.datadir = {},
798+
.notifications = notifications};
799+
return SetOptsFromArgs(*this->m_node.args, options, args);
800+
};
801+
//! Like get_opts, but requires the provided args to be valid and unwraps the result
802+
auto get_valid_opts = [&](const std::vector<const char*>& args) {
803+
const auto result{get_opts(args)};
804+
BOOST_REQUIRE_MESSAGE(result, util::ErrorString(result).original);
805+
return *result;
806+
};
807+
808+
// test -assumevalid
809+
BOOST_CHECK(!get_valid_opts({}).assumed_valid_block.has_value());
810+
BOOST_CHECK(get_valid_opts({"-assumevalid="}).assumed_valid_block.value().IsNull());
811+
BOOST_CHECK(get_valid_opts({"-assumevalid=0"}).assumed_valid_block.value().IsNull());
812+
BOOST_CHECK(get_valid_opts({"-noassumevalid"}).assumed_valid_block.value().IsNull());
813+
BOOST_CHECK_EQUAL(get_valid_opts({"-assumevalid=0x1234"}).assumed_valid_block.value().ToString(), std::string(60, '0') + "1234");
814+
const std::string cmd{"-assumevalid=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"};
815+
BOOST_CHECK_EQUAL(get_valid_opts({cmd.c_str()}).assumed_valid_block.value().ToString(), cmd.substr(13, cmd.size()));
816+
817+
BOOST_CHECK(!get_opts({"-assumevalid=xyz"})); // invalid hex characters
818+
BOOST_CHECK(!get_opts({"-assumevalid=01234567890123456789012345678901234567890123456789012345678901234"})); // > 64 hex chars
819+
820+
// test -minimumchainwork
821+
BOOST_CHECK(!get_valid_opts({}).minimum_chain_work.has_value());
822+
BOOST_CHECK_EQUAL(get_valid_opts({"-minimumchainwork=0"}).minimum_chain_work.value().GetCompact(), 0U);
823+
BOOST_CHECK_EQUAL(get_valid_opts({"-nominimumchainwork"}).minimum_chain_work.value().GetCompact(), 0U);
824+
BOOST_CHECK_EQUAL(get_valid_opts({"-minimumchainwork=0x1234"}).minimum_chain_work.value().GetCompact(), 0x02123400U);
825+
826+
BOOST_CHECK(!get_opts({"-minimumchainwork=xyz"})); // invalid hex characters
827+
BOOST_CHECK(!get_opts({"-minimumchainwork=01234567890123456789012345678901234567890123456789012345678901234"})); // > 64 hex chars
828+
}
829+
772830
BOOST_AUTO_TEST_SUITE_END()

src/uint256.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <crypto/common.h>
1010
#include <span.h>
1111
#include <util/strencodings.h>
12+
#include <util/string.h>
1213

1314
#include <algorithm>
1415
#include <array>
@@ -17,6 +18,7 @@
1718
#include <cstring>
1819
#include <optional>
1920
#include <string>
21+
#include <string_view>
2022

2123
/** Template base class for fixed-sized opaque blobs. */
2224
template<unsigned int BITS>
@@ -157,6 +159,25 @@ std::optional<uintN_t> FromHex(std::string_view str)
157159
rv.SetHexDeprecated(str);
158160
return rv;
159161
}
162+
/**
163+
* @brief Like FromHex(std::string_view str), but allows an "0x" prefix
164+
* and pads the input with leading zeroes if it is shorter than
165+
* the expected length of uintN_t::size()*2.
166+
*
167+
* Designed to be used when dealing with user input.
168+
*/
169+
template <class uintN_t>
170+
std::optional<uintN_t> FromUserHex(std::string_view input)
171+
{
172+
input = util::RemovePrefixView(input, "0x");
173+
constexpr auto expected_size{uintN_t::size() * 2};
174+
if (input.size() < expected_size) {
175+
auto padded = std::string(expected_size, '0');
176+
std::copy(input.begin(), input.end(), padded.begin() + expected_size - input.size());
177+
return FromHex<uintN_t>(padded);
178+
}
179+
return FromHex<uintN_t>(input);
180+
}
160181
} // namespace detail
161182

162183
/** 160-bit opaque blob.
@@ -178,6 +199,7 @@ class uint160 : public base_blob<160> {
178199
class uint256 : public base_blob<256> {
179200
public:
180201
static std::optional<uint256> FromHex(std::string_view str) { return detail::FromHex<uint256>(str); }
202+
static std::optional<uint256> FromUserHex(std::string_view str) { return detail::FromUserHex<uint256>(str); }
181203
constexpr uint256() = default;
182204
consteval explicit uint256(std::string_view hex_str) : base_blob<256>(hex_str) {}
183205
constexpr explicit uint256(uint8_t v) : base_blob<256>(v) {}

src/util/strencodings.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,6 @@ bool IsHex(std::string_view str)
4646
return (str.size() > 0) && (str.size()%2 == 0);
4747
}
4848

49-
bool IsHexNumber(std::string_view str)
50-
{
51-
if (str.substr(0, 2) == "0x") str.remove_prefix(2);
52-
for (char c : str) {
53-
if (HexDigit(c) < 0) return false;
54-
}
55-
// Return false for empty string or "0x".
56-
return str.size() > 0;
57-
}
58-
5949
template <typename Byte>
6050
std::optional<std::vector<Byte>> TryParseHex(std::string_view str)
6151
{

src/util/strencodings.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ std::vector<Byte> ParseHex(std::string_view hex_str)
7070
/* Returns true if each character in str is a hex character, and has an even
7171
* number of hex digits.*/
7272
bool IsHex(std::string_view str);
73-
/**
74-
* Return true if the string is a hex number, optionally prefixed with "0x"
75-
*/
76-
bool IsHexNumber(std::string_view str);
7773
std::optional<std::vector<unsigned char>> DecodeBase64(std::string_view str);
7874
std::string EncodeBase64(Span<const unsigned char> input);
7975
inline std::string EncodeBase64(Span<const std::byte> input) { return EncodeBase64(MakeUCharSpan(input)); }

test/functional/feature_assumevalid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ def run_test(self):
139139
height += 1
140140

141141
# Start node1 and node2 with assumevalid so they accept a block with a bad signature.
142-
self.start_node(1, extra_args=["-assumevalid=" + hex(block102.sha256)])
143-
self.start_node(2, extra_args=["-assumevalid=" + hex(block102.sha256)])
142+
self.start_node(1, extra_args=["-assumevalid=" + block102.hash])
143+
self.start_node(2, extra_args=["-assumevalid=" + block102.hash])
144144

145145
p2p0 = self.nodes[0].add_p2p_connection(BaseNode())
146146
p2p0.send_header_for_blocks(self.blocks[0:2000])

0 commit comments

Comments
 (0)