Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
5 changes: 5 additions & 0 deletions src/hash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Copy link
Copy Markdown
Collaborator

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() in BIP32Hash().

Copy link
Copy Markdown
Member Author

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.

{
CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(nChild, 32).Finalize(output);
}

uint256 SHA256Uint256(const uint256& input)
{
uint256 result;
Expand Down
5 changes: 5 additions & 0 deletions src/hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ unsigned int MurmurHash3(unsigned int nHashSeed, Span<const unsigned char> 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).
Expand Down
28 changes: 28 additions & 0 deletions src/key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <secp256k1_ellswift.h>
#include <secp256k1_recovery.h>

#include <algorithm>

static secp256k1_context* secp256k1_context_sign = nullptr;

/** These functions are taken from the libsecp256k1 distribution and are very ugly. */
Expand Down Expand Up @@ -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<const unsigned char> 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<unsigned char, secure_allocator<unsigned char>> 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<const std::byte> ent32) const
{
assert(keydata);
Expand Down
5 changes: 5 additions & 0 deletions src/key.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const unsigned char> nChild, bool hardened, const ChainCode& cc) const;
Comment thread
PastaPastaPasta marked this conversation as resolved.

/**
* Verify thoroughly whether a private key and a public key match.
* This is done using a different mechanism than just regenerating it.
Expand Down
28 changes: 28 additions & 0 deletions src/pubkey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; })) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am not confident about keeping a compatibility mode, since Derive256 now has an implicit case purely based on input and if we have behaviour assuming that Derive256 will always emit a DIP-14 derivation, silently switching to BIP-32 derivation might not be ideal, would be preferable to fail entirely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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);
Expand Down
5 changes: 5 additions & 0 deletions src/pubkey.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const unsigned char> nChild, const ChainCode& cc) const;
};

/** An ElligatorSwift-encoded public key. */
Expand Down
205 changes: 205 additions & 0 deletions src/test/dip14_tests.cpp
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()
43 changes: 43 additions & 0 deletions src/test/fuzz/key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <test/fuzz/util.h>
#include <util/strencodings.h>

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
Expand Down Expand Up @@ -99,6 +100,48 @@ FUZZ_TARGET(key, .init = initialize_key)

const CPubKey pubkey = key.GetPubKey();

const auto test_derive256 = [&](const std::array<unsigned char, 32>& 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<unsigned char, 32> 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<unsigned char, 32> 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<unsigned char, 32> 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));
Expand Down
Loading
Loading