Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53ca65b
fix(platform-wallet): scan DashPay contact accounts from request height
lklimek Sep 23, 2026
c2b8c7f
fix(platform-wallet): scope DashPay contact scan checkpoint to the ac…
lklimek Sep 23, 2026
01cc3c1
fix(swift-example-app): fund every Core send from one pooled source set
lklimek Sep 23, 2026
6189113
chore(swift-sdk): drop out-of-scope Swift and CI keychain changes
lklimek Sep 23, 2026
eb37d58
refactor(platform-wallet): keep accountReference version shift local
lklimek Sep 23, 2026
a45ce65
refactor(platform-encryption): expose the accountReference version shift
lklimek Sep 23, 2026
01f88df
refactor(platform-encryption): add secret-free account_reference_vers…
lklimek Sep 23, 2026
6dccd39
docs(platform-encryption): restore ASK28 rustdoc and name the account…
lklimek Sep 23, 2026
66187ba
fix(platform-wallet): stop contact-driven rescans and bound receiving…
lklimek Sep 23, 2026
e56d946
fix(platform-wallet): defer receiving rescans until a sent sweep comp…
lklimek Sep 23, 2026
e256607
refactor(platform-wallet): tighten sent-height API and cover multi-ow…
lklimek Sep 23, 2026
6146c43
refactor(platform-wallet): share the sent-sweep pipeline with tests a…
lklimek Sep 23, 2026
504a717
docs(platform-wallet): link the in-flight scan race tracking issue
lklimek Sep 23, 2026
f9426a4
test(platform-wallet): complete the sent sweep before asserting the r…
lklimek Sep 24, 2026
2ae4d4d
Merge branch 'v5.1-dev' into fix/platform-wallet-contact-scan-height
lklimek Oct 12, 2026
67f7c3b
fix(platform-wallet): integrate contact rescans with v5.1-dev
lklimek Oct 19, 2026
9a18844
fix(platform-wallet): restore receiving accounts for sent-only contacts
lklimek Oct 20, 2026
107b351
fix(platform-wallet): recover missing established receiving accounts
lklimek Oct 20, 2026
6d293f2
Merge branch 'v5.1-dev' into fix/platform-wallet-contact-scan-height
lklimek Oct 22, 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
33 changes: 24 additions & 9 deletions packages/rs-platform-encryption/src/account_reference.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
//! DIP-15 `accountReference` (masked account index).

/// Bit position of the rotation `version` in a masked `accountReference`:
/// the top 4 bits carry the version, the low 28 bits the masked account index.
pub const ACCOUNT_REFERENCE_VERSION_SHIFT: u32 = 28;

/// Mask selecting the low 28 bits (the masked account index) of an
/// `accountReference`; the complement of the version bits.
const ACCOUNT_INDEX_MASK: u32 = (1 << ACCOUNT_REFERENCE_VERSION_SHIFT) - 1;

/// Rotation `version` of a masked `accountReference`. Unlike
/// [`unmask_account_reference`] it needs no secret: the version bits are not
/// masked, so any party can tell whether a request is a re-key (`version > 0`).
pub const fn account_reference_version(account_reference: u32) -> u32 {
account_reference >> ACCOUNT_REFERENCE_VERSION_SHIFT
}

/// `ASK28 = (HMAC-SHA256(sender_secret_key, compact_xpub))[28..32] big-endian >> 4`.
///
/// HMAC input is the 69-byte DIP-15 compact form (the `encryptedPublicKey`
Expand Down Expand Up @@ -45,8 +60,8 @@ pub fn calculate_account_reference(
version: u32,
) -> u32 {
let ask28 = account_secret_key_28(sender_secret_key, compact_xpub);
let shortened_account_bits = account_index & 0x0FFF_FFFF;
let version_bits = version << 28;
let shortened_account_bits = account_index & ACCOUNT_INDEX_MASK;
let version_bits = version << ACCOUNT_REFERENCE_VERSION_SHIFT;
version_bits | (ask28 ^ shortened_account_bits)
}

