diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 22dea93afea6..df2773669860 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -106,6 +106,7 @@ BITCOIN_TESTS =\ test/cuckoocache_tests.cpp \ test/denialofservice_tests.cpp \ test/dip0020opcodes_tests.cpp \ + test/dip14_tests.cpp \ test/descriptor_tests.cpp \ test/dynamic_activation_thresholds_tests.cpp \ test/evo_assetlocks_tests.cpp \ diff --git a/src/hash.cpp b/src/hash.cpp index 0544468bfd94..bbb652f597b0 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -75,6 +75,11 @@ void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char he CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(num, 4).Finalize(output); } +void DIP14Hash(const ChainCode& chainCode, const unsigned char nChild[32], unsigned char header, const unsigned char data[32], unsigned char output[64]) +{ + CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(nChild, 32).Finalize(output); +} + uint256 SHA256Uint256(const uint256& input) { uint256 result; diff --git a/src/hash.h b/src/hash.h index 8db12e811cd4..bba188207066 100644 --- a/src/hash.h +++ b/src/hash.h @@ -242,6 +242,11 @@ unsigned int MurmurHash3(unsigned int nHashSeed, Span vData void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char header, const unsigned char data[32], unsigned char output[64]); +/** DIP-14 child key derivation HMAC: like BIP32Hash but with a 256-bit child + * index, serialized big-endian (ser256). Used for Dash Platform (DashPay) + * derivation paths. */ +void DIP14Hash(const ChainCode& chainCode, const unsigned char nChild[32], unsigned char header, const unsigned char data[32], unsigned char output[64]); + /** Return a HashWriter primed for tagged hashes (as specified in BIP 340). * * The returned object will have SHA256(tag) written to it twice (= 64 bytes). diff --git a/src/key.cpp b/src/key.cpp index 84a068d71454..9cad42d288e9 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -13,6 +13,8 @@ #include #include +#include + static secp256k1_context* secp256k1_context_sign = nullptr; /** These functions are taken from the libsecp256k1 distribution and are very ugly. */ @@ -305,6 +307,32 @@ bool CKey::Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const return ret; } +bool CKey::Derive256(CKey& keyChild, ChainCode& ccChild, Span nChild, bool hardened, const ChainCode& cc) const { + assert(IsValid()); + assert(IsCompressed()); + if (nChild.size() != 32) return false; + // DIP-14 compatibility mode: indexes below 2^32 derive exactly as BIP32, + // with the hardened flag folded into the high bit of the 32-bit index. + if (std::all_of(nChild.begin(), nChild.begin() + 28, [](unsigned char c) { return c == 0; })) { + uint32_t child32 = ReadBE32(nChild.data() + 28); + return Derive(keyChild, ccChild, child32 | (hardened ? 0x80000000u : 0), cc); + } + std::vector> vout(64); + if (!hardened) { + CPubKey pubkey = GetPubKey(); + assert(pubkey.size() == CPubKey::COMPRESSED_SIZE); + DIP14Hash(cc, nChild.data(), *pubkey.begin(), pubkey.begin() + 1, vout.data()); + } else { + assert(size() == 32); + DIP14Hash(cc, nChild.data(), 0, begin(), vout.data()); + } + memcpy(ccChild.begin(), vout.data() + 32, 32); + keyChild.Set(begin(), begin() + 32, true); + bool ret = secp256k1_ec_seckey_tweak_add(secp256k1_context_sign, (unsigned char*)keyChild.begin(), vout.data()); + if (!ret) keyChild.ClearKeyData(); + return ret; +} + EllSwiftPubKey CKey::EllSwiftCreate(Span ent32) const { assert(keydata); diff --git a/src/key.h b/src/key.h index c4c713c02f24..25c4277dd328 100644 --- a/src/key.h +++ b/src/key.h @@ -157,6 +157,11 @@ class CKey //! Derive BIP32 child key. [[nodiscard]] bool Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const; + //! Derive DIP-14 child key with a 256-bit index (32 bytes, big-endian). + //! Indexes below 2^32 fall back to BIP32 derivation for compatibility + //! (the hardened flag is then folded into the 32-bit index). + [[nodiscard]] bool Derive256(CKey& keyChild, ChainCode& ccChild, Span nChild, bool hardened, const ChainCode& cc) const; + /** * Verify thoroughly whether a private key and a public key match. * This is done using a different mechanism than just regenerating it. diff --git a/src/pubkey.cpp b/src/pubkey.cpp index 8de5143dbb0b..a69aa2fd1d7e 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -258,6 +258,34 @@ bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChi return true; } +bool CPubKey::Derive256(CPubKey& pubkeyChild, ChainCode& ccChild, Span nChild, const ChainCode& cc) const { + assert(IsValid()); + if (nChild.size() != 32) return false; + assert(size() == COMPRESSED_SIZE); + // DIP-14 compatibility mode: indexes below 2^32 derive exactly as BIP32. + // A hardened (high bit set) 32-bit index cannot be derived from a pubkey. + if (std::all_of(nChild.begin(), nChild.begin() + 28, [](unsigned char c) { return c == 0; })) { + uint32_t child32 = ReadBE32(nChild.data() + 28); + if (child32 >> 31) return false; + return Derive(pubkeyChild, ccChild, child32, cc); + } + unsigned char out[64]; + DIP14Hash(cc, nChild.data(), *begin(), begin() + 1, out); + memcpy(ccChild.begin(), out + 32, 32); + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, vch, size())) { + return false; + } + if (!secp256k1_ec_pubkey_tweak_add(secp256k1_context_static, &pubkey, out)) { + return false; + } + unsigned char pub[COMPRESSED_SIZE]; + size_t publen = COMPRESSED_SIZE; + secp256k1_ec_pubkey_serialize(secp256k1_context_static, pub, &publen, &pubkey, SECP256K1_EC_COMPRESSED); + pubkeyChild.Set(pub, pub + publen); + return true; +} + EllSwiftPubKey::EllSwiftPubKey(Span ellswift) noexcept { assert(ellswift.size() == SIZE); diff --git a/src/pubkey.h b/src/pubkey.h index 5de7fa9519a8..a696821e27a7 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -215,6 +215,11 @@ class CPubKey //! Derive BIP32 child pubkey. [[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const; + + //! Derive DIP-14 (non-hardened) child pubkey with a 256-bit index + //! (32 bytes, big-endian). Indexes below 2^32 fall back to BIP32 + //! derivation for compatibility; a hardened 32-bit index fails. + [[nodiscard]] bool Derive256(CPubKey& pubkeyChild, ChainCode& ccChild, Span nChild, const ChainCode& cc) const; }; /** An ElligatorSwift-encoded public key. */ diff --git a/src/test/dip14_tests.cpp b/src/test/dip14_tests.cpp new file mode 100644 index 000000000000..545c0fc1fe78 --- /dev/null +++ b/src/test/dip14_tests.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2026 The Dash 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 +#include +#include + +#include + +#include +#include +#include + +BOOST_FIXTURE_TEST_SUITE(dip14_tests, BasicTestingSetup) + +namespace { +//! One step of a DIP-14 derivation path: a 256-bit child index (big-endian) +//! plus hardened flag. +struct PathElement { + std::array index{}; + bool hardened{false}; +}; + +PathElement Elem256(const std::string& hex, bool hardened) +{ + const std::vector v{ParseHex(hex)}; + BOOST_REQUIRE_EQUAL(v.size(), 32U); + PathElement e; + std::copy(v.begin(), v.end(), e.index.begin()); + e.hardened = hardened; + return e; +} + +PathElement Elem32(uint32_t index, bool hardened) +{ + PathElement e; + WriteBE32(e.index.data() + 28, index); + e.hardened = hardened; + return e; +} + +//! Master key from the seed shared by all DIP-14 test vectors (dashpay/dips +//! dip-0014.md), from mnemonic "birth kingdom trash renew flavor utility +//! donkey gasp regular alert pave layer". +CExtKey MasterKey() +{ + const std::vector seed{ParseHex( + "b16d3782e714da7c55a397d5f19104cfed7ffa8036ac514509bbb50807f8ac59" + "8eeb26f0797bd8cc221a6cbff2168d90a5e9ee025a5bd977977b9eccd97894bb")}; + CExtKey master; + master.SetSeed(MakeByteSpan(seed)); + return master; +} + +void DerivePath(const std::vector& path, CKey& key_out, ChainCode& cc_out) +{ + const CExtKey master{MasterKey()}; + key_out = master.key; + cc_out = master.chaincode; + for (const PathElement& e : path) { + CKey child; + ChainCode cc_child; + BOOST_REQUIRE(key_out.Derive256(child, cc_child, e.index, e.hardened, cc_out)); + key_out = child; + cc_out = cc_child; + } +} + +std::string DerivedKeyHex(const std::vector& path) +{ + CKey key; + ChainCode cc; + DerivePath(path, key, cc); + return HexStr(Span{key.begin(), key.size()}); +} +} // namespace + +// DIP-14 test vector 1: m//'//0 +BOOST_AUTO_TEST_CASE(dip14_vector_1) +{ + const std::vector path{ + Elem256("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b", false), + Elem256("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6", true), + Elem256("4c4592ca670c983fc43397dfd21a6f427fac9b4ac53cb4dcdc6522ec51e81e79", false), + Elem32(0, false), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "e8781fdef72862968cd9a4d2df34edaf9dcc5b17629ec505f0d2d1a8ed6f9f09"); +} + +// DIP-14 test vector 2: m/9'/5'/15'/0'/'/'/0 (DIP-15 shape) +BOOST_AUTO_TEST_CASE(dip14_vector_2) +{ + const std::vector path{ + Elem32(9, true), + Elem32(5, true), + Elem32(15, true), + Elem32(0, true), + Elem256("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a", true), + Elem256("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5", true), + Elem32(0, false), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "fac40790776d171ee1db90899b5eb2df2f7d2aaf35ad56f07ffb8ed2c57f8e60"); +} + +// DIP-14 test vector 3: m/ (single 256-bit non-hardened step) +BOOST_AUTO_TEST_CASE(dip14_vector_3) +{ + const std::vector path{ + Elem256("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b", false), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "f6a95ae75ea8362d9478932f71b262b3d981918fe030316686a475dea4889938"); +} + +// DIP-14 test vector 4: m//' +BOOST_AUTO_TEST_CASE(dip14_vector_4) +{ + const std::vector path{ + Elem256("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b", false), + Elem256("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6", true), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "b898ad92d3a0698bc3117d3777d82676673816ce52f4fc2f1263a2f676825f90"); +} + +//! Indexes below 2^32 must derive exactly as BIP32, so a DIP-14 path mixing +//! 32-bit and 256-bit steps stays compatible with existing BIP32 code. +BOOST_AUTO_TEST_CASE(dip14_bip32_compatibility) +{ + const CExtKey master{MasterKey()}; + + CKey child_bip32, child_dip14; + ChainCode cc_bip32, cc_dip14; + BOOST_REQUIRE(master.key.Derive(child_bip32, cc_bip32, 5, master.chaincode)); + BOOST_REQUIRE(master.key.Derive256(child_dip14, cc_dip14, Elem32(5, false).index, false, master.chaincode)); + BOOST_CHECK(child_bip32.GetPrivKey() == child_dip14.GetPrivKey()); + BOOST_CHECK(cc_bip32 == cc_dip14); + + BOOST_REQUIRE(master.key.Derive(child_bip32, cc_bip32, 5 | 0x80000000u, master.chaincode)); + BOOST_REQUIRE(master.key.Derive256(child_dip14, cc_dip14, Elem32(5, false).index, true, master.chaincode)); + BOOST_CHECK(child_bip32.GetPrivKey() == child_dip14.GetPrivKey()); + BOOST_CHECK(cc_bip32 == cc_dip14); + + const CPubKey parent_pub{master.key.GetPubKey()}; + CPubKey pub_bip32, pub_dip14; + BOOST_REQUIRE(parent_pub.Derive(pub_bip32, cc_bip32, 5, master.chaincode)); + BOOST_REQUIRE(parent_pub.Derive256(pub_dip14, cc_dip14, Elem32(5, false).index, master.chaincode)); + BOOST_CHECK(pub_bip32 == pub_dip14); + BOOST_CHECK(cc_bip32 == cc_dip14); +} + +//! Non-hardened 256-bit public derivation must match private derivation +//! (this is what lets a contact derive our friendship addresses from an +//! exported xpub), and hardened derivation must be rejected on the public +//! side. +BOOST_AUTO_TEST_CASE(dip14_public_derivation_matches) +{ + CKey parent_key; + ChainCode parent_cc; + DerivePath({Elem32(9, true), Elem32(1, true), Elem32(15, true), Elem32(0, true)}, parent_key, parent_cc); + + const PathElement id_a{Elem256("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a", false)}; + const PathElement id_b{Elem256("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5", false)}; + + // Private side: parent/idA/idB + CKey mid_key, leaf_key; + ChainCode mid_cc, leaf_cc; + BOOST_REQUIRE(parent_key.Derive256(mid_key, mid_cc, id_a.index, false, parent_cc)); + BOOST_REQUIRE(mid_key.Derive256(leaf_key, leaf_cc, id_b.index, false, mid_cc)); + + // Public side: neuter parent, then derive idA/idB + const CPubKey parent_pub{parent_key.GetPubKey()}; + CPubKey mid_pub, leaf_pub; + ChainCode mid_pub_cc, leaf_pub_cc; + BOOST_REQUIRE(parent_pub.Derive256(mid_pub, mid_pub_cc, id_a.index, parent_cc)); + BOOST_REQUIRE(mid_pub.Derive256(leaf_pub, leaf_pub_cc, id_b.index, mid_pub_cc)); + + BOOST_CHECK(leaf_pub == leaf_key.GetPubKey()); + BOOST_CHECK(leaf_pub_cc == leaf_cc); + + // A hardened 32-bit index (high bit set) cannot be derived from a pubkey. + CPubKey unused; + ChainCode unused_cc; + BOOST_CHECK(!parent_pub.Derive256(unused, unused_cc, Elem32(0x80000000u, false).index, parent_cc)); +} + +BOOST_AUTO_TEST_CASE(dip14_rejects_invalid_index_sizes) +{ + const CExtKey master{MasterKey()}; + const CPubKey parent_pub{master.key.GetPubKey()}; + const std::array short_index{}; + const std::array long_index{}; + CKey child_key; + CPubKey child_pubkey; + ChainCode child_cc; + + BOOST_CHECK(!master.key.Derive256(child_key, child_cc, short_index, false, master.chaincode)); + BOOST_CHECK(!master.key.Derive256(child_key, child_cc, long_index, false, master.chaincode)); + BOOST_CHECK(!parent_pub.Derive256(child_pubkey, child_cc, short_index, master.chaincode)); + BOOST_CHECK(!parent_pub.Derive256(child_pubkey, child_cc, long_index, master.chaincode)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp index 08521b822aa9..46c2c9b069b6 100644 --- a/src/test/fuzz/key.cpp +++ b/src/test/fuzz/key.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -99,6 +100,48 @@ FUZZ_TARGET(key, .init = initialize_key) const CPubKey pubkey = key.GetPubKey(); + const auto test_derive256 = [&](const std::array& child_index) { + CKey child_key; + ChainCode child_chaincode; + const bool private_ok = key.Derive256(child_key, child_chaincode, child_index, false, random_uint256); + + CPubKey child_pubkey; + ChainCode child_pub_chaincode; + const bool public_ok = pubkey.Derive256(child_pubkey, child_pub_chaincode, child_index, random_uint256); + assert(private_ok == public_ok); + if (private_ok) { + assert(child_key.IsValid()); + assert(child_pubkey == child_key.GetPubKey()); + assert(child_chaincode == child_pub_chaincode); + } + + CKey hardened_child_key; + ChainCode hardened_child_chaincode; + if (key.Derive256(hardened_child_key, hardened_child_chaincode, child_index, true, random_uint256)) { + assert(hardened_child_key.IsValid()); + } + }; + + std::array wide_child_index; + std::copy(random_uint256.begin(), random_uint256.end(), wide_child_index.begin()); + wide_child_index.front() |= 1; + test_derive256(wide_child_index); + + std::array boundary_child_index{}; + std::copy(random_uint256.begin(), random_uint256.begin() + 4, boundary_child_index.end() - 4); + boundary_child_index[27] = 1; + test_derive256(boundary_child_index); + + std::array bip32_child_index{}; + std::copy(random_uint256.begin(), random_uint256.begin() + 4, bip32_child_index.end() - 4); + bip32_child_index[28] &= 0x7f; + test_derive256(bip32_child_index); + + bip32_child_index[28] |= 0x80; + CPubKey hardened_child_pubkey; + ChainCode hardened_child_chaincode; + assert(!pubkey.Derive256(hardened_child_pubkey, hardened_child_chaincode, bip32_child_index, random_uint256)); + { assert(pubkey.size() == 33); assert(key.VerifyPubKey(pubkey)); diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index 0cecc5006c14..c393f7d9a6bb 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -61,6 +61,7 @@ src/test/block_reward_reallocation_tests.cpp src/test/bls_tests.cpp src/test/coinjoin_*.cpp src/test/dip0020opcodes_tests.cpp +src/test/dip14_tests.cpp src/test/dynamic_activation*.cpp src/test/evo*.cpp src/test/llmq*.cpp