Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3b8fc64
feat(wallet): add Platform (DIP-9/13/14/15) key derivation helpers
PastaPastaPasta Aug 12, 2026
c6fe429
feat(wallet): add generic per-wallet Platform data records
PastaPastaPasta Aug 12, 2026
01c8a8a
feat(wallet): add DIP-15 friendship keychain import and platform key …
PastaPastaPasta Aug 12, 2026
a67dbc2
fix(wallet): validate parent pubkey before DIP-14 public derivation
PastaPastaPasta Aug 12, 2026
d583a42
fix(wallet): fail platform seed selection when the pinned seed is una…
PastaPastaPasta Aug 12, 2026
2cd5bcd
fix(wallet): preserve friendship descriptor state on re-import
PastaPastaPasta Aug 12, 2026
005459f
fix(wallet): drop platform data from memory only after the database e…
PastaPastaPasta Aug 12, 2026
31637d7
fix(wallet): fail platform seed selection on a malformed seed pin
PastaPastaPasta Aug 12, 2026
3a430b5
fix(wallet): require a full unlock before serving the legacy platform…
PastaPastaPasta Aug 12, 2026
47c0751
test(wallet): pin friendship derivation against rust-dashcore key-wallet
PastaPastaPasta Aug 13, 2026
b78f3cf
chore(wallet): annotate m_platform_data locking, add missing includes
PastaPastaPasta Aug 13, 2026
80a46ec
fix(wallet): skip empty-mnemonic descriptors in platform seed selection
PastaPastaPasta Aug 13, 2026
3f954b4
fix(wallet): reject unknown platform key types before derivation
PastaPastaPasta Aug 13, 2026
b1da3d4
fix(wallet): treat corrupt platform data records as wallet corruption
PastaPastaPasta Aug 13, 2026
d5e41f4
refactor(wallet): drop the unused friendship import label parameter
PastaPastaPasta Aug 13, 2026
d58facb
fix(wallet): keep damaged Platform cache records noncritical on load
PastaPastaPasta Aug 17, 2026
6a92a1c
fix(wallet): validate mnemonics before deriving the platform seed
PastaPastaPasta Aug 17, 2026
449da14
wallet: make the Platform seed provider descriptor-wallet-only
PastaPastaPasta Aug 18, 2026
81ba952
refactor(wallet): confine Platform derivation to key managers
PastaPastaPasta Aug 18, 2026
939ee20
refactor(wallet): avoid serializing friendship xprv
PastaPastaPasta Aug 18, 2026
b2a3c40
fix(wallet): derive Platform keys from descriptor root
PastaPastaPasta Aug 19, 2026
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
4 changes: 3 additions & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ BITCOIN_CORE_H = \
wallet/hdchain.h \
wallet/ismine.h \
wallet/load.h \
wallet/platformkeys.h \
wallet/platformtypes.h \
wallet/receive.h \
wallet/rpc/util.h \
wallet/rpc/wallet.h \
Expand Down Expand Up @@ -709,6 +711,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 \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
29 changes: 29 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <util/message.h>
#include <util/result.h>
#include <util/ui_change_type.h>
#include <wallet/platformtypes.h>

#include <cstdint>
#include <functional>
Expand Down Expand Up @@ -142,6 +143,26 @@ class Wallet
//! Sign special transaction payload
virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector<unsigned char>& vchSig) = 0;

//! Derive and return a public Platform key. The mnemonic-owning key
//! manager performs derivation without exposing the wallet's root seed.
virtual wallet::PlatformKeyResult<CPubKey> getPlatformPubKey(const wallet::PlatformKeyRequest& request) = 0;

//! Sign a 32-byte digest with a platform key (compact/recoverable ECDSA,
//! as used by Platform state transitions).
virtual wallet::PlatformKeyResult<std::vector<unsigned char>> signPlatformDigest(const wallet::PlatformKeyRequest& request,
const uint256& digest) = 0;

//! ECDH shared secret between the identity authentication key
//! and a counterparty public key, using the libsecp256k1 ECDH KDF.
virtual wallet::PlatformKeyResult<SecureVector> platformECDHSecret(const wallet::IdentityAuthKey& key,
const CPubKey& counterparty) = 0;

//! Ensure our private DIP-15 receiving chain is imported as a ranged
//! descriptor and return the corresponding public chain. Derivation,
//! descriptor update and result publication are one wallet-locked action.
virtual wallet::PlatformKeyResult<wallet::FriendshipXpub> ensureFriendshipReceivingKeychain(
const wallet::FriendshipKeychainRequest& request) = 0;

//! Return whether wallet has private key.
virtual bool isSpendable(const CScript& script) = 0;
virtual bool isSpendable(const CTxDestination& dest) = 0;
Expand Down Expand Up @@ -170,6 +191,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<unsigned char>& value) = 0;

//! All Platform data records whose key starts with prefix.
virtual std::map<std::string, std::vector<unsigned char>> getPlatformData(const std::string& prefix) = 0;

//! Display address on external signer
virtual bool displayAddress(const CTxDestination& dest) = 0;

