Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
16afd79
fix(platform-wallet): typed persister errors with bounded transient r…
lklimek Sep 2, 2026
f93aa3f
feat(platform-wallet-ffi): distinguishable persister result codes 49/50
lklimek Sep 2, 2026
5adbe62
fix(platform-wallet): invalidate filter-scan generation on contact ac…
lklimek Sep 2, 2026
881dbd0
fix(platform-wallet): drop the local SPV timeout, fix Swift funding i…
lklimek Sep 2, 2026
40e04c2
refactor(platform-wallet-ffi): reuse the shared provider-account rebuild
lklimek Sep 2, 2026
d320b35
Merge remote-tracking branch 'origin/v4.2-dev' into feat/platform-wal…
lklimek Sep 10, 2026
1363aa0
fix(platform-wallet): drop an unreachable read-failure arm left by th…
lklimek Sep 10, 2026
2b12cfb
refactor(platform-wallet): share provider key account rebuild between…
lklimek Sep 10, 2026
6e168f7
Merge branch 'v4.2-dev' into feat/platform-wallet-ffi-persister-codes…
lklimek Sep 11, 2026
a9dbddb
fix(platform-wallet): declare bls/eddsa explicitly on the provider_ke…
lklimek Sep 11, 2026
f1250ab
refactor(platform-wallet-storage): share provider_key_test_wallet wit…
lklimek Sep 11, 2026
0b08878
Merge remote-tracking branch 'origin/v4.2-dev' into feat/platform-wal…
lklimek Sep 14, 2026
e1c6c89
refactor(platform-wallet): separate behavior fixes from shared rebuil…
lklimek Sep 14, 2026
df13421
fix(platform-wallet): isolate contact registration and wallet behavio…
lklimek Sep 14, 2026
19ac6fa
fix(platform-wallet): scan contact accounts from request height
lklimek Sep 14, 2026
9e03d41
Merge branch 'v4.2-dev' into fix/platform-wallet-contact-scan-height
lklimek Sep 15, 2026
239acb0
Merge branch 'v4.2-dev' into fix/platform-wallet-contact-scan-height
lklimek Sep 16, 2026
6e78eb9
fix(swift-sdk): align Core funding checks and reject keychain lookup …
lklimek Sep 16, 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
110 changes: 87 additions & 23 deletions packages/rs-platform-wallet/src/wallet/identity/network/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use dpp::identity::Identity;
use dpp::prelude::Identifier;
use key_wallet::account::AccountType;
use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait;
use key_wallet::wallet::managed_wallet_info::managed_account_operations::ManagedAccountOperations;
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;

use super::*;
use crate::broadcaster::TransactionBroadcaster;
Expand All @@ -14,6 +16,60 @@ use crate::wallet::identity::types::dashpay::established_contact::EstablishedCon
use crate::wallet::identity::types::dashpay::payment::DashpayAddressMatch;
use crate::wallet::platform_wallet::PlatformWalletInfo;

/// Return the last certified Core height to keep when adding a contact account.
/// DIP-15 records height `H` so recovery resumes at `H + 1`; locally rotated
/// relationships fall back to wallet birth because their original `H` is gone.
pub(super) fn contact_scan_checkpoint(
info: &crate::wallet::PlatformWalletInfo,
owner: &Identifier,
contact: &Identifier,
) -> u32 {
let birth_checkpoint = info.core_wallet.birth_height().saturating_sub(1);
let Some(managed) = info.identity_manager.managed_identity(owner) else {
return birth_checkpoint;
};
let dashpay = managed.dashpay();

let mut requests = Vec::with_capacity(2);
if let Some(established) = dashpay.established_contacts().get(contact) {
requests.push(&established.outgoing_request);
requests.push(&established.incoming_request);
}
requests.extend(dashpay.sent_contact_requests().get(contact));
requests.extend(dashpay.incoming_contact_requests().get(contact));
let request_checkpoint = (!requests.is_empty()
&& requests
.iter()
.all(|request| request.account_reference >> 28 == 0))
.then(|| {
Comment on lines +40 to +44

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Name the DIP-15 rotation-bit shift instead of inlining >> 28

The request.account_reference >> 28 == 0 test re-derives the DIP-15 layout (low 28 bits masked index, top 4 bits rotation version) whose canonical owner is rs-platform-encryption (calculate_account_reference/unmask_account_reference in account_reference.rs). The same literal shape appears as 1 << 28 in the payments.rs tests, so a future layout change must be found by text search across three sites. Behavior is correct today and the fallback (rewind to wallet birth) is the safe direction, so this is maintainability only: a shared named constant or a secret-free version-bit accessor in the owning crate would keep the single source of truth.

source: muse-spark-1.3-contributor (phase2-reviewer: general, architecture-layering, rust-quality)

requests
.iter()
.map(|request| request.core_height_created_at)
.min()
.unwrap_or(0)
});

request_checkpoint
.unwrap_or(birth_checkpoint)
.max(birth_checkpoint)
}
Comment on lines +40 to +55

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Validate the contact-request height before using it as a scan checkpoint

