forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(wallet): add Platform key provider, data records and DIP-15 friendship keychain seams #7581
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
Merged
PastaPastaPasta
merged 21 commits into
dashpay:develop
from
PastaPastaPasta:dashpay/wallet-seams
Aug 19, 2026
Merged
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 c6fe429
feat(wallet): add generic per-wallet Platform data records
PastaPastaPasta 01c8a8a
feat(wallet): add DIP-15 friendship keychain import and platform key …
PastaPastaPasta a67dbc2
fix(wallet): validate parent pubkey before DIP-14 public derivation
PastaPastaPasta d583a42
fix(wallet): fail platform seed selection when the pinned seed is una…
PastaPastaPasta 2cd5bcd
fix(wallet): preserve friendship descriptor state on re-import
PastaPastaPasta 005459f
fix(wallet): drop platform data from memory only after the database e…
PastaPastaPasta 31637d7
fix(wallet): fail platform seed selection on a malformed seed pin
PastaPastaPasta 3a430b5
fix(wallet): require a full unlock before serving the legacy platform…
PastaPastaPasta 47c0751
test(wallet): pin friendship derivation against rust-dashcore key-wallet
PastaPastaPasta b78f3cf
chore(wallet): annotate m_platform_data locking, add missing includes
PastaPastaPasta 80a46ec
fix(wallet): skip empty-mnemonic descriptors in platform seed selection
PastaPastaPasta 3f954b4
fix(wallet): reject unknown platform key types before derivation
PastaPastaPasta b1da3d4
fix(wallet): treat corrupt platform data records as wallet corruption
PastaPastaPasta d5e41f4
refactor(wallet): drop the unused friendship import label parameter
PastaPastaPasta d58facb
fix(wallet): keep damaged Platform cache records noncritical on load
PastaPastaPasta 6a92a1c
fix(wallet): validate mnemonics before deriving the platform seed
PastaPastaPasta 449da14
wallet: make the Platform seed provider descriptor-wallet-only
PastaPastaPasta 81ba952
refactor(wallet): confine Platform derivation to key managers
PastaPastaPasta 939ee20
refactor(wallet): avoid serializing friendship xprv
PastaPastaPasta b2a3c40
fix(wallet): derive Platform keys from descriptor root
PastaPastaPasta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.