Expand Down
31 changes: 31 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <interfaces/wallet.h>

#include <chain.h>
#include <chainparams.h>
#include <coinjoin/client.h>
#include <consensus/amount.h>
#include <interfaces/chain.h>
Expand Down Expand Up @@ -33,6 +34,7 @@
#include <wallet/wallet.h>
#include <wallet/hdchain.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/walletutil.h>
#include <evo/deterministicmns.h>
#include <masternode/sync.h>
#include <txdb.h>
Expand Down Expand Up @@ -239,6 +241,25 @@ class WalletImpl : public Wallet
{
return m_wallet->SignSpecialTxPayload(hash, keyid, vchSig);
}
wallet::PlatformKeyResult<CPubKey> getPlatformPubKey(const wallet::PlatformKeyRequest& request) override
{
return m_wallet->GetPlatformPubKey(request);
}
wallet::PlatformKeyResult<std::vector<unsigned char>> signPlatformDigest(const wallet::PlatformKeyRequest& request,
const uint256& digest) override
{
return m_wallet->SignPlatformDigest(request, digest);
}
wallet::PlatformKeyResult<SecureVector> platformECDHSecret(const wallet::IdentityAuthKey& key,
const CPubKey& counterparty) override
{
return m_wallet->PlatformECDHSecret(key, counterparty);
}
wallet::PlatformKeyResult<wallet::FriendshipXpub> ensureFriendshipReceivingKeychain(
const wallet::FriendshipKeychainRequest& request) override
{
return m_wallet->EnsureFriendshipReceivingKeychain(request);
}
bool isSpendable(const CScript& script) override
{
LOCK(m_wallet->cs_wallet);
Expand Down Expand Up @@ -316,6 +337,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<unsigned char>& value) override
{
LOCK(m_wallet->cs_wallet);
return m_wallet->WritePlatformData(key, value);
}
std::map<std::string, std::vector<unsigned char>> 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);
Expand Down
162 changes: 162 additions & 0 deletions src/wallet/platformkeys.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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 <wallet/platformkeys.h>

#include <secp256k1.h>
#include <secp256k1_ecdh.h>

#include <algorithm>
#include <cassert>
#include <type_traits>

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 PlatformKeyPath(uint32_t coin_type, const PlatformKeyRequest& request)
{
return std::visit(
[coin_type](const auto& key) -> Path {
using Key = std::decay_t<decltype(key)>;
if constexpr (std::is_same_v<Key, IdentityAuthKey>) {
return IdentityAuthKeyPath(coin_type, key.identity_index, key.key_index);
} else {
uint32_t subfeature;
PathElement index;
if constexpr (std::is_same_v<Key, RegistrationFundingKey>) {
subfeature = IDENTITY_REGISTRATION_FUNDING;
index = PathElement::Normal(key.identity_index);
} else if constexpr (std::is_same_v<Key, TopupFundingKey>) {
subfeature = IDENTITY_TOPUP_FUNDING;
index = PathElement::Normal(key.funding_index);
} else {
static_assert(std::is_same_v<Key, InvitationFundingKey>);
subfeature = IDENTITY_INVITATION_FUNDING;
index = PathElement::Hardened(key.invitation_index);
}
return {
PathElement::Hardened(FEATURE_PURPOSE),
PathElement::Hardened(coin_type),
PathElement::Hardened(FEATURE_IDENTITIES),
PathElement::Hardened(subfeature),
index,
};
}
},
request);
}

Path FriendshipPath(uint32_t coin_type, uint32_t account, Span<const uint8_t> user_a_id, Span<const uint8_t> user_b_id)
{
// dashj FriendKeyChain.getContactPath():
// m/9'/coin'/15'/account'/identity_a/identity_b with two 256-bit 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<uint8_t, 32> 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<const uint8_t> 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<uint32_t>(&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<std::array<uint8_t, 32>>(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)
{
// CPubKey::Derive() asserts a valid compressed parent; contact xpubs are
// externally supplied, so reject them here instead. A syntactically
// compressed but invalid curve point is caught by pubkey parsing inside.
if (element.hardened || !parent.pubkey.IsCompressed()) return false;
if (const auto* index32 = std::get_if<uint32_t>(&element.index)) {
if (*index32 >> 31) return false;
return parent.pubkey.Derive(out.pubkey, out.chaincode, *index32, parent.chaincode);
Comment thread
PastaPastaPasta marked this conversation as resolved.
Comment thread
PastaPastaPasta marked this conversation as resolved.
}
const auto& index256 = std::get<std::array<uint8_t, 32>>(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;
}

} // namespace wallet::platformkeys

namespace wallet {

bool DeriveFriendshipPaymentDestination(const FriendshipXpub& xpub, uint32_t index, CTxDestination& destination_out)
{
platformkeys::ExtPubKey256 child;
if (!platformkeys::DerivePubKey({xpub.pubkey, xpub.chaincode}, platformkeys::PathElement::Normal(index), child)) {
return false;
}
destination_out = PKHash{child.pubkey};
return true;
}

} // namespace wallet
Loading
Loading