From b3295ab4608aba2341590f15947e364326716720 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 14 Apr 2026 03:02:34 -0400 Subject: [PATCH 01/21] refactor: define BIP32_HARDENED and BIP32_UNHARDENED constants Replace magic 0x80000000 literals with named constants for BIP32 hardened/unhardened child derivation. --- src/script/descriptor.cpp | 6 +++--- src/test/bip32_tests.cpp | 13 +++++++------ src/util/bip32.cpp | 2 +- src/util/bip32.h | 5 +++++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 19315dd6a30f..d22209cb3d49 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -429,7 +429,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider std::copy(keyid.begin(), keyid.begin() + sizeof(info.fingerprint), info.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); // Derive keys or fetch them from cache CExtPubKey final_extkey = m_root_extkey; @@ -450,7 +450,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); final_extkey = xprv.Neuter(); if (lh_xprv.key.IsValid()) { last_hardened_extkey = lh_xprv.Neuter(); @@ -576,7 +576,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)) return; out.keys.emplace(extkey.key.GetPubKey().GetID(), extkey.key); } std::optional GetRootPubKey() const override diff --git a/src/test/bip32_tests.cpp b/src/test/bip32_tests.cpp index 1df368ade744..dbbe5d15a015 100644 --- a/src/test/bip32_tests.cpp +++ b/src/test/bip32_tests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -42,13 +43,13 @@ TestVector test1 = TestVector("000102030405060708090a0b0c0d0e0f") ("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", - 0x80000000) + BIP32_HARDENED) ("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", 1) ("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", - 0x80000002) + BIP32_HARDENED | 2) ("xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", 2) @@ -84,7 +85,7 @@ TestVector test3 = TestVector("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be") ("xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", - 0x80000000) + BIP32_HARDENED) ("xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", 0); @@ -93,10 +94,10 @@ TestVector test4 = TestVector("3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678") ("xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa", "xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv", - 0x80000000) + BIP32_HARDENED) ("xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m", "xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G", - 0x80000001) + BIP32_HARDENED | 1) ("xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt", "xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1", 0); @@ -144,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)) { // Compare with public derivation CExtPubKey pubkeyNew2; BOOST_CHECK(pubkey.Derive(pubkeyNew2, derive.nChild)); diff --git a/src/util/bip32.cpp b/src/util/bip32.cpp index 2488eacf5156..77d7cf868ef2 100644 --- a/src/util/bip32.cpp +++ b/src/util/bip32.cpp @@ -33,7 +33,7 @@ bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypa if (pos != item.size() - 1) { return false; } - path |= 0x80000000; + path |= BIP32_HARDENED; item = item.substr(0, item.size() - 1); // Drop the last character which is the hardened tick } diff --git a/src/util/bip32.h b/src/util/bip32.h index af48147aa808..265b02ce4641 100644 --- a/src/util/bip32.h +++ b/src/util/bip32.h @@ -9,6 +9,11 @@ #include #include +/** BIP32 unhardened child index (no high bit set) */ +static constexpr uint32_t BIP32_UNHARDENED = 0x0; +/** BIP32 hardened derivation flag (2^31) */ +static constexpr uint32_t BIP32_HARDENED = 0x80000000; + /** Parse an HD keypaths like "m/7/0'/2000". */ [[nodiscard]] bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypath); From 4e2e42c732ceb8ddfd1b474520d712aaf8b1885d Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 14 Apr 2026 03:24:52 -0400 Subject: [PATCH 02/21] refactor: deduplicate keypath element parsing Extract ParseKeyPathElement() as a shared helper in util/bip32 that parses a single BIP32 key path element (e.g. "0", "0'", "0h"). Both ParseHDKeypath() and the descriptor parser's ParseKeyPathNum() now delegate to it, eliminating duplicated parsing logic. --- src/script/descriptor.cpp | 24 +++--------- src/util/bip32.cpp | 53 +++++++++++++++++---------- src/util/bip32.h | 7 ++++ src/wallet/test/psbt_wallet_tests.cpp | 27 +++++++++++--- 4 files changed, 67 insertions(+), 44 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index d22209cb3d49..6a5aafb0bd65 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1754,25 +1754,13 @@ enum class ParseScriptContext { std::optional ParseKeyPathNum(std::span 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 index{ParseKeyPathElement(elem, hardened, error)}; + if (!index.has_value()) return std::nullopt; + if (hardened) { + has_hardened = true; + apostrophe = elem.back() == '\''; } - const auto p{ToIntegral(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(*p | (((uint32_t)hardened) << 31)); + return *index | (hardened ? BIP32_HARDENED : BIP32_UNHARDENED); } /** diff --git a/src/util/bip32.cpp b/src/util/bip32.cpp index 77d7cf868ef2..a27ddc288fec 100644 --- a/src/util/bip32.cpp +++ b/src/util/bip32.cpp @@ -11,6 +11,34 @@ #include #include #include +#include + +std::optional ParseKeyPathElement(std::span elem, bool& is_hardened, std::string& error) +{ + is_hardened = false; + const std::string_view raw{elem.begin(), elem.end()}; + if (elem.empty()) { + error = strprintf("Key path value '%s' is not a valid uint32", raw); + return std::nullopt; + } + + const char last = elem.back(); + if (last == '\'' || last == 'h') { + elem = elem.first(elem.size() - 1); + is_hardened = true; + } + + const auto number{ToIntegral(std::string_view{elem.begin(), elem.end()})}; + if (!number) { + error = strprintf("Key path value '%s' is not a valid uint32", raw); + return std::nullopt; + } + if (*number >= BIP32_HARDENED) { + error = strprintf("Key path value %u is out of range", *number); + return std::nullopt; + } + return *number; +} bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypath) { @@ -25,26 +53,11 @@ bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypa } return false; } - // Finds whether it is hardened - uint32_t path = 0; - size_t pos = item.find('\''); - 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 |= BIP32_HARDENED; - 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(item)}; - if (!number) { - return false; - } - path |= *number; - - keypath.push_back(path); + bool hardened = false; + std::string error; + const auto index{ParseKeyPathElement(std::span{item.data(), item.size()}, hardened, error)}; + if (!index.has_value()) return false; + keypath.push_back(*index | (hardened ? BIP32_HARDENED : BIP32_UNHARDENED)); first = false; } return true; diff --git a/src/util/bip32.h b/src/util/bip32.h index 265b02ce4641..9ecc5c052bee 100644 --- a/src/util/bip32.h +++ b/src/util/bip32.h @@ -6,6 +6,8 @@ #define BITCOIN_UTIL_BIP32_H #include +#include +#include #include #include @@ -14,6 +16,11 @@ static constexpr uint32_t BIP32_UNHARDENED = 0x0; /** BIP32 hardened derivation flag (2^31) */ static constexpr uint32_t BIP32_HARDENED = 0x80000000; +/** Parse a single key path element like "0", "0'", or "0h". + * Returns the child index and sets is_hardened, or nullopt on failure + * (in which case `error` is populated with a human-readable message). */ +std::optional ParseKeyPathElement(std::span elem, bool& is_hardened, std::string& error); + /** Parse an HD keypaths like "m/7/0'/2000". */ [[nodiscard]] bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypath); diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index 91b69b9d624b..5ea7ec08bcc2 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -114,8 +114,10 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath) BOOST_CHECK(ParseHDKeypath("42", keypath)); BOOST_CHECK(!ParseHDKeypath("m42", keypath)); - BOOST_CHECK(ParseHDKeypath("4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) - BOOST_CHECK(!ParseHDKeypath("4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1 + BOOST_CHECK(ParseHDKeypath("2147483647", keypath)); // 2147483647 == 0x7FFFFFFF (max non-hardened) + BOOST_CHECK(!ParseHDKeypath("2147483648", keypath)); // 2147483648 == 0x80000000 (would collide with hardened bit) + BOOST_CHECK(!ParseHDKeypath("4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) + BOOST_CHECK(!ParseHDKeypath("4294967296", keypath)); // 4294967296 > uint32_t max BOOST_CHECK(ParseHDKeypath("m", keypath)); BOOST_CHECK(!ParseHDKeypath("n", keypath)); @@ -129,8 +131,17 @@ 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/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)); @@ -147,11 +158,15 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath) BOOST_CHECK(ParseHDKeypath("m/1/", keypath)); BOOST_CHECK(!ParseHDKeypath("m/1//", keypath)); - BOOST_CHECK(ParseHDKeypath("m/0/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) - BOOST_CHECK(!ParseHDKeypath("m/0/4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1 + BOOST_CHECK(ParseHDKeypath("m/0/2147483647", keypath)); // 2147483647 == 0x7FFFFFFF (max non-hardened) + BOOST_CHECK(!ParseHDKeypath("m/0/2147483648", keypath)); // 2147483648 == 0x80000000 (would collide with hardened bit) + BOOST_CHECK(!ParseHDKeypath("m/0/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) + BOOST_CHECK(!ParseHDKeypath("m/0/4294967296", keypath)); // 4294967296 > uint32_t max - BOOST_CHECK(ParseHDKeypath("m/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) - BOOST_CHECK(!ParseHDKeypath("m/4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1 + BOOST_CHECK(ParseHDKeypath("m/2147483647", keypath)); // 2147483647 == 0x7FFFFFFF (max non-hardened) + BOOST_CHECK(!ParseHDKeypath("m/2147483648", keypath)); // 2147483648 == 0x80000000 (would collide with hardened bit) + BOOST_CHECK(!ParseHDKeypath("m/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max) + BOOST_CHECK(!ParseHDKeypath("m/4294967296", keypath)); // 4294967296 > uint32_t max } BOOST_AUTO_TEST_SUITE_END() From 57a1f309c3d423f8385c36035bea63bf55a1ef94 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 14 Jan 2026 16:04:48 +0100 Subject: [PATCH 03/21] crypto: add NonceFromBytes/NonceToBytes helpers Add helper methods to AEADChaCha20Poly1305 for converting between the internal Nonce96 type ({uint32_t, uint64_t}) and a 12-byte array representation (big-endian). RFC8439 defines the nonce as 96 opaque bits, but our implementation splits it. These helpers make it convenient to work with byte-based nonce representations. --- src/crypto/chacha20poly1305.h | 37 +++++++++++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 29 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h index 9a863dda97b4..7b21aca54ed9 100644 --- a/src/crypto/chacha20poly1305.h +++ b/src/crypto/chacha20poly1305.h @@ -34,6 +34,43 @@ class AEADChaCha20Poly1305 /** 96-bit nonce type. */ using Nonce96 = ChaCha20::Nonce96; + /** Size of the nonce in bytes. */ + static constexpr unsigned NONCE_SIZE = 12; + + /** Convert a 12-byte array to a Nonce96. + * + * RFC8439 defines the nonce as 96 opaque bits. This helper converts + * a byte array (big-endian) to the internal {uint32_t, uint64_t} representation. + */ + static Nonce96 NonceFromBytes(std::span nonce_bytes) noexcept + { + return { + (uint32_t(uint8_t(nonce_bytes[0])) << 24) | (uint32_t(uint8_t(nonce_bytes[1])) << 16) | + (uint32_t(uint8_t(nonce_bytes[2])) << 8) | uint32_t(uint8_t(nonce_bytes[3])), + (uint64_t(uint8_t(nonce_bytes[4])) << 56) | (uint64_t(uint8_t(nonce_bytes[5])) << 48) | + (uint64_t(uint8_t(nonce_bytes[6])) << 40) | (uint64_t(uint8_t(nonce_bytes[7])) << 32) | + (uint64_t(uint8_t(nonce_bytes[8])) << 24) | (uint64_t(uint8_t(nonce_bytes[9])) << 16) | + (uint64_t(uint8_t(nonce_bytes[10])) << 8) | uint64_t(uint8_t(nonce_bytes[11])) + }; + } + + /** Convert a Nonce96 back to a 12-byte array (big-endian). */ + static void NonceToBytes(Nonce96 nonce, std::span nonce_bytes) noexcept + { + nonce_bytes[0] = std::byte((nonce.first >> 24) & 0xFF); + nonce_bytes[1] = std::byte((nonce.first >> 16) & 0xFF); + nonce_bytes[2] = std::byte((nonce.first >> 8) & 0xFF); + nonce_bytes[3] = std::byte(nonce.first & 0xFF); + nonce_bytes[4] = std::byte((nonce.second >> 56) & 0xFF); + nonce_bytes[5] = std::byte((nonce.second >> 48) & 0xFF); + nonce_bytes[6] = std::byte((nonce.second >> 40) & 0xFF); + nonce_bytes[7] = std::byte((nonce.second >> 32) & 0xFF); + nonce_bytes[8] = std::byte((nonce.second >> 24) & 0xFF); + nonce_bytes[9] = std::byte((nonce.second >> 16) & 0xFF); + nonce_bytes[10] = std::byte((nonce.second >> 8) & 0xFF); + nonce_bytes[11] = std::byte(nonce.second & 0xFF); + } + /** Encrypt a message with a specified 96-bit nonce and aad. * * Requires cipher.size() = plain.size() + EXPANSION. diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index b348793bfb63..d128eafd978e 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -1051,6 +1052,34 @@ BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors) "14b94829deb27f0b1923a2af704ae5d6"); } +BOOST_AUTO_TEST_CASE(chacha20poly1305_nonce_conversion) +{ + // Test NonceFromBytes/NonceToBytes roundtrip + auto key = ParseHex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"); + auto nonce_bytes = ParseHex("000000000001020304050607"); + + // Convert bytes to Nonce96 + auto nonce = AEADChaCha20Poly1305::NonceFromBytes(std::span{nonce_bytes.data(), 12}); + // Expected: first 4 bytes = 0x00000000, next 8 bytes = 0x0001020304050607 + BOOST_CHECK_EQUAL(nonce.first, 0x00000000U); + BOOST_CHECK_EQUAL(nonce.second, 0x0001020304050607ULL); + + // Convert back to bytes and check roundtrip + std::array roundtrip_bytes; + AEADChaCha20Poly1305::NonceToBytes(nonce, roundtrip_bytes); + BOOST_CHECK(std::ranges::equal(nonce_bytes, roundtrip_bytes)); + + // Test with different values to ensure byte ordering is correct + auto nonce_bytes2 = ParseHex("aabbccdd11223344556677ff"); + auto nonce2 = AEADChaCha20Poly1305::NonceFromBytes(std::span{nonce_bytes2.data(), 12}); + BOOST_CHECK_EQUAL(nonce2.first, 0xaabbccddU); + BOOST_CHECK_EQUAL(nonce2.second, 0x11223344556677ffULL); + + std::array roundtrip_bytes2; + AEADChaCha20Poly1305::NonceToBytes(nonce2, roundtrip_bytes2); + BOOST_CHECK(std::ranges::equal(nonce_bytes2, roundtrip_bytes2)); +} + BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) { // Use rfc5869 test vectors but truncated to 32 bytes (our implementation only support length 32) From 349877e36f3ce2c9c2e680091e7febdfed7df8f8 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 15 Jan 2026 09:13:33 +0100 Subject: [PATCH 04/21] util: ParseHDKeypath allow h as hardened indicator BIP-380 specifies that descriptors can use either ' or h as the hardened indicator. ParseHDKeypath only supported the former. This prepares for using ParseHDKeypath with paths extracted from descriptors which typically use 'h' for shell-escaping convenience. Co-authored-by: Sjors Provoost --- src/wallet/test/psbt_wallet_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index 5ea7ec08bcc2..9d13bb20fed7 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -143,6 +143,9 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath) BOOST_CHECK(!ParseHDKeypath("m/'0/0'", keypath)); BOOST_CHECK(!ParseHDKeypath("m/h0/0'", keypath)); + BOOST_CHECK(ParseHDKeypath("m/0h/0h", keypath)); + BOOST_CHECK(!ParseHDKeypath("m/h0/0h", keypath)); + BOOST_CHECK(ParseHDKeypath("m/0/0", keypath)); BOOST_CHECK(!ParseHDKeypath("n/0/0", keypath)); From 4eb416f28f92fcb6cff6786f6f8fb7ec14f16fb6 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 14 Jan 2026 15:09:32 +0100 Subject: [PATCH 05/21] wallet: add WalletDescriptorInfo helper for descriptor serialization Introduces WalletDescriptorInfo struct and DescriptorInfoToUniValue() helper to avoid code duplication when serializing descriptor metadata to UniValue. Refactors listdescriptors RPC to use the new helper. Co-authored-by: Sjors Provoost --- src/wallet/rpc/backup.cpp | 30 +++--------------------------- src/wallet/rpc/util.cpp | 21 +++++++++++++++++++++ src/wallet/rpc/util.h | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 4b87ba232385..a65bde083ab0 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -509,16 +509,7 @@ RPCMethod listdescriptors() const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans(); - struct WalletDescInfo { - std::string descriptor; - uint64_t creation_time; - bool active; - std::optional internal; - std::optional> range; - int64_t next_index; - }; - - std::vector wallet_descriptors; + std::vector wallet_descriptors; for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) { const auto desc_spk_man = dynamic_cast(spk_man); if (!desc_spk_man) { @@ -544,23 +535,8 @@ RPCMethod listdescriptors() }); UniValue descriptors(UniValue::VARR); - for (const WalletDescInfo& info : wallet_descriptors) { - UniValue spk(UniValue::VOBJ); - spk.pushKV("desc", info.descriptor); - spk.pushKV("timestamp", info.creation_time); - spk.pushKV("active", info.active); - if (info.internal.has_value()) { - spk.pushKV("internal", info.internal.value()); - } - if (info.range.has_value()) { - UniValue range(UniValue::VARR); - range.push_back(info.range->first); - range.push_back(info.range->second - 1); - spk.pushKV("range", std::move(range)); - spk.pushKV("next", info.next_index); - spk.pushKV("next_index", info.next_index); - } - descriptors.push_back(std::move(spk)); + for (const WalletDescriptorInfo& info : wallet_descriptors) { + descriptors.push_back(DescriptorInfoToUniValue(info)); } UniValue response(UniValue::VOBJ); diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index 77a8745ced7e..07bbe9318805 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -162,4 +162,25 @@ void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) entry.pushKV("lastprocessedblock", std::move(lastprocessedblock)); } +UniValue DescriptorInfoToUniValue(const WalletDescriptorInfo& info) +{ + UniValue obj(UniValue::VOBJ); + obj.pushKV("desc", info.descriptor); + obj.pushKV("timestamp", info.creation_time); + obj.pushKV("active", info.active); + if (info.internal.has_value()) { + obj.pushKV("internal", info.internal.value()); + } + if (info.range.has_value()) { + UniValue range(UniValue::VARR); + range.push_back(info.range->first); + // range_end is exclusive internally, display as inclusive (hence -1) + range.push_back(info.range->second - 1); + obj.pushKV("range", std::move(range)); + obj.pushKV("next", info.next_index); + obj.pushKV("next_index", info.next_index); + } + return obj; +} + } // namespace wallet diff --git a/src/wallet/rpc/util.h b/src/wallet/rpc/util.h index 88fdc6639f41..6a111b6a32aa 100644 --- a/src/wallet/rpc/util.h +++ b/src/wallet/rpc/util.h @@ -56,6 +56,27 @@ void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, void HandleWalletError(const std::shared_ptr& wallet, DatabaseStatus& status, bilingual_str& error); void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +/** + * Information about a wallet descriptor, used for serialization to JSON. + * This struct captures all the metadata needed for listdescriptors output + * and importdescriptors input. + */ +struct WalletDescriptorInfo { + std::string descriptor; + uint64_t creation_time; + bool active; + std::optional internal; + std::optional> range; + int64_t next_index; +}; + +/** + * Convert a WalletDescriptorInfo to a UniValue object. + * The output format is compatible with both listdescriptors output and + * importdescriptors input. + */ +UniValue DescriptorInfoToUniValue(const WalletDescriptorInfo& info); } // namespace wallet #endif // BITCOIN_WALLET_RPC_UTIL_H From 5741144f2e0be5cdaa973ad1e233e03febbedb0d Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 14 Jan 2026 16:24:06 +0100 Subject: [PATCH 06/21] wallet: BIP-xxxx key normalization primitives Add functions for normalizing public keys to x-only format as specified in BIP-xxxx (Bitcoin Encrypted Backup). These primitives form the foundation for the encryption scheme. Functions added: - NormalizeToXOnly(): Convert CPubKey or CExtPubKey to 32-byte x-only format - IsNUMSPoint(): Check if a key is the BIP341 unspendable NUMS point - ExtractKeysFromDescriptor(): Extract and normalize all keys from a descriptor Includes test vectors from the BIP specification. Co-authored-by: Sjors Provoost --- src/test/CMakeLists.txt | 1 + .../data/bip_encrypted_backup_keys_types.json | 27 +++++ src/wallet/CMakeLists.txt | 1 + src/wallet/encryptedbackup.cpp | 92 ++++++++++++++ src/wallet/encryptedbackup.h | 73 +++++++++++ src/wallet/test/CMakeLists.txt | 1 + src/wallet/test/encrypted_backup_tests.cpp | 113 ++++++++++++++++++ 7 files changed, 308 insertions(+) create mode 100644 src/test/data/bip_encrypted_backup_keys_types.json create mode 100644 src/wallet/encryptedbackup.cpp create mode 100644 src/wallet/encryptedbackup.h create mode 100644 src/wallet/test/encrypted_backup_tests.cpp diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 1365d6c147a6..505d10a02947 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -144,6 +144,7 @@ include(TargetDataSources) target_json_data_sources(test_bitcoin data/base58_encode_decode.json data/bip341_wallet_vectors.json + data/bip_encrypted_backup_keys_types.json data/blockfilters.json data/key_io_invalid.json data/key_io_valid.json diff --git a/src/test/data/bip_encrypted_backup_keys_types.json b/src/test/data/bip_encrypted_backup_keys_types.json new file mode 100644 index 000000000000..ce91762d9865 --- /dev/null +++ b/src/test/data/bip_encrypted_backup_keys_types.json @@ -0,0 +1,27 @@ +[ + { + "description": "Xpub with origin and multipath", + "key": "[58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "Xpub with origin and w/o multipath", + "key": "[d4ab66f1/48'/1'/1'/2']tpubDFTxBKyUCgkwp5enwZh3t2FJ5AMJqmCWoh1NRT13qNYQb1iKTUrAG6u5gpsDYhG8cZGXouYWuQtzcuSVjPStTc4dwU6JqPMFtgaLGvSQXhi", + "expected": "8e886919a6b72579a28bd292505d2afd41c1b5012414c5e24d7b59f4abdfc0ce" + }, + { + "description": "Compressed public key", + "key": "02ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "X only public key", + "key": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "Uncompressed public key", + "key": "04ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b517089e956909c4c07e8529f45f3ff8904d28df5a181619e21bdf748a896322530039", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + } +] \ No newline at end of file diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 36fd3ef95aec..9b0ed984ece4 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(bitcoin_wallet STATIC EXCLUDE_FROM_ALL crypter.cpp db.cpp dump.cpp + encryptedbackup.cpp external_signer_scriptpubkeyman.cpp feebumper.cpp fees.cpp diff --git a/src/wallet/encryptedbackup.cpp b/src/wallet/encryptedbackup.cpp new file mode 100644 index 000000000000..a3801b0a7c92 --- /dev/null +++ b/src/wallet/encryptedbackup.cpp @@ -0,0 +1,92 @@ +// Copyright (c) 2025-present 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 + +#include + +#include +#include