From 3b8fc64b7c4727eee14bd8ee1791d94c130fedb4 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 11 Aug 2026 23:20:07 -0500 Subject: [PATCH 01/21] feat(wallet): add Platform (DIP-9/13/14/15) key derivation helpers Pure BIP32/DIP-14 key-path math for the wallet's Platform key provider: DIP-9 feature-purpose paths, DIP-13 identity authentication/funding paths, DIP-15 friendship keychain paths with 256-bit non-hardened identity components, private and public (watch-only) derivation, the libsecp256k1 ECDH KDF used for DashPay contact request encryption, and a keyed seed fingerprint for pinning multi-seed wallets to one platform seed. The secp256k1 subtree is now built with the ECDH module enabled, which ComputeECDHSecret requires. Tests pin the DIP-14 test vectors (dashpay/dips dip-0014.md) through the path walker, public/private derivation consistency, ECDH symmetry and the seed fingerprint. --- configure.ac | 4 +- src/Makefile.am | 2 + src/Makefile.test.include | 1 + src/wallet/platformkeys.cpp | 141 ++++++++++++++++++++++ src/wallet/platformkeys.h | 113 ++++++++++++++++++ src/wallet/test/platformkeys_tests.cpp | 159 +++++++++++++++++++++++++ test/util/data/non-backported.txt | 2 + 7 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 src/wallet/platformkeys.cpp create mode 100644 src/wallet/platformkeys.h create mode 100644 src/wallet/test/platformkeys_tests.cpp diff --git a/configure.ac b/configure.ac index d36f8046cfa2..daf733fb2224 100644 --- a/configure.ac +++ b/configure.ac @@ -2077,7 +2077,9 @@ CPPFLAGS="$CPPFLAGS_TEMP" if test -n "$use_sanitizers"; then export SECP_CFLAGS="$SECP_CFLAGS $SANITIZER_CFLAGS" fi -ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery --disable-module-ecdh --disable-openssl-tests" +dnl The ECDH module is required by the wallet's Platform key provider +dnl (DashPay contact request encryption, wallet/platformkeys.cpp). +ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery --enable-module-ecdh --disable-openssl-tests" AC_CONFIG_SUBDIRS([src/dashbls src/secp256k1]) AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am index 0cf8ce802fdb..8580466b06f8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -478,6 +478,7 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/platformkeys.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ @@ -709,6 +710,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/hdchain.cpp \ wallet/interfaces.cpp \ wallet/load.cpp \ + wallet/platformkeys.cpp \ wallet/receive.cpp \ wallet/rpc/addresses.cpp \ wallet/rpc/backup.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 805083ffeb5f..a3c8cb3aead3 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -224,6 +224,7 @@ if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ wallet/test/coinjoin_tests.cpp \ + wallet/test/platformkeys_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ wallet/test/spend_tests.cpp \ wallet/test/wallet_tests.cpp \ diff --git a/src/wallet/platformkeys.cpp b/src/wallet/platformkeys.cpp new file mode 100644 index 000000000000..8ebf87238322 --- /dev/null +++ b/src/wallet/platformkeys.cpp @@ -0,0 +1,141 @@ +// 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 + +namespace wallet::platformkeys { + +Path IdentityAuthKeyPath(uint32_t coin_type, uint32_t identity_index, uint32_t key_index) +{ + // dashj DerivationPathFactory.blockchainIdentityECDSADerivationPath(index): + // m/9'/coin'/5'/0'(sub-feature)/0'(key type ECDSA)/identity'/key' + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_IDENTITIES), + PathElement::Hardened(IDENTITY_AUTHENTICATION), + PathElement::Hardened(AUTH_KEY_TYPE_ECDSA), + PathElement::Hardened(identity_index), + PathElement::Hardened(key_index), + }; +} + +Path IdentityFundingPath(uint32_t coin_type, uint32_t subfeature, uint32_t index) +{ + // dashj DerivationPathFactory.blockchainIdentity{Registration,Topup}Funding- + // DerivationPath() / identityInvitationFundingDerivationPath(), plus the + // hardened address index appended by AuthenticationKeyChain: + // m/9'/coin'/5'/{1,2,3}'/index' + assert(subfeature == IDENTITY_REGISTRATION_FUNDING || subfeature == IDENTITY_TOPUP_FUNDING || + subfeature == IDENTITY_INVITATION_FUNDING); + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_IDENTITIES), + PathElement::Hardened(subfeature), + PathElement::Hardened(index), + }; +} + +Path FriendshipPath(uint32_t coin_type, uint32_t account, Span user_a_id, Span user_b_id) +{ + // dashj FriendKeyChain.getContactPath(): + // m/9'/coin'/15'/account'/userA/userB with the two 256-bit identity ids + // NOT hardened (DIP-14/DIP-15), enabling watch-only xpub derivation. + assert(user_a_id.size() == 32); + assert(user_b_id.size() == 32); + std::array a, b; + std::copy(user_a_id.begin(), user_a_id.end(), a.begin()); + std::copy(user_b_id.begin(), user_b_id.end(), b.begin()); + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_DASHPAY), + PathElement::Hardened(account), + PathElement::Normal256(a), + PathElement::Normal256(b), + }; +} + +bool DeriveExtKey(Span seed, const Path& path, ExtKey256& out) +{ + CExtKey master; + master.SetSeed(MakeByteSpan(seed)); + if (!master.key.IsValid()) return false; + + CKey key{master.key}; + ChainCode chaincode{master.chaincode}; + + for (const auto& element : path) { + CKey child_key; + ChainCode child_cc; + bool ok{false}; + if (const auto* index32 = std::get_if(&element.index)) { + if (*index32 >> 31) return false; // must use the hardened flag instead + ok = key.Derive(child_key, child_cc, *index32 | (element.hardened ? 0x80000000u : 0), chaincode); + } else { + const auto& index256 = std::get>(element.index); + ok = key.Derive256(child_key, child_cc, index256, element.hardened, chaincode); + } + if (!ok) return false; + key = child_key; + chaincode = child_cc; + } + + out.key = key; + out.chaincode = chaincode; + return true; +} + +bool DerivePubKey(const ExtPubKey256& parent, const PathElement& element, ExtPubKey256& out) +{ + if (element.hardened) return false; + if (const auto* index32 = std::get_if(&element.index)) { + if (*index32 >> 31) return false; + return parent.pubkey.Derive(out.pubkey, out.chaincode, *index32, parent.chaincode); + } + const auto& index256 = std::get>(element.index); + return parent.pubkey.Derive256(out.pubkey, out.chaincode, index256, parent.chaincode); +} + +bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out) +{ + if (!key.IsValid() || !counterparty.IsValid()) return false; + + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, counterparty.data(), counterparty.size())) { + return false; + } + + secret_out.assign(32, 0); + // Default KDF: SHA256 of the compressed shared point — identical to + // dashj's Secp256k1ECDHAgreement (DashPay contact request encryption). + if (!secp256k1_ecdh(secp256k1_context_static, secret_out.data(), &pubkey, + UCharCast(key.begin()), nullptr, nullptr)) { + secret_out.clear(); + return false; + } + return true; +} + +std::array SeedFingerprint(Span seed) +{ + static constexpr char DOMAIN_KEY[]{"DashPlatform/seed-id/v1"}; + unsigned char mac[CHMAC_SHA256::OUTPUT_SIZE]; + CHMAC_SHA256{reinterpret_cast(DOMAIN_KEY), sizeof(DOMAIN_KEY) - 1} + .Write(seed.data(), seed.size()) + .Finalize(mac); + std::array out; + std::copy(mac, mac + out.size(), out.begin()); + return out; +} + +} // namespace wallet::platformkeys diff --git a/src/wallet/platformkeys.h b/src/wallet/platformkeys.h new file mode 100644 index 000000000000..f137090206f4 --- /dev/null +++ b/src/wallet/platformkeys.h @@ -0,0 +1,113 @@ +// 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. + +#ifndef BITCOIN_WALLET_PLATFORMKEYS_H +#define BITCOIN_WALLET_PLATFORMKEYS_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Dash Platform (DIP-9/13/14/15) key derivation for the wallet. + * + * This is pure BIP32/DIP-14 key math: it knows nothing about Platform + * documents, contracts or the network. It is only used through the + * interfaces::Wallet platform key provider methods. + * + * References: + * - DIP-9 feature-purpose derivation (m/9'/...) + * - DIP-13 identity keys (m/9'/coin'/5'/...) + * - DIP-14 256-bit child indexes (CKey::Derive256) + * - DIP-15 DashPay friendship keychains (m/9'/coin'/15'/account'/idA/idB) + * - dashj DerivationPathFactory.java / FriendKeyChain.java (reference impl) + */ +namespace wallet::platformkeys { + +//! One component of a derivation path: a 31-bit BIP32 index or a DIP-14 +//! 256-bit index (big-endian bytes), plus a hardened flag. +struct PathElement { + std::variant> index; + bool hardened{false}; + + static PathElement Hardened(uint32_t i) { return {i, true}; } + static PathElement Normal(uint32_t i) { return {i, false}; } + static PathElement Normal256(const std::array& i) { return {i, false}; } +}; +using Path = std::vector; + +// DIP-9 feature purpose, and DIP-13/DIP-15 feature types under it. +inline constexpr uint32_t FEATURE_PURPOSE{9}; +inline constexpr uint32_t FEATURE_IDENTITIES{5}; // DIP-13 +inline constexpr uint32_t FEATURE_DASHPAY{15}; // DIP-15 + +// DIP-13 sub-features: m/9'/coin'/5'/' +inline constexpr uint32_t IDENTITY_AUTHENTICATION{0}; +inline constexpr uint32_t IDENTITY_REGISTRATION_FUNDING{1}; +inline constexpr uint32_t IDENTITY_TOPUP_FUNDING{2}; +inline constexpr uint32_t IDENTITY_INVITATION_FUNDING{3}; + +// DIP-13 authentication key types: m/9'/coin'/5'/0'/' +inline constexpr uint32_t AUTH_KEY_TYPE_ECDSA{0}; +inline constexpr uint32_t AUTH_KEY_TYPE_BLS{1}; + +//! An extended key produced by walking a (possibly 256-bit) derivation path. +//! Unlike CExtKey this does not carry BIP32 serialization metadata; DIP-14 +//! extended-key serialization tracks the path separately. +struct ExtKey256 { + CKey key; + ChainCode chaincode; + + ExtKey256() = default; +}; + +struct ExtPubKey256 { + CPubKey pubkey; + ChainCode chaincode; +}; + +//! m/9'/coin'/5'/0'/0'/'/' — ECDSA identity authentication key +//! (all components hardened; key 0 = MASTER, key 1 = HIGH security level). +Path IdentityAuthKeyPath(uint32_t coin_type, uint32_t identity_index, uint32_t key_index); + +//! m/9'/coin'/5'/'/' — L1 funding keys for identity +//! registration (1'), top-ups (2') and invitations (3'). +Path IdentityFundingPath(uint32_t coin_type, uint32_t subfeature, uint32_t index); + +//! m/9'/coin'/15'/'// — DIP-15 friendship keychain +//! root. The two 256-bit identity ids are NOT hardened (this is what allows +//! watch-only derivation from an exported xpub); the receiving chain is +//! (userA = my id, userB = their id), the sending chain is the reverse. +Path FriendshipPath(uint32_t coin_type, uint32_t account, Span user_a_id, Span user_b_id); + +//! Derive the extended key at `path` from a BIP39 seed (the same 64-byte +//! seed CHDChain stores). Returns false if any derivation step fails. +[[nodiscard]] bool DeriveExtKey(Span seed, const Path& path, ExtKey256& out); + +//! Derive a (non-hardened) child extended pubkey. Fails on hardened steps. +[[nodiscard]] bool DerivePubKey(const ExtPubKey256& parent, const PathElement& element, ExtPubKey256& out); + +//! ECDH shared secret between `key` and `counterparty`, using the libsecp256k1 +//! ECDH KDF (SHA256 of the compressed shared point). This matches dashj's +//! KeyCrypterECDH / Secp256k1ECDHAgreement, used for DashPay contact request +//! encryption. Returns a 32-byte secret. +[[nodiscard]] bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out); + +//! Public 8-byte identifier of a platform seed: the first 8 bytes of +//! HMAC-SHA256(key="DashPlatform/seed-id/v1", msg=seed). Persisted in the +//! wallet (record "platform/seed-id") so platform data created from one seed +//! is never silently signed over with another in multi-seed wallets. Keyed +//! rather than a bare hash so the stored value is useless as a brute-force +//! oracle against weak seeds. +std::array SeedFingerprint(Span seed); + +} // namespace wallet::platformkeys + +#endif // BITCOIN_WALLET_PLATFORMKEYS_H diff --git a/src/wallet/test/platformkeys_tests.cpp b/src/wallet/test/platformkeys_tests.cpp new file mode 100644 index 000000000000..40afde391922 --- /dev/null +++ b/src/wallet/test/platformkeys_tests.cpp @@ -0,0 +1,159 @@ +// 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 + +using namespace wallet; +using namespace wallet::platformkeys; + +BOOST_FIXTURE_TEST_SUITE(platformkeys_tests, BasicTestingSetup) + +//! 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". +static SecureVector Dip14Seed() +{ + const std::vector seed{ParseHex( + "b16d3782e714da7c55a397d5f19104cfed7ffa8036ac514509bbb50807f8ac59" + "8eeb26f0797bd8cc221a6cbff2168d90a5e9ee025a5bd977977b9eccd97894bb")}; + return SecureVector{seed.begin(), seed.end()}; +} + +static std::array Arr32(const std::string& hex) +{ + const std::vector v{ParseHex(hex)}; + BOOST_REQUIRE_EQUAL(v.size(), 32U); + std::array out; + std::copy(v.begin(), v.end(), out.begin()); + return out; +} + +static std::string DerivedKeyHex(const Path& path) +{ + ExtKey256 out; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), path, out)); + return HexStr(Span{out.key.begin(), out.key.size()}); +} + +// DIP-14 test vector 1: m//'//0 +BOOST_AUTO_TEST_CASE(dip14_vector_1) +{ + const Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + PathElement{Arr32("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6"), true}, + PathElement::Normal256(Arr32("4c4592ca670c983fc43397dfd21a6f427fac9b4ac53cb4dcdc6522ec51e81e79")), + PathElement::Normal(0), + }; + 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 Path path{ + PathElement::Hardened(9), + PathElement::Hardened(5), + PathElement::Hardened(15), + PathElement::Hardened(0), + PathElement{Arr32("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a"), true}, + PathElement{Arr32("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5"), true}, + PathElement::Normal(0), + }; + 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 Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "f6a95ae75ea8362d9478932f71b262b3d981918fe030316686a475dea4889938"); +} + +// DIP-14 test vector 4: m//' +BOOST_AUTO_TEST_CASE(dip14_vector_4) +{ + const Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + PathElement{Arr32("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6"), true}, + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "b898ad92d3a0698bc3117d3777d82676673816ce52f4fc2f1263a2f676825f90"); +} + +//! Non-hardened 256-bit public derivation must match private derivation +//! (this is what lets a contact derive our friendship addresses from an +//! exported xpub). +BOOST_AUTO_TEST_CASE(dip14_public_derivation_matches) +{ + const Path parent_path{ + PathElement::Hardened(9), + PathElement::Hardened(1), + PathElement::Hardened(15), + PathElement::Hardened(0), + }; + ExtKey256 parent; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), parent_path, parent)); + + const auto id_a{Arr32("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a")}; + const auto id_b{Arr32("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5")}; + + // Private side: parent/idA/idB + Path leaf_path{parent_path}; + leaf_path.push_back(PathElement::Normal256(id_a)); + leaf_path.push_back(PathElement::Normal256(id_b)); + ExtKey256 leaf; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), leaf_path, leaf)); + + // Public side: neuter parent, then derive idA/idB + ExtPubKey256 pub_parent{parent.key.GetPubKey(), parent.chaincode}; + ExtPubKey256 pub_mid, pub_leaf; + BOOST_REQUIRE(DerivePubKey(pub_parent, PathElement::Normal256(id_a), pub_mid)); + BOOST_REQUIRE(DerivePubKey(pub_mid, PathElement::Normal256(id_b), pub_leaf)); + + BOOST_CHECK(pub_leaf.pubkey == leaf.key.GetPubKey()); + BOOST_CHECK(pub_leaf.chaincode == leaf.chaincode); + + // Hardened steps must be rejected on the public side. + ExtPubKey256 unused; + BOOST_CHECK(!DerivePubKey(pub_parent, PathElement{id_a, true}, unused)); + BOOST_CHECK(!DerivePubKey(pub_parent, PathElement::Hardened(0), unused)); +} + +//! ECDH secrets must be symmetric and match the libsecp256k1 KDF +//! (SHA256 of compressed shared point), as used by DashPay contact requests. +BOOST_AUTO_TEST_CASE(ecdh_symmetry) +{ + CKey a, b; + a.MakeNewKey(/*fCompressed=*/true); + b.MakeNewKey(/*fCompressed=*/true); + + SecureVector s_ab, s_ba; + BOOST_REQUIRE(ComputeECDHSecret(a, b.GetPubKey(), s_ab)); + BOOST_REQUIRE(ComputeECDHSecret(b, a.GetPubKey(), s_ba)); + BOOST_CHECK_EQUAL(s_ab.size(), 32U); + BOOST_CHECK(s_ab == s_ba); +} + +//! HMAC-SHA256 keyed fingerprints must be stable (the wallet's +//! "platform/seed-id" record depends on it) and sensitive to the seed. +BOOST_AUTO_TEST_CASE(seed_fingerprint_vector) +{ + const auto fingerprint{SeedFingerprint(Dip14Seed())}; + BOOST_CHECK_EQUAL(HexStr(fingerprint), "2e42d1b4f12197d1"); + + SecureVector tweaked{Dip14Seed()}; + tweaked[0] ^= 1; + BOOST_CHECK(SeedFingerprint(tweaked) != fingerprint); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index 84d49954a6a0..09067d0339d4 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -85,4 +85,6 @@ src/util/wpipe.* src/wallet/bip39* src/wallet/coinjoin.* src/wallet/hdchain.* +src/wallet/platformkeys.* +src/wallet/test/platformkeys_tests.cpp src/hash_x11.h From c6fe42956867b0dae5b367795d9f37c8adb9be47 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 11 Aug 2026 23:22:31 -0500 Subject: [PATCH 02/21] feat(wallet): add generic per-wallet Platform data records Adds an opaque string-keyed key/value store to the wallet database (DBKeys::PLATFORM_DATA) with write/erase, prefix queries, and a load path into CWallet::m_platform_data, exposed through interfaces::Wallet. Records persist in the wallet database and travel with backups; the wallet itself never interprets them. Tests cover write/prefix-query/erase and the ReadKeyValue load path. --- src/interfaces/wallet.h | 8 +++++++ src/wallet/interfaces.cpp | 10 +++++++++ src/wallet/test/walletdb_tests.cpp | 35 ++++++++++++++++++++++++++++++ src/wallet/wallet.cpp | 30 +++++++++++++++++++++++++ src/wallet/wallet.h | 11 ++++++++++ src/wallet/walletdb.cpp | 17 +++++++++++++++ src/wallet/walletdb.h | 6 +++++ 7 files changed, 117 insertions(+) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 9fc779906135..b6b4d1b59d07 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -170,6 +170,14 @@ class Wallet //! Save or remove receive request. virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0; + //! Write (or, with an empty value, erase) a generic Platform data + //! record. Records are persisted in the wallet database and travel with + //! backups; they are opaque to the wallet itself. + virtual bool writePlatformData(const std::string& key, const std::vector& value) = 0; + + //! All Platform data records whose key starts with prefix. + virtual std::map> getPlatformData(const std::string& prefix) = 0; + //! Display address on external signer virtual bool displayAddress(const CTxDestination& dest) = 0; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 4af755044de8..7bcfe5035235 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -316,6 +316,16 @@ class WalletImpl : public Wallet return value.empty() ? m_wallet->EraseAddressReceiveRequest(batch, dest, id) : m_wallet->SetAddressReceiveRequest(batch, dest, id, value); } + bool writePlatformData(const std::string& key, const std::vector& value) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->WritePlatformData(key, value); + } + std::map> getPlatformData(const std::string& prefix) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->GetPlatformData(prefix); + } bool displayAddress(const CTxDestination& dest) override { LOCK(m_wallet->cs_wallet); diff --git a/src/wallet/test/walletdb_tests.cpp b/src/wallet/test/walletdb_tests.cpp index 0b453b1dece4..3dd2c6e8aa79 100644 --- a/src/wallet/test/walletdb_tests.cpp +++ b/src/wallet/test/walletdb_tests.cpp @@ -61,5 +61,40 @@ BOOST_AUTO_TEST_CASE(walletdb_hdchain_type_mismatch) BOOST_CHECK_EQUAL(strErr, "Error reading wallet database: HD chain type mismatch"); } +BOOST_AUTO_TEST_CASE(walletdb_platform_data_records) +{ + LOCK(m_wallet.cs_wallet); + + // Round-trip through the in-memory map: write, prefix query, erase. + const std::vector value_a{0x01, 0x02}; + const std::vector value_b{0x03}; + BOOST_CHECK(m_wallet.WritePlatformData("platform/identity/0", value_a)); + BOOST_CHECK(m_wallet.WritePlatformData("platform/identity/1", value_b)); + BOOST_CHECK(m_wallet.WritePlatformData("platform/seed-id", value_b)); + + auto records{m_wallet.GetPlatformData("platform/identity/")}; + BOOST_CHECK_EQUAL(records.size(), 2U); + BOOST_CHECK(records.at("platform/identity/0") == value_a); + BOOST_CHECK(records.at("platform/identity/1") == value_b); + BOOST_CHECK_EQUAL(m_wallet.GetPlatformData("").size(), 3U); + BOOST_CHECK(m_wallet.GetPlatformData("platform/idem").empty()); + + // An empty value erases the record. + BOOST_CHECK(m_wallet.WritePlatformData("platform/identity/0", {})); + records = m_wallet.GetPlatformData("platform/identity/"); + BOOST_CHECK_EQUAL(records.size(), 1U); + BOOST_CHECK_EQUAL(records.count("platform/identity/0"), 0U); + + // The wallet-load path (ReadKeyValue) must populate the same map. + CDataStream ssKey(SER_DISK, CLIENT_VERSION); + CDataStream ssValue(SER_DISK, CLIENT_VERSION); + ssKey << std::make_pair(DBKeys::PLATFORM_DATA, std::string{"platform/loaded"}); + ssValue << value_a; + std::string strType, strErr; + BOOST_CHECK(ReadKeyValue(&m_wallet, ssKey, ssValue, strType, strErr)); + BOOST_CHECK_EQUAL(strType, DBKeys::PLATFORM_DATA); + BOOST_CHECK(m_wallet.GetPlatformData("platform/loaded").at("platform/loaded") == value_a); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace wallet diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 6a941af05ca2..ff6297abdc26 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3811,6 +3811,36 @@ bool CWallet::WriteGovernanceObject(const Governance::Object& obj) return batch.WriteGovernanceObject(obj) && LoadGovernanceObject(obj); } +void CWallet::LoadPlatformData(const std::string& key, const std::vector& value) +{ + AssertLockHeld(cs_wallet); + m_platform_data[key] = value; +} + +bool CWallet::WritePlatformData(const std::string& key, const std::vector& value) +{ + AssertLockHeld(cs_wallet); + WalletBatch batch(GetDatabase()); + if (value.empty()) { + m_platform_data.erase(key); + return batch.ErasePlatformData(key); + } + if (!batch.WritePlatformData(key, value)) return false; + m_platform_data[key] = value; + return true; +} + +std::map> CWallet::GetPlatformData(const std::string& prefix) const +{ + AssertLockHeld(cs_wallet); + std::map> ret; + for (auto it = m_platform_data.lower_bound(prefix); it != m_platform_data.end(); ++it) { + if (it->first.compare(0, prefix.size(), prefix) != 0) break; + ret.emplace(it->first, it->second); + } + return ret; +} + std::vector CWallet::GetGovernanceObjects() { AssertLockHeld(cs_wallet); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 059e7712c6c4..5338f397bd61 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -488,6 +488,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati // Map from governance object hash to governance object, they are added by gobject_prepare. std::map m_gobjects; + // Generic per-wallet key/value records used by Dash Platform integration + // (opaque to the wallet; persisted as DBKeys::PLATFORM_DATA). + std::map> m_platform_data; + typedef std::map MasterKeyMap; MasterKeyMap mapMasterKeys; unsigned int nMasterKeyMaxID = 0; @@ -1003,6 +1007,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati /** Returns a vector containing pointers to the governance objects in m_gobjects */ std::vector GetGovernanceObjects() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** Load a Platform data record into m_platform_data (wallet load). */ + void LoadPlatformData(const std::string& key, const std::vector& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** Write (or, with an empty value, erase) a Platform data record. */ + bool WritePlatformData(const std::string& key, const std::vector& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** All Platform data records whose key starts with prefix. */ + std::map> GetPlatformData(const std::string& prefix) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** * Blocks until the wallet state is up-to-date to /at least/ the current * chain at the time this function is entered diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 4b82c166a345..a8b3f08592b0 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -55,6 +55,7 @@ const std::string MINVERSION{"minversion"}; const std::string NAME{"name"}; const std::string OLD_KEY{"wkey"}; const std::string ORDERPOSNEXT{"orderposnext"}; +const std::string PLATFORM_DATA{"platform_data"}; const std::string POOL{"pool"}; const std::string PURPOSE{"purpose"}; const std::string PRIVATESEND_SALT{"ps_salt"}; @@ -247,6 +248,16 @@ bool WalletBatch::WriteGovernanceObject(const Governance::Object& obj) return WriteIC(std::make_pair(DBKeys::G_OBJECT, obj.GetHash()), obj, false); } +bool WalletBatch::WritePlatformData(const std::string& key, const std::vector& value) +{ + return WriteIC(std::make_pair(DBKeys::PLATFORM_DATA, key), value, true); +} + +bool WalletBatch::ErasePlatformData(const std::string& key) +{ + return EraseIC(std::make_pair(DBKeys::PLATFORM_DATA, key)); +} + bool WalletBatch::WriteActiveScriptPubKeyMan(const uint256& id, bool internal) { std::string key = internal ? DBKeys::ACTIVEINTERNALSPK : DBKeys::ACTIVEEXTERNALSPK; @@ -645,6 +656,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, strErr = "Invalid governance object: LoadGovernanceObject"; return false; } + } else if (strType == DBKeys::PLATFORM_DATA) { + std::string strKey; + std::vector vchValue; + ssKey >> strKey; + ssValue >> vchValue; + pwallet->LoadPlatformData(strKey, vchValue); } else if (strType == DBKeys::OLD_KEY) { strErr = "Found unsupported 'wkey' record, try loading with version 0.17"; return false; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 30c6aebdca30..f06177efeb2f 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -87,6 +87,7 @@ extern const std::string MINVERSION; extern const std::string NAME; extern const std::string OLD_KEY; extern const std::string ORDERPOSNEXT; +extern const std::string PLATFORM_DATA; extern const std::string POOL; extern const std::string PURPOSE; extern const std::string PRIVATESEND_SALT; @@ -230,6 +231,11 @@ class WalletBatch /** Write a CGovernanceObject to the database */ bool WriteGovernanceObject(const Governance::Object& obj); + //! Generic per-wallet key/value records used by Dash Platform + //! integration (flow state, identity metadata). Opaque to the wallet. + bool WritePlatformData(const std::string& key, const std::vector& value); + bool ErasePlatformData(const std::string& key); + bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase); bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret, const std::vector& crypted_mnemonic, const std::vector& crypted_mnemonic_passphrase); bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor); From 01c8a8a8fce8cdb4f0c1a71e2398a91ceed6cee2 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 11 Aug 2026 23:28:57 -0500 Subject: [PATCH 03/21] feat(wallet): add DIP-15 friendship keychain import and platform key provider seams Exposes a platform key provider through interfaces::Wallet: DIP-13 identity authentication/funding pubkeys and compact signatures, ECDH secrets for DashPay contact requests, DIP-15 friendship xpubs, and a stateless contact payment-destination derivation from a stored xpub. importFriendshipKeychains imports only the wallet's OWN receiving chain as a ranged private descriptor. The contact's receiving chain is deliberately never imported: its scriptPubKeys must not be IsMine, or payments to the contact would decompose as payments-to-self. Contact payment destinations are derived statelessly from the contact's xpub instead. GetPlatformSeed picks the backing BIP39 seed deterministically for multi-seed descriptor wallets: a pinned platform/seed-id record wins, otherwise the candidate from the lowest spk_man ID; legacy wallets use their HD chain seed. Tests cover own-chain spendability (ISMINE_SPENDABLE and AvailableCoins), the contact chain staying ISMINE_NO, deterministic seed selection with the seed-id override, and seed-only-restore rederivation of auth keys, friendship xpubs, ECDH secrets and imported funds, including import idempotency. --- src/Makefile.am | 2 + src/interfaces/wallet.h | 54 ++++ src/wallet/interfaces.cpp | 126 +++++++++ src/wallet/platformseed.cpp | 65 +++++ src/wallet/platformseed.h | 29 ++ src/wallet/test/platformkeys_tests.cpp | 354 +++++++++++++++++++++++++ test/util/data/non-backported.txt | 1 + 7 files changed, 631 insertions(+) create mode 100644 src/wallet/platformseed.cpp create mode 100644 src/wallet/platformseed.h diff --git a/src/Makefile.am b/src/Makefile.am index 8580466b06f8..38db9b939fb8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -479,6 +479,7 @@ BITCOIN_CORE_H = \ wallet/ismine.h \ wallet/load.h \ wallet/platformkeys.h \ + wallet/platformseed.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ @@ -711,6 +712,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/interfaces.cpp \ wallet/load.cpp \ wallet/platformkeys.cpp \ + wallet/platformseed.cpp \ wallet/receive.cpp \ wallet/rpc/addresses.cpp \ wallet/rpc/backup.cpp \ diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index b6b4d1b59d07..d662c0695aaf 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -16,10 +16,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -142,6 +144,58 @@ class Wallet //! Sign special transaction payload virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector& vchSig) = 0; + //! Dash Platform (DIP-13) key classes served by the platform key provider + //! methods below. Raw private keys never cross this interface; the wallet + //! derives keys from its HD seed on demand and only returns public keys, + //! signatures and ECDH secrets. All methods fail when the wallet is + //! locked or has no HD seed. + enum class PlatformKeyType { + IdentityAuth, //!< m/9'/coin'/5'/0'/0'/'/' + RegistrationFunding, //!< m/9'/coin'/5'/1'/' + TopupFunding, //!< m/9'/coin'/5'/2'/' + InvitationFunding, //!< m/9'/coin'/5'/3'/' + }; + + //! Derive and return the public key for a platform key. For + //! IdentityAuth, account is the identity index; for funding keys it is + //! ignored. + virtual bool getPlatformPubKey(PlatformKeyType type, uint32_t account, uint32_t index, CPubKey& pubkey_out) = 0; + + //! Sign a 32-byte digest with a platform key (compact/recoverable ECDSA, + //! as used by Platform state transitions). + virtual bool signPlatformDigest(PlatformKeyType type, uint32_t account, uint32_t index, const uint256& digest, std::vector& vchSig) = 0; + + //! ECDH shared secret between the identity authentication key + //! (identity_index, key_index) and a counterparty public key, using the + //! libsecp256k1 ECDH KDF. Used for DashPay contact request encryption. + virtual bool platformECDHSecret(uint32_t identity_index, uint32_t key_index, const CPubKey& counterparty, SecureVector& secret_out) = 0; + + //! DIP-15 friendship extended public key (pubkey + chain code) at + //! m/9'/coin'/15'/'//. For our receiving chain + //! user_a is our identity id and user_b the contact's. + virtual bool getFriendshipXpub(uint32_t account, const uint256& user_a_id, const uint256& user_b_id, CPubKey& pubkey_out, uint256& chaincode_out) = 0; + + //! Import the local private DIP-15 receiving chain for a friendship as a + //! ranged descriptor. The contact's own receiving chain is deliberately + //! never imported (its outputs must not be IsMine, or payments to the + //! contact would decompose as payments-to-self); payment destinations are + //! derived statelessly via getFriendshipPaymentDestination instead. + //! creation_time (0 = unknown/genesis) bounds later rescans; re-importing + //! an already-imported friendship updates it in place. + virtual bool importFriendshipKeychains(uint32_t account, const uint256& my_id, + const uint256& their_id, int64_t creation_time, const std::string& label, std::string& error) = 0; + + //! Derive a contact payment destination from their serialized DIP-15 + //! friendship xpub without advancing any wallet-global keypool. + virtual bool getFriendshipPaymentDestination(const CPubKey& their_pubkey, + const uint256& their_chaincode, uint32_t index, CTxDestination& destination_out) = 0; + + //! Fingerprint of the seed the platform key provider derives from + //! (wallet::platformkeys::SeedFingerprint), or nullopt when no seed is + //! available (locked, watch-only, or no mnemonic). Never prompts for an + //! unlock. + virtual std::optional> getPlatformSeedId() = 0; + //! Return whether wallet has private key. virtual bool isSpendable(const CScript& script) = 0; virtual bool isSpendable(const CTxDestination& dest) = 0; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 7bcfe5035235..713056fa50e7 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -5,14 +5,17 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include +#include