Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 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
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
4 changes: 4 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/platformseed.h \
wallet/receive.h \
wallet/rpc/util.h \
wallet/rpc/wallet.h \
Expand Down Expand Up @@ -709,6 +711,8 @@ libbitcoin_wallet_a_SOURCES = \
wallet/hdchain.cpp \
wallet/interfaces.cpp \
wallet/load.cpp \
wallet/platformkeys.cpp \
wallet/platformseed.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
62 changes: 62 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
#include <util/result.h>
#include <util/ui_change_type.h>

#include <array>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <psbt.h>
#include <set>
#include <string>
Expand Down Expand Up @@ -142,6 +144,58 @@ class Wallet
//! Sign special transaction payload
virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector<unsigned char>& 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'/<account>'/<index>'
RegistrationFunding, //!< m/9'/coin'/5'/1'/<index>'
TopupFunding, //!< m/9'/coin'/5'/2'/<index>'
InvitationFunding, //!< m/9'/coin'/5'/3'/<index>'
};

//! 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<unsigned char>& 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'/<account>'/<user_a>/<user_b>. 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, 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<std::array<uint8_t, 8>> getPlatformSeedId() = 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 +224,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
150 changes: 150 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
#include <interfaces/wallet.h>

#include <chain.h>
#include <chainparams.h>
#include <coinjoin/client.h>
#include <consensus/amount.h>
#include <interfaces/chain.h>
#include <interfaces/coinjoin.h>
#include <interfaces/handler.h>
#include <key_io.h>
#include <policy/fees.h>
#include <primitives/transaction.h>
#include <rpc/server.h>
#include <script/descriptor.h>
#include <script/standard.h>
#include <support/allocators/secure.h>
#include <sync.h>
Expand All @@ -32,7 +35,10 @@
#include <wallet/spend.h>
#include <wallet/wallet.h>
#include <wallet/hdchain.h>
#include <wallet/platformkeys.h>
#include <wallet/platformseed.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 +245,140 @@ class WalletImpl : public Wallet
{
return m_wallet->SignSpecialTxPayload(hash, keyid, vchSig);
}
//! Fetch the BIP39 seed backing this wallet's HD chain, with a
//! deterministic choice among multi-seed descriptor wallets (see
//! wallet/platformseed.h).
bool getPlatformSeed(SecureVector& seed_out)
{
return platformkeys::GetPlatformSeed(*m_wallet, seed_out);
}
std::optional<std::array<uint8_t, 8>> getPlatformSeedId() override
{
SecureVector seed;
if (!getPlatformSeed(seed)) return std::nullopt;
return platformkeys::SeedFingerprint(seed);
}
bool derivePlatformKey(PlatformKeyType type, uint32_t account, uint32_t index, platformkeys::ExtKey256& out)
{
SecureVector seed;
if (!getPlatformSeed(seed)) return false;
const auto coin_type{static_cast<uint32_t>(Params().ExtCoinType())};
platformkeys::Path path;
switch (type) {
Comment thread
PastaPastaPasta marked this conversation as resolved.
Outdated
case PlatformKeyType::IdentityAuth:
path = platformkeys::IdentityAuthKeyPath(coin_type, account, index);
break;
case PlatformKeyType::RegistrationFunding:
path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_REGISTRATION_FUNDING, index);
break;
case PlatformKeyType::TopupFunding:
path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_TOPUP_FUNDING, index);
break;
case PlatformKeyType::InvitationFunding:
path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_INVITATION_FUNDING, index);
break;
}
// A value outside the enum leaves the path empty; deriving would
// return the BIP32 master key rather than a Platform key.
if (path.empty()) return false;
return platformkeys::DeriveExtKey(seed, path, out);
}
bool getPlatformPubKey(PlatformKeyType type, uint32_t account, uint32_t index, CPubKey& pubkey_out) override
{
platformkeys::ExtKey256 ext_key;
if (!derivePlatformKey(type, account, index, ext_key)) return false;
pubkey_out = ext_key.key.GetPubKey();
return true;
}
bool signPlatformDigest(PlatformKeyType type, uint32_t account, uint32_t index, const uint256& digest, std::vector<unsigned char>& vchSig) override
{
platformkeys::ExtKey256 ext_key;
if (!derivePlatformKey(type, account, index, ext_key)) return false;
return ext_key.key.SignCompact(digest, vchSig);
}
bool platformECDHSecret(uint32_t identity_index, uint32_t key_index, const CPubKey& counterparty, SecureVector& secret_out) override
{
platformkeys::ExtKey256 ext_key;
if (!derivePlatformKey(PlatformKeyType::IdentityAuth, identity_index, key_index, ext_key)) return false;
return platformkeys::ComputeECDHSecret(ext_key.key, counterparty, secret_out);
}
bool getFriendshipXpub(uint32_t account, const uint256& user_a_id, const uint256& user_b_id, CPubKey& pubkey_out, uint256& chaincode_out) override
{
SecureVector seed;
if (!getPlatformSeed(seed)) return false;
const auto path = platformkeys::FriendshipPath(Params().ExtCoinType(), account,
Span{user_a_id.begin(), uint256::size()},
Span{user_b_id.begin(), uint256::size()});
Comment thread
PastaPastaPasta marked this conversation as resolved.
Outdated
platformkeys::ExtKey256 ext_key;
if (!platformkeys::DeriveExtKey(seed, path, ext_key)) return false;
pubkey_out = ext_key.key.GetPubKey();
chaincode_out = ext_key.chaincode;
return true;
}
bool importFriendshipKeychains(uint32_t account, const uint256& my_id, const uint256& their_id,
int64_t creation_time, std::string& error) override
{
SecureVector seed;
if (!getPlatformSeed(seed)) { error = "wallet must be unlocked"; return false; }
const auto path = platformkeys::FriendshipPath(Params().ExtCoinType(), account,
Span{my_id.begin(), uint256::size()}, Span{their_id.begin(), uint256::size()});
platformkeys::ExtKey256 own;
if (!platformkeys::DeriveExtKey(seed, path, own)) { error = "could not derive receiving friendship key"; return false; }

CExtKey xprv{};
xprv.chaincode = own.chaincode;
xprv.key = own.key;

// Only our private receiving chain becomes a wallet descriptor. The
// contact's receiving chain must NOT be imported: its scriptPubKeys
// would become IsMine, so payments to the contact would be classified
// as payments-to-self and their outputs treated as our own. Payment
// destinations for the contact are derived statelessly from their
// xpub (kept in the wallet's platform data records) via
// getFriendshipPaymentDestination.
FlatSigningProvider provider;
auto parsed = Parse("pkh(" + EncodeExtKey(xprv) + "/*)", provider, error,
/*require_checksum=*/false);
if (!parsed) return false;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (creation_time < 0) creation_time = 0;
WalletDescriptor wallet_descriptor(std::move(parsed), /*creation_time=*/creation_time,
/*range_start=*/0, /*range_end=*/1000,
/*next_index=*/0);
Comment thread
PastaPastaPasta marked this conversation as resolved.
Outdated

LOCK(m_wallet->cs_wallet);
if (!m_wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
error = "DashPay contact payments require a descriptor wallet";
return false;
}
// A friendship that is already imported matches its existing spk_man
// and is updated in place (AddWalletDescriptor). The update must keep
// the existing range, derivation progress and earliest birth time:
// TopUp() may have grown the range past the initial one (a shrinking
// update throws in CanUpdateToWalletDescriptor), and a later creation
// time could exclude old history from rescans.
if (auto* existing = m_wallet->GetDescriptorScriptPubKeyMan(wallet_descriptor)) {
LOCK(existing->cs_desc_man);
const WalletDescriptor current{existing->GetWalletDescriptor()};
wallet_descriptor.range_start = current.range_start;
wallet_descriptor.range_end = std::max(wallet_descriptor.range_end, current.range_end);
wallet_descriptor.next_index = current.next_index;
wallet_descriptor.creation_time = std::min(wallet_descriptor.creation_time, current.creation_time);
}
if (!m_wallet->AddWalletDescriptor(wallet_descriptor, provider, /*label=*/"", /*internal=*/false)) {
if (error.empty()) error = "could not import receiving friendship descriptor";
return false;
}
return true;
}
bool getFriendshipPaymentDestination(const CPubKey& their_pubkey, const uint256& their_chaincode,
uint32_t index, CTxDestination& destination_out) override
{
platformkeys::ExtPubKey256 parent{their_pubkey, their_chaincode};
platformkeys::ExtPubKey256 child;
if (!platformkeys::DerivePubKey(parent, platformkeys::PathElement::Normal(index), child)) return false;
destination_out = PKHash{child.pubkey};
return true;
}
bool isSpendable(const CScript& script) override
{
LOCK(m_wallet->cs_wallet);
Expand Down Expand Up @@ -316,6 +456,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
Loading
Loading