Expand All @@ -60,8 +75,8 @@ pub fn unmask_account_reference(
compact_xpub: &[u8],
) -> (u32, u32) {
let ask28 = account_secret_key_28(sender_secret_key, compact_xpub);
let version = account_reference >> 28;
let account_index = (account_reference & 0x0FFF_FFFF) ^ ask28;
let version = account_reference_version(account_reference);
let account_index = (account_reference & ACCOUNT_INDEX_MASK) ^ ask28;
(version, account_index)
}

Expand All @@ -81,15 +96,15 @@ mod tests {
let secret_key = [1u8; 32];
let compact = test_compact_xpub();
assert_eq!(
calculate_account_reference(&secret_key, &compact, 0, 0) >> 28,
account_reference_version(calculate_account_reference(&secret_key, &compact, 0, 0)),
0
);
assert_eq!(
calculate_account_reference(&secret_key, &compact, 0, 1) >> 28,
account_reference_version(calculate_account_reference(&secret_key, &compact, 0, 1)),
1
);
assert_eq!(
calculate_account_reference(&secret_key, &compact, 0, 15) >> 28,
account_reference_version(calculate_account_reference(&secret_key, &compact, 0, 15)),
15
);
}
Expand Down Expand Up @@ -123,13 +138,13 @@ mod tests {

let reference = calculate_account_reference(&secret_key, &compact, 0, 0);
assert_eq!(
reference & 0x0FFF_FFFF,
reference & ACCOUNT_INDEX_MASK,
expected_ask28,
"ASK28 must be digest bytes [28..32] big-endian >> 4 (iOS dash-shared-core)"
);
let old_ask28 = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]) >> 4;
assert_ne!(
reference & 0x0FFF_FFFF,
reference & ACCOUNT_INDEX_MASK,
old_ask28,
"head-of-digest extraction is the old bug"
);
Expand Down
5 changes: 4 additions & 1 deletion packages/rs-platform-encryption/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ mod ecdh;
mod error;

pub use account_label::{decrypt_account_label, encrypt_account_label};
pub use account_reference::{calculate_account_reference, unmask_account_reference};
pub use account_reference::{
account_reference_version, calculate_account_reference, unmask_account_reference,
ACCOUNT_REFERENCE_VERSION_SHIFT,
};
pub use aes::{decrypt_aes_256_cbc, encrypt_aes_256_cbc};
pub use compact_xpub::{
compact_xpub_bytes, decrypt_extended_public_key, encrypt_extended_public_key,
Expand Down
4 changes: 2 additions & 2 deletions packages/rs-platform-wallet/src/changeset/dashpay_backfill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ use super::{PersistenceCapabilities, PlatformWalletPersistence};
/// was recorded — the earliest height whose block the scanner is guaranteed to
/// test against this contact's addresses. It is stored per contact rather than
/// as one wallet-wide floor because a contact's checkpoint can *drop* after it
/// was covered (the reciprocal request turns out to be older than ours, or a
/// rotation falls back to wallet birth): a wallet-wide floor would then read
/// was covered (an older outgoing publication becomes known, or a rotation
/// falls back to wallet birth): a wallet-wide floor would then read
/// as "still covered" for blocks the scan never tested with that contact.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
6 changes: 3 additions & 3 deletions packages/rs-platform-wallet/src/manager/dashpay_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,9 @@ impl DashPaySyncManager {
// Local-only: DIP-15 §12.6 coreHeight backfill — lower SPV synced_height
// to re-scan for incoming payments that landed on a contact's receival
// address before it was watched (restore-from-seed / 2nd device /
// offline-accept→pay). After the reconcile above so newly established
// receival accounts are visible; a per-contact guard prevents
// re-triggering and thrashing the in-flight backfill.
// offline-accept→pay). After the reconcile above so newly registered
// receival accounts (sent or established) are visible; a per-contact
// guard prevents re-triggering and thrashing the in-flight backfill.
if let Err(e) = identity.dashpay().reconcile_dashpay_rescan().await {
tracing::warn!(
wallet_id = %hex::encode(wallet_id),
Expand Down
Loading
Loading