-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add DIP-14 256-bit child key derivation (Derive256) #7511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -258,6 +258,34 @@ bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChi | |
| return true; | ||
| } | ||
|
|
||
| bool CPubKey::Derive256(CPubKey& pubkeyChild, ChainCode& ccChild, Span<const unsigned char> 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; })) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am not confident about keeping a compatibility mode, since
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping compatibility mode is intentional here. DIP-14 defines the below-2^32 compatibility behavior, and this PR's vectors explicitly lock that contract to BIP-32. In the DashPay PR train, higher-level callers do not expose this as an untyped choice: wallet::platformkeys::PathElement distinguishes uint32_t from the 32-byte identity path element and routes them to Derive and Derive256 respectively. Removing compatibility in this low-level primitive would diverge from the stated DIP-14 contract without improving the later caller boundary. 🤖 Posted autonomously by Codex on behalf of pasta. |
||
| 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<const std::byte> ellswift) noexcept | ||
| { | ||
| assert(ellswift.size() == SIZE); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <crypto/common.h> | ||
| #include <key.h> | ||
| #include <pubkey.h> | ||
| #include <span.h> | ||
| #include <test/util/setup_common.h> | ||
| #include <util/strencodings.h> | ||
|
|
||
| #include <boost/test/unit_test.hpp> | ||
|
|
||
| #include <array> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| 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<unsigned char, 32> index{}; | ||
| bool hardened{false}; | ||
| }; | ||
|
|
||
| PathElement Elem256(const std::string& hex, bool hardened) | ||
| { | ||
| const std::vector<unsigned char> 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<unsigned char> seed{ParseHex( | ||
| "b16d3782e714da7c55a397d5f19104cfed7ffa8036ac514509bbb50807f8ac59" | ||
| "8eeb26f0797bd8cc221a6cbff2168d90a5e9ee025a5bd977977b9eccd97894bb")}; | ||
| CExtKey master; | ||
| master.SetSeed(MakeByteSpan(seed)); | ||
| return master; | ||
| } | ||
|
|
||
| void DerivePath(const std::vector<PathElement>& 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<PathElement>& path) | ||
| { | ||
| CKey key; | ||
| ChainCode cc; | ||
| DerivePath(path, key, cc); | ||
| return HexStr(Span{key.begin(), key.size()}); | ||
| } | ||
| } // namespace | ||
|
|
||
| // DIP-14 test vector 1: m/<id1>/<id2>'/<id3>/0 | ||
| BOOST_AUTO_TEST_CASE(dip14_vector_1) | ||
| { | ||
| const std::vector<PathElement> 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'/<idA>'/<idB>'/0 (DIP-15 shape) | ||
| BOOST_AUTO_TEST_CASE(dip14_vector_2) | ||
| { | ||
| const std::vector<PathElement> 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/<id> (single 256-bit non-hardened step) | ||
| BOOST_AUTO_TEST_CASE(dip14_vector_3) | ||
| { | ||
| const std::vector<PathElement> path{ | ||
| Elem256("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b", false), | ||
| }; | ||
| BOOST_CHECK_EQUAL(DerivedKeyHex(path), "f6a95ae75ea8362d9478932f71b262b3d981918fe030316686a475dea4889938"); | ||
| } | ||
|
|
||
| // DIP-14 test vector 4: m/<id1>/<id2>' | ||
| BOOST_AUTO_TEST_CASE(dip14_vector_4) | ||
| { | ||
| const std::vector<PathElement> 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<unsigned char, 31> short_index{}; | ||
| const std::array<unsigned char, 33> 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably create a type for children that can accept 32-bit or 256-bit integers and encodes them appropriately embedding the BE semantics as part of the wire definition. Would also apply to the
WriteBE32()inBIP32Hash().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That typed distinction belongs at the wallet path-policy layer, and the later DashPay train already supplies it: wallet::platformkeys::PathElement is a variant of uint32_t and std::array<uint8_t, 32>, routing each case to Derive or Derive256. Pulling that policy type into BIP32Hash/CKey in this foundational PR would couple the low-level crypto API to a later wallet abstraction and widen the dependency PR. I am therefore leaving this primitive byte-oriented and keeping the type-safe selection in the downstream layer.
🤖 Posted autonomously by Codex on behalf of pasta.