The checkpoint uses the minimum core_height_created_at from contact requests whenever the account-reference check passes, but it does not validate that the height is a plausible locally known Core height. A malicious or malformed contact request can claim an arbitrarily high height. When the wallet has already scanned beyond that value, previous_checkpoint.min(scan_checkpoint) preserves the already-advanced checkpoint, so the wallet does not revisit blocks containing payments to the newly registered contact account. Treat invalid or unknown heights as unavailable and fall back to the wallet birth boundary, or clamp accepted heights to a locally known Core tip before updating the checkpoint.

source: gpt-6-astra (phase2-reviewer: general, architecture-layering, rust-quality, security-auditor)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved (re-reviewed at 6e78eb9f): Fixed — the checkpoint now clamps to the wallet-birth floor, falls back to birth for rotated or missing heights, and both call sites can only rewind the scan, so untrusted heights can no longer push it forward or earlier than birth.


fn add_managed_contact_account(
info: &mut crate::wallet::PlatformWalletInfo,
wallet: &key_wallet::Wallet,
account_type: AccountType,
scan_checkpoint: u32,
) -> key_wallet::Result<()> {
let previous_checkpoint = info.core_wallet.synced_height();
// Upstream adds the account, bumps the scanner generation, and rewinds to
// wallet birth. Under this same manager write lock, restore only the range
// certified for the new account while preserving any deeper pending scan.
info.add_managed_account(wallet, account_type)?;
info.core_wallet
.update_synced_height(previous_checkpoint.min(scan_checkpoint));
Ok(())
}

/// Build the persistence round for a newly registered DashPay account
/// (`DashpayReceivingFunds` / `DashpayExternalAccount`): the
/// [`AccountRegistrationEntry`] plus the account's initial address-pool
Expand Down Expand Up @@ -203,9 +259,9 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
is_watch_only: false,
};

// DashPay accounts are funds-bearing; use the typed
// `insert_funds_bearing_account` API exposed by the post-split
// collection rather than wrapping in `OwnedManagedCoreAccount`.
// Build the initial funds-bearing state for persistence. The live
// insertion below goes through `ManagedAccountOperations` so upstream
// also invalidates the wallet's prior filter-scan generation.
let managed = key_wallet::managed_account::ManagedCoreFundsAccount::from_account(&account);

// Persist the registration BEFORE the in-memory inserts: a store
Expand All @@ -227,6 +283,7 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
let (wallet, info) = wm
.get_wallet_mut_and_info_mut(&self.wallet_id)
.ok_or_else(|| PlatformWalletError::WalletNotFound(hex::encode(self.wallet_id)))?;
let scan_checkpoint = contact_scan_checkpoint(info, our_identity_id, contact_identity_id);

// Mirror the restored shape: the immutable `wallet.accounts`
// collection holds the Account (like `build_wallet_start_state`
Expand All @@ -239,14 +296,22 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
"Failed to add contact account to wallet: {e}"
))
})?;
info.core_wallet
.accounts
.insert_funds_bearing_account(managed)
.map_err(|e| {
PlatformWalletError::InvalidIdentityData(format!(
"Failed to register contact account: {e}"
))
})?;
add_managed_contact_account(info, wallet, account_type, scan_checkpoint).map_err(|e| {
PlatformWalletError::InvalidIdentityData(format!(
"Failed to register contact account: {e}"
))
})?;
if let Some(managed) = info.identity_manager.managed_identity_mut(our_identity_id) {
if managed
.dashpay()
.established_contacts()
.contains_key(contact_identity_id)
{
managed
.dashpay_rescan_triggered_mut()
.insert(*contact_identity_id);
}
}

tracing::info!(
our_identity = %our_identity_id,
Expand Down Expand Up @@ -534,8 +599,9 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
is_watch_only: true,
};

// DashpayExternalAccount is funds-bearing; insert via the
// typed `insert_funds` API after the upstream split.
// Build the initial funds-bearing state for persistence. The live
// insertion below goes through `ManagedAccountOperations` so upstream
// also invalidates the wallet's prior filter-scan generation.
let managed = key_wallet::managed_account::ManagedCoreFundsAccount::from_account(&account);

// Persist the registration BEFORE the in-memory inserts (same
Expand All @@ -562,6 +628,7 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
self.wallet_id,
)))
})?;
let scan_checkpoint = contact_scan_checkpoint(info, our_identity_id, &contact_identity_id);

// (a) Insert Account into the immutable wallet account collection so the
// xpub is accessible by `send_payment`.
Expand All @@ -574,16 +641,13 @@ impl<B: TransactionBroadcaster + ?Sized> DashPayView<'_, B> {
)))
})?;

// (b) Insert ManagedCoreFundsAccount for address-pool tracking.
info.core_wallet
.accounts
.insert_funds_bearing_account(managed)
.map_err(|e| {
Transient(PlatformWalletError::InvalidIdentityData(format!(
"Failed to register external contact account: {}",
e
)))
})?;
// (b) Insert the managed account and invalidate prior filter coverage.
add_managed_contact_account(info, wallet, account_type, scan_checkpoint).map_err(|e| {
Transient(PlatformWalletError::InvalidIdentityData(format!(
"Failed to register external contact account: {}",
e
)))
})?;

tracing::info!(
our_identity = %our_identity_id,
Expand Down
Loading
Loading