Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1791fe5
fix(platform-wallet): accept legacy dashj key purposes on inbound con…
jeanpierreroma Aug 11, 2026
982f107
perf(platform-wallet): keep the contact fetch off the drain's repeati…
jeanpierreroma Aug 11, 2026
289b8fb
refactor(platform-wallet): source the recipient-key cohort from the s…
jeanpierreroma Aug 11, 2026
afec36d
fix(platform-wallet): don't charge a legacy-cohort decrypt failure to…
jeanpierreroma Aug 11, 2026
d822704
Merge branch 'fix/dashpay-legacy-key-purpose' into fix/dashpay-drain-…
jeanpierreroma Aug 11, 2026
608dac5
fix(platform-wallet): make the split validation's mixed-failure polic…
jeanpierreroma Aug 11, 2026
38e6c91
fix(platform-wallet): classify sender-side widening as legacy too
jeanpierreroma Aug 11, 2026
b8c2a83
Merge branch 'fix/dashpay-legacy-key-purpose' into fix/dashpay-drain-…
jeanpierreroma Aug 11, 2026
ee984ad
feat(platform-wallet): make a failed legacy external build self-diagn…
jeanpierreroma Aug 11, 2026
94f10e3
Merge branch 'fix/dashpay-legacy-key-purpose' into fix/dashpay-drain-…
jeanpierreroma Aug 11, 2026
abe80c6
perf(platform-wallet): move purpose-mismatch reasons into the drain s…
jeanpierreroma Aug 11, 2026
8415a50
Merge remote-tracking branch 'origin/v4.2-dev' into fix/dashpay-drain…
jeanpierreroma Aug 11, 2026
d9afc99
fix(platform-wallet): state the decrypt diagnostics as likelihoods, n…
jeanpierreroma Aug 11, 2026
fea6e95
fix(platform-wallet): pool the same funding sources for a contact pay…
romchornyi Aug 11, 2026
0eac01e
fix(platform-wallet): only an immutable fault may permanently break a…
jeanpierreroma Aug 11, 2026
52fe0d9
Merge remote-tracking branch 'origin/fix/dashpay-drain-retry-cost' in…
jeanpierreroma Aug 11, 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
1 change: 1 addition & 0 deletions packages/rs-platform-wallet/src/wallet/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pub mod wallet;
pub use balance::WalletBalance;
pub use balance_handler::BalanceUpdateHandler;
pub use generation::WalletGeneration;
pub(crate) use transaction::resolve_source_accounts;
pub use transaction::{SignedCoreTransaction, ASSET_LOCK_FUNDING_SOURCES, SEND_FUNDING_SOURCES};
pub use wallet::CoreWallet;
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ pub const ASSET_LOCK_FUNDING_SOURCES: [AccountTypePreference; 3] = SEND_FUNDING_
/// DashPay source. A set selector matching nothing resolves to an empty list,
/// not an error — a wallet with no contacts still sends from its standard
/// accounts.
fn resolve_source_accounts(
pub(crate) fn resolve_source_accounts(
accounts: &key_wallet::account::ManagedAccountCollection,
preference: AccountTypePreference,
source_index: u32,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,28 @@ pub fn validate_contact_request(
sender_key_index: u32,
recipient_identity: &Identity,
recipient_key_index: u32,
) -> ContactRequestValidation {
let mut validation = validate_sender_key(sender_identity, sender_key_index);
validation.merge(validate_recipient_key(
recipient_identity,
recipient_key_index,
));
validation
}

/// The sender half of [`validate_contact_request`] — the checks that need the
/// **counterparty's** identity.
///
/// Crate-private: external callers go through the complete
/// [`validate_contact_request`] contract. Split out so the deferred-crypto
/// drain can run the recipient half first — see [`validate_recipient_key`] for
/// why that ordering matters, and what it changes for a mixed failure.
pub(crate) fn validate_sender_key(
sender_identity: &Identity,
sender_key_index: u32,
) -> ContactRequestValidation {
let mut validation = ContactRequestValidation::new();

// -----------------------------------------------------------------------
// Sender key validation
// -----------------------------------------------------------------------
match sender_identity.get_public_key_by_id(sender_key_index) {
Some(key) => {
// Must be ECDSA_SECP256K1 for ECDH.
Expand Down Expand Up @@ -213,9 +229,44 @@ pub fn validate_contact_request(
}
}

// -----------------------------------------------------------------------
// Recipient key validation
// -----------------------------------------------------------------------
validation
}

/// The recipient half of [`validate_contact_request`] — the checks that need
/// only **our own** identity, which is always already resident.
///
/// Split out because the deferred-crypto drain would otherwise pay a Platform
/// round trip (`Identity::fetch` of the contact) before it could discover that
/// the request is unusable for a reason it could have known locally. A
/// purpose-rejected entry stays queued by design — the policy, not the
/// immutable document, is what might change — so that fetch was repeating on
/// every sweep, forever. Mainnet logs from one wallet show 27 contacts and 396
/// such fetch-then-reject cycles in a single session. Running this half first
/// costs nothing and removes the network entirely from that loop.
///
/// # What this changes for a MIXED failure
///
/// Deciding on this half alone is a real policy change, not just a reordering.
/// When our key is purpose-rejected AND the sender's key carries a hard fault
/// (missing / disabled / wrong type), the composed [`validate_contact_request`]
/// would merge both, see `hard_error`, and mark the channel permanently
/// broken. Stopping here classifies it purpose-only and leaves it queued.
///
/// That is the intended outcome. The `hard_error` precedence exists to stop a
/// genuinely permanent fault from becoming a retry-forever loop — but the
/// forever-loop it guards against was expensive precisely because each retry
/// fetched. With the fetch gone, a purpose-rejected entry costs a map lookup
/// per sweep, while marking the channel broken is unappealable by the user:
/// only a fresh request from the CONTACT clears it. Deferring the broken mark
/// until the fault is one we can see locally trades a cheap retry for an
/// irreversible one. A sender-side hard fault still marks the channel broken
/// the moment our own key stops being the blocker.
pub(crate) fn validate_recipient_key(
recipient_identity: &Identity,
recipient_key_index: u32,
) -> ContactRequestValidation {
let mut validation = ContactRequestValidation::new();

match recipient_identity.get_public_key_by_id(recipient_key_index) {
Some(key) => {
// Must be an ECDSA variant for ECDH compatibility.
Expand Down
Loading
Loading