Merge bitcoin/bitcoin#35069: Refactor keypath parser
What changed, and why it matters
This is a code cleanup (refactor) that replaces scattered hard-coded numbers with named constants and merges two nearly identical BIP32 key-path parsers into one shared utility. It does not add new features or fix a known security bug, but it reduces the chance of future parser inconsistencies and makes the code easier to audit. A few extra test cases were added to confirm invalid inputs are still rejected.
No immediate action required. Treat as routine maintenance. Reviewers may want to verify that ParseKeyPathElement preserves the exact rejection behavior of the old parsers (e.g., hardened marker only at end, numeric cap at 2^31-1) and that the new error messages do not break downstream consumers.
Security signals we found
Refactoring of BIP32 key-path parsing to a single shared parser
Replacement of magic 0x80000000 literals with named constants
No change to hardened-marker semantics or numeric range checks
Additional unit tests for invalid key-path elements
No vendor disclosure of a security vulnerability
Evidence from the diff
The commit introduces BIP32_HARDENED_FLAG (0x80000000) and BIP32_UNHARDENED_FLAG constants, a new KeyPathElement struct, and a shared ParseKeyPathElement() helper. It replaces the duplicated ParseKeyPathNum logic in descriptor.cpp and the hand-rolled parsing in ParseHDKeypath() in util/bip32.cpp. The hardened marker handling and the 0x7FFFFFFF numeric cap remain functionally the same. Tests were updated/expanded to cover ‘h’, invalid suffixes, and out-of-range values in both parsers. Error messages for empty multipath elements changed slightly from ‘not a valid uint32’ to ‘not valid’.
Changed components
src/util/bip32.cppsrc/util/bip32.hsrc/script/descriptor.cppsrc/wallet/walletdb.cppsrc/test/bip32_tests.cppsrc/test/descriptor_tests.cppInspect captured patch +109 / −82
### src/script/descriptor.cpp
@@ -27,6 +27,7 @@
#include <uint256.h>
#include <util/bip32.h>
#include <util/check.h>
+#include <util/expected.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/vector.h>
@@ -454,7 +455,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
info.fingerprint = m_root_extkey.id_key_fingerprint();
info.path = m_path;
if (m_derive == DeriveType::UNHARDENED_RANGED) info.path.push_back((uint32_t)pos);
- if (m_derive == DeriveType::HARDENED_RANGED) info.path.push_back(((uint32_t)pos) | 0x80000000L);
+ if (m_derive == DeriveType::HARDENED_RANGED) info.path.push_back(((uint32_t)pos) | BIP32_HARDENED_FLAG);
// Derive keys or fetch them from cache
CExtPubKey final_extkey = m_root_extkey;
@@ -475,7 +476,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
if (!GetDerivedExtKey(arg, xprv, lh_xprv)) return std::nullopt;
parent_extkey = xprv.Neuter();
if (m_derive == DeriveType::UNHARDENED_RANGED) der = xprv.Derive(xprv, pos);
- if (m_derive == DeriveType::HARDENED_RANGED) der = xprv.Derive(xprv, pos | 0x80000000UL);
+ if (m_derive == DeriveType::HARDENED_RANGED) der = xprv.Derive(xprv, pos | BIP32_HARDENED_FLAG);
final_extkey = xprv.Neuter();
if (lh_xprv.key.IsValid()) {
last_hardened_extkey = lh_xprv.Neuter();
@@ -599,7 +600,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
CExtKey dummy;
if (!GetDerivedExtKey(arg, extkey, dummy)) return;
if (m_derive == DeriveType::UNHARDENED_RANGED && !extkey.Derive(extkey, pos)) return;
- if (m_derive == DeriveType::HARDENED_RANGED && !extkey.Derive(extkey, pos | 0x80000000UL)) return;
+ if (m_derive == DeriveType::HARDENED_RANGED && !extkey.Derive(extkey, pos | BIP32_HARDENED_FLAG)) return;
out.keys.emplace(extkey.key.GetPubKey().GetID(), extkey.key);
}
std::optional<CPubKey> GetRootPubKey() const override
@@ -1817,30 +1818,6 @@ enum class ParseScriptContext {
MUSIG, //!< Inside musig() (implies P2TR, cannot have nested musig())
};
-std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostrophe, std::string& error, bool& has_hardened)
-{
- bool hardened = false;
- if (elem.size() > 0) {
- const char last = elem[elem.size() - 1];
- if (last == '\'' || last == 'h') {
- elem = elem.first(elem.size() - 1);
- hardened = true;
- apostrophe = last == '\'';
- }
- }
- const auto p{ToIntegral<uint32_t>(std::string_view{elem.begin(), elem.end()})};
- if (!p) {
- error = strprintf("Key path value '%s' is not a valid uint32", std::string_view{elem.begin(), elem.end()});
- return std::nullopt;
- } else if (*p > 0x7FFFFFFFUL) {
- error = strprintf("Key path value %u is out of range", *p);
- return std::nullopt;
- }
- has_hardened = has_hardened || hardened;
-
- return std::make_optional<uint32_t>(*p | (((uint32_t)hardened) << 31));
-}
-
/**
* Parse a key path, being passed a split list of elements (the first element is ignored because it is always the key).
*
@@ -1854,6 +1831,19 @@ std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostr
**/
[[nodiscard]] bool ParseKeyPath(const std::vector<std::span<const char>>& split, std::vector<KeyPath>& out, bool& apostrophe, std::string& error, bool allow_multipath, bool& has_hardened)
{
+ auto parse_elem = [&](std::span<const char> elem) -> std::optional<uint32_t> {
+ const auto parsed{ParseKeyPathElement(elem)};
+ if (!parsed) {
+ error = parsed.error();
+ return std::nullopt;
+ }
+ if (parsed->is_hardened) {
+ has_hardened = true;
+ apostrophe = elem.back() == '\'';
+ }
+ return parsed->ChildNumber();
+ };
+
KeyPath path;
struct MultipathSubstitutes {
size_t placeholder_index;
@@ -1886,7 +1876,7 @@ std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostr
substitutes.emplace();
std::unordered_set<uint32_t> seen_substitutes;
for (const auto& num : nums) {
- const auto& op_num = ParseKeyPathNum(num, apostrophe, error, has_hardened);
+ const auto& op_num = parse_elem(num);
if (!op_num) return false;
auto [_, inserted] = seen_substitutes.insert(*op_num);
if (!inserted) {
@@ -1899,7 +1889,7 @@ std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostr
path.emplace_back(); // Placeholder for multipath segment
substitutes->placeholder_index = path.size() - 1;
} else {
- const auto& op_num = ParseKeyPathNum(elem, apostrophe, error, has_hardened);
+ const auto& op_num = parse_elem(elem);
if (!op_num) return false;
path.emplace_back(*op_num);
}
### src/test/bip32_tests.cpp
@@ -43,13 +43,13 @@ TestVector test1 =
TestVector("000102030405060708090a0b0c0d0e0f")
("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
- 0x80000000)
+ BIP32_HARDENED_FLAG)
("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw",
"xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7",
1)
("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
"xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs",
- 0x80000002)
+ BIP32_HARDENED_FLAG | 2)
("xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
"xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM",
2)
@@ -85,7 +85,7 @@ TestVector test3 =
TestVector("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be")
("xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",
"xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6",
- 0x80000000)
+ BIP32_HARDENED_FLAG)
("xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",
"xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L",
0);
@@ -94,10 +94,10 @@ TestVector test4 =
TestVector("3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678")
("xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa",
"xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv",
- 0x80000000)
+ BIP32_HARDENED_FLAG)
("xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m",
"xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G",
- 0x80000001)
+ BIP32_HARDENED_FLAG | 1)
("xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt",
"xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1",
0);
@@ -145,7 +145,7 @@ void RunTest(const TestVector& test)
CExtKey keyNew;
BOOST_CHECK(key.Derive(keyNew, derive.nChild));
CExtPubKey pubkeyNew = keyNew.Neuter();
- if (!(derive.nChild & 0x80000000)) {
+ if (!(derive.nChild & BIP32_HARDENED_FLAG)) {
// Compare with public derivation
CExtPubKey pubkeyNew2;
BOOST_CHECK(pubkey.Derive(pubkeyNew2, derive.nChild));
@@ -217,8 +217,8 @@ BOOST_AUTO_TEST_CASE(bip32_has_hardened_derivation)
{
const std::vector<uint32_t> empty;
const std::vector<uint32_t> unhardened{0, 1, 2};
- const std::vector<uint32_t> hardened{0x80000000U};
- const std::vector<uint32_t> mixed{0, 1 | 0x80000000U, 2};
+ const std::vector<uint32_t> hardened{BIP32_HARDENED_FLAG};
+ const std::vector<uint32_t> mixed{0, BIP32_HARDENED_FLAG | 1, 2};
BOOST_CHECK(!HasHardenedDerivation(empty));
BOOST_CHECK(!HasHardenedDerivation(unhardened));
BOOST_CHECK(HasHardenedDerivation(hardened));
@@ -297,17 +297,25 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath)
BOOST_CHECK(ParseHDKeypath("m/0'", keypath));
BOOST_CHECK(!ParseHDKeypath("m/0''", keypath));
+ BOOST_CHECK(ParseHDKeypath("m/0h", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0hh", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0x", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0a", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0G", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/h0", keypath));
+
keypath.clear();
BOOST_REQUIRE(ParseHDKeypath("m/0h/1h/2h", keypath));
BOOST_REQUIRE_EQUAL(keypath.size(), 3);
- BOOST_CHECK_EQUAL(keypath[0], 0x80000000U);
- BOOST_CHECK_EQUAL(keypath[1], 0x80000001U);
- BOOST_CHECK_EQUAL(keypath[2], 0x80000002U);
- BOOST_CHECK(!ParseHDKeypath("m/0hh", keypath));
- BOOST_CHECK(!ParseHDKeypath("m/h0", keypath));
+ BOOST_CHECK_EQUAL(keypath[0], BIP32_HARDENED_FLAG);
+ BOOST_CHECK_EQUAL(keypath[1], BIP32_HARDENED_FLAG | 1);
+ BOOST_CHECK_EQUAL(keypath[2], BIP32_HARDENED_FLAG | 2);
BOOST_CHECK(ParseHDKeypath("m/0'/0'", keypath));
+ BOOST_CHECK(ParseHDKeypath("m/0h/0h", keypath));
+ BOOST_CHECK(ParseHDKeypath("m/0'/0h", keypath));
BOOST_CHECK(!ParseHDKeypath("m/'0/0'", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/h0/0'", keypath));
BOOST_CHECK(ParseHDKeypath("m/0/0", keypath));
BOOST_CHECK(!ParseHDKeypath("n/0/0", keypath));
@@ -323,6 +331,17 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath)
BOOST_CHECK(ParseHDKeypath("m/1/", keypath));
BOOST_CHECK(!ParseHDKeypath("m/1//", keypath));
+
+ // The cap applies to every element, wherever it sits in the path.
+ BOOST_CHECK(ParseHDKeypath("m/2147483647", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/2147483648", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/4294967295", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/4294967296", keypath));
+
+ BOOST_CHECK(ParseHDKeypath("m/0/2147483647", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0/2147483648", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0/4294967295", keypath));
+ BOOST_CHECK(!ParseHDKeypath("m/0/4294967296", keypath));
}
BOOST_AUTO_TEST_SUITE_END()
### src/test/descriptor_tests.cpp
@@ -985,9 +985,9 @@ BOOST_AUTO_TEST_CASE(descriptor_test)
CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<>/*)", "wpkh(): Multipath key path specifiers must have at least two items");
CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0/*)", "wpkh(): Key path value '<0' is not a valid uint32");
CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0>/*)", "wpkh(): Key path value '0>' is not a valid uint32");
- CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;>/*)", "wpkh(): Key path value '' is not a valid uint32");
- CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<;1>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<;1>/*)", "wpkh(): Key path value '' is not a valid uint32");
- CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;1;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1;>/*)", "wpkh(): Key path value '' is not a valid uint32");
+ CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;>/*)", "wpkh(): Key path value '' is not valid");
+ CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<;1>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<;1>/*)", "wpkh(): Key path value '' is not valid");
+ CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;1;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1;>/*)", "wpkh(): Key path value '' is not valid");
CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<1;1>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<1;1>/*)", "wpkh(): Duplicated key path value 1 in multipath specifier");
// Multisig constructions
### src/util/bip32.cpp
@@ -9,10 +9,34 @@
#include <algorithm>
#include <cstdint>
-#include <cstdio>
#include <optional>
#include <span>
#include <sstream>
+#include <string_view>
+
+util::Expected<KeyPathElement, std::string> ParseKeyPathElement(std::span<const char> elem)
+{
+ const std::string_view raw{elem.begin(), elem.end()};
+ if (elem.empty()) {
+ return util::Unexpected{strprintf("Key path value '%s' is not valid", raw)};
+ }
+
+ bool is_hardened = false;
+ const char last = elem.back();
+ if (last == '\'' || last == 'h') {
+ elem = elem.first(elem.size() - 1);
+ is_hardened = true;
+ }
+
+ const auto number{ToIntegral<uint32_t>(std::string_view{elem.begin(), elem.end()})};
+ if (!number) {
+ return util::Unexpected{strprintf("Key path value '%s' is not a valid uint32", raw)};
+ }
+ if (*number >= BIP32_HARDENED_FLAG) {
+ return util::Unexpected{strprintf("Key path value %u is out of range", *number)};
+ }
+ return KeyPathElement{*number, is_hardened};
+}
bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath)
{
@@ -27,34 +51,9 @@ bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypa
}
return false;
}
- // Finds whether it is hardened
- uint32_t path = 0;
- size_t pos = item.find('\'');
- if (pos == std::string::npos) {
- pos = item.find('h');
- }
- if (pos != std::string::npos) {
- // The hardened tick can only be in the last index of the string
- if (pos != item.size() - 1) {
- return false;
- }
- path |= 0x80000000;
- item = item.substr(0, item.size() - 1); // Drop the last character which is the hardened tick
- }
-
- // Ensure this is only numbers
- const auto number{ToIntegral<uint32_t>(item)};
- if (!number) {
- return false;
- }
- // A BIP32 child index is 31 bits; the top bit is reserved for the
- // hardened marker, so the numeric part must not exceed 2^31 - 1.
- if (*number > 0x7fffffff) {
- return false;
- }
- path |= *number;
-
- keypath.push_back(path);
+ const auto parsed{ParseKeyPathElement(std::span<const char>{item.data(), item.size()})};
+ if (!parsed) return false;
+ keypath.push_back(parsed->ChildNumber());
first = false;
}
return true;
### src/util/bip32.h
@@ -8,8 +8,27 @@
#include <cstdint>
#include <span>
#include <string>
+#include <util/expected.h>
#include <vector>
+/** BIP32 unhardened derivation index (no high bit set) */
+static constexpr uint32_t BIP32_UNHARDENED_FLAG = 0x0;
+/** BIP32 hardened derivation flag (2^31) */
+static constexpr uint32_t BIP32_HARDENED_FLAG = 0x80000000;
+
+struct KeyPathElement {
+ /** Derivation index, without the hardened flag */
+ uint32_t index;
+ bool is_hardened;
+
+ /** Derivation index with the hardened flag applied */
+ uint32_t ChildNumber() const { return index | (is_hardened ? BIP32_HARDENED_FLAG : BIP32_UNHARDENED_FLAG); }
+};
+
+/** Parse a single key path element like "0", "0'", or "0h".
+ * Returns the derivation index and hardened status, or an error message. */
+util::Expected<KeyPathElement, std::string> ParseKeyPathElement(std::span<const char> elem);
+
/** Parse an HD keypaths like "m/7/0'/2000". */
[[nodiscard]] bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath);
### src/wallet/test/psbt_wallet_tests.cpp
@@ -4,12 +4,12 @@
#include <key_io.h>
#include <node/types.h>
+#include <test/util/setup_common.h>
#include <util/strencodings.h>
+#include <wallet/test/wallet_test_fixture.h>
#include <wallet/wallet.h>
#include <boost/test/unit_test.hpp>
-#include <test/util/setup_common.h>
-#include <wallet/test/wallet_test_fixture.h>
using namespace util::hex_literals;
@@ -22,7 +22,7 @@ static void import_descriptor(CWallet& wallet, const std::string& descriptor)
AssertLockHeld(wallet.cs_wallet);
FlatSigningProvider provider;
std::string error;
- auto descs = Parse(descriptor, provider, error, /* require_checksum=*/ false);
+ auto descs = Parse(descriptor, provider, error, /* require_checksum=*/false);
assert(descs.size() == 1);
auto& desc = descs.at(0);
WalletDescriptor w_desc(std::move(desc), 0, 0, 10, 0);
### src/wallet/walletdb.cpp
@@ -627,20 +627,20 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
strErr = "Error reading wallet database: keymeta found with unexpected path";
return DBErrors::NONCRITICAL_ERROR;
}
- if (path[0] != 0x80000000) {
+ if (path[0] != BIP32_HARDENED_FLAG) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000) for the element at index 0", path[0]);
return DBErrors::NONCRITICAL_ERROR;
}
- if (path[1] != 0x80000000 && path[1] != (1 | 0x80000000)) {
+ if (path[1] != BIP32_HARDENED_FLAG && path[1] != (1 | BIP32_HARDENED_FLAG)) {
strErr = strprintf("Unexpected path index of 0x%08x (expected 0x80000000 or 0x80000001) for the element at index 1", path[1]);
return DBErrors::NONCRITICAL_ERROR;
}
- if ((path[2] & 0x80000000) == 0) {
+ if ((path[2] & BIP32_HARDENED_FLAG) == 0) {
strErr = strprintf("Unexpected path index of 0x%08x (expected to be greater than or equal to 0x80000000)", path[2]);
return DBErrors::NONCRITICAL_ERROR;
}
- internal = path[1] == (1 | 0x80000000);
- index = path[2] & ~0x80000000;
+ internal = path[1] == (1 | BIP32_HARDENED_FLAG);
+ index = path[2] & ~BIP32_HARDENED_FLAG;
}
// Insert a new CHDChain, or get the one that already existsWhy this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.