Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 8 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,14 @@ members = [
]

[workspace.dependencies]
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "393b612269c158925451235a5d9c0ffa5e2eeed2" }

dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" }
tokio-metrics = "0.5"

# Size-tuned profile for the iOS `rs-unified-sdk-ffi` staticlib, which
Expand Down
25 changes: 6 additions & 19 deletions packages/rs-platform-wallet-ffi/src/derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,19 @@ use std::str::FromStr;

use dashcore::secp256k1::Secp256k1;
use key_wallet::bip32::{DerivationPath, ExtendedPrivKey};
use key_wallet::mnemonic::{Language, Mnemonic};
use key_wallet::mnemonic::Mnemonic;
use zeroize::Zeroizing;

use crate::error::*;
use crate::types::{FFINetwork, Network};
use crate::{check_ptr, unwrap_result_or_return};

fn parse_mnemonic_any_language(phrase: &str) -> Result<Mnemonic, &'static str> {
const LANGUAGES: [Language; 10] = [
Language::English,
Language::Spanish,
Language::French,
Language::Italian,
Language::Japanese,
Language::Korean,
Language::ChineseSimplified,
Language::ChineseTraditional,
Language::Czech,
Language::Portuguese,
];
for lang in LANGUAGES {
if let Ok(m) = Mnemonic::from_phrase(phrase, lang) {
return Ok(m);
}
}
Err("phrase does not match any supported BIP-39 wordlist")
// Upstream's `from_phrase` IS the auto-detecting parse since
// rust-dashcore#981 — one path, English diagnostics preserved when
// nothing matches. This wrapper survives only to narrow the error to
// the `&'static str` its callers report.
Mnemonic::from_phrase(phrase).map_err(|_| "phrase does not match any supported BIP-39 wordlist")
}

/// Derive a 32-byte ECDSA private key at a BIP-32 derivation path from
Expand Down
25 changes: 6 additions & 19 deletions packages/rs-platform-wallet-ffi/src/identity_keys_from_mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use key_wallet::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPu
use key_wallet::dip9::{
IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET,
};
use key_wallet::mnemonic::{Language, Mnemonic};
use key_wallet::mnemonic::Mnemonic;
use zeroize::Zeroizing;

use crate::error::*;
Expand Down Expand Up @@ -55,24 +55,11 @@ pub(crate) unsafe fn zeroize_and_free_row(row: &mut IdentityKeyPreviewFFI) {

/// Parse a BIP-39 mnemonic against every supported wordlist.
pub(crate) fn parse_mnemonic_any_language(phrase: &str) -> Result<Mnemonic, &'static str> {
const LANGUAGES: [Language; 10] = [
Language::English,
Language::Spanish,
Language::French,
Language::Italian,
Language::Japanese,
Language::Korean,
Language::ChineseSimplified,
Language::ChineseTraditional,
Language::Czech,
Language::Portuguese,
];
for lang in LANGUAGES {
if let Ok(m) = Mnemonic::from_phrase(phrase, lang) {
return Ok(m);
}
}
Err("phrase does not match any supported BIP-39 wordlist")
// Upstream's `from_phrase` IS the auto-detecting parse since
// rust-dashcore#981 — one path, English diagnostics preserved when
// nothing matches. This wrapper survives only to narrow the error to
// the `&'static str` its callers report.
Mnemonic::from_phrase(phrase).map_err(|_| "phrase does not match any supported BIP-39 wordlist")
}

/// Resolve a wallet's BIP-39 mnemonic via a Swift-owned
Expand Down
6 changes: 1 addition & 5 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7774,7 +7774,7 @@ mod tests {
use key_wallet::account::{Account, AccountType, StandardAccountType};
use key_wallet::bip32::{ExtendedPrivKey, ExtendedPubKey};
use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait;
use key_wallet::mnemonic::{Language, Mnemonic};
use key_wallet::mnemonic::Mnemonic;
use key_wallet::wallet::Wallet;

/// Regression: restored pool addresses must be tagged with the
Expand Down Expand Up @@ -7923,7 +7923,6 @@ mod tests {
// `account_collection_test.rs` uses.
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Language::English,
)
.expect("static BIP-39 vector must parse");
let seed = mnemonic.to_seed("");
Expand Down Expand Up @@ -7957,7 +7956,6 @@ mod tests {
fn test_managed_wallet_info_with_account(account_type: AccountType) -> ManagedWalletInfo {
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Language::English,
)
.expect("static BIP-39 vector must parse");
let seed = mnemonic.to_seed("");
Expand Down Expand Up @@ -8073,7 +8071,6 @@ mod tests {
fn test_managed_wallet_info_with_provider_owner() -> ManagedWalletInfo {
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Language::English,
)
.expect("static BIP-39 vector must parse");
let seed = mnemonic.to_seed("");
Expand Down Expand Up @@ -8516,7 +8513,6 @@ mod tests {
fn account_xpub_survives_persist_restore_round_trip() {
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
Language::English,
)
.expect("static BIP-39 vector must parse");
let seed = mnemonic.to_seed("");
Expand Down
100 changes: 83 additions & 17 deletions packages/rs-platform-wallet/src/changeset/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,29 @@ fn context_rank(context: &key_wallet::transaction_checking::TransactionContext)

impl Merge for CoreChangeSet {
fn merge(&mut self, other: Self) {
// A record arriving after a sweep that removed the same transaction
// reinstates it, and every persister writes records before replaying
// sweeps — so without this the sweep would delete a row the wallet
// has since brought back. Reachable through IS-lock precedence: an
// unconfirmed transaction is swept when an IS-locked conflict lands,
// then returns chainlocked and sweeps that conflict in turn.
//
// The release set stays as it is. It is the aggregate for every loser
// in the batch, so dropping it when one of them is reinstated would
// discard coins freed by the losers that are still going. Entries
// belonging to the reinstated transaction are inert on every backend:
// each scopes its release to the remaining losers' own inputs, or
// withholds any outpoint a surviving record claims — and the
// reinstating record is exactly such a claim.
if !other.records.is_empty() && !self.sweeps.is_empty() {
let reinstated: std::collections::HashSet<Txid> =
other.records.iter().map(|record| record.txid).collect();
for batch in &mut self.sweeps {
batch.txids.retain(|txid| !reinstated.contains(txid));
}
self.sweeps.retain(|batch| !batch.txids.is_empty());
}

// Records: coalesce by txid, NEWEST-WINS (dashpay/platform#4387).
//
// The event bridge already folded each event's per-account
Expand Down Expand Up @@ -1338,32 +1361,47 @@ impl Merge for AssetLockChangeSet {
// swift-sdk `persistAssetLocks`), making the store order of
// racing snapshots immaterial.
for (out_point, entry) in other.asset_locks {
if entry.status == AssetLockStatus::Consumed {
// A Consumed write supersedes any earlier-folded
// tombstone for the outpoint — Consumed rows are
// deliberately retained for historical lookup (see the
// variant doc), so the terminal write wins over a stale
// removal exactly as it wins over a stale status.
self.removed.remove(&out_point);
} else if let Some(existing) = self.asset_locks.get(&out_point) {
if existing.status == AssetLockStatus::Consumed {
continue;
if entry.status != AssetLockStatus::Consumed {
if let Some(existing) = self.asset_locks.get(&out_point) {
if existing.status == AssetLockStatus::Consumed {
continue;
}
}
}
// Every ACCEPTED upsert supersedes an earlier-folded tombstone
// for its outpoint, not just a Consumed one. Sweeps are a
// removal producer now (`remove_tracked_asset_locks_for_swept`),
// and a swept funding transaction can return chainlocked in the
// same folded drain — the reinstating record re-inserts the
// entry through reconstruction at a non-Consumed status, and
// letting the sweep's tombstone ride along would have the store
// delete the row it just reinstated (SQLite applies upserts
// before removals) while the in-memory wallet keeps it. This is
// the asset-lock mirror of `CoreChangeSet::merge`'s
// reinstated-txid retraction. For Consumed the same line also
// covers the historical rule: the terminal write wins over a
// stale removal exactly as it wins over a stale status.
self.removed.remove(&out_point);
self.asset_locks.insert(out_point, entry);
}
// Tombstones folded after a Consumed upsert are dropped for the
// same reason. The only removal emitter (`untrack_asset_lock`)
// fires exclusively for Built rows whose broadcast was
// definitively rejected, so a Consumed/removed pair for one
// outpoint has no legitimate producer — this is defense in
// depth matching the upsert guard.
// Tombstones folded after a Consumed upsert are dropped — Consumed
// rows are deliberately retained for historical lookup (see the
// variant doc). Any other pending upsert is dropped WITH the
// tombstone landing: a removal is upstream's newer word for the
// outpoint (a lock tracked and then swept, or a Built row rejected
// at broadcast, inside one fold), and carrying the dead upsert
// alongside the tombstone would make every store's correctness
// depend on applying upserts before removals. Together with the
// retraction above this keeps the invariant every backend relies
// on: a merged changeset never carries both an upsert and a
// tombstone for the same outpoint.
for out_point in other.removed {
let consumed = self
.asset_locks
.get(&out_point)
.is_some_and(|entry| entry.status == AssetLockStatus::Consumed);
if !consumed {
self.asset_locks.remove(&out_point);
self.removed.insert(out_point);
}
}
Expand Down Expand Up @@ -2374,10 +2412,38 @@ mod tests {
folded.asset_locks[&outpoint].status,
AssetLockStatus::Consumed
);
// …and a legitimate removal (rejected Built row) still folds.
// …and a legitimate removal (rejected Built row, or a sweep of the
// funding tx) still folds — taking the now-dead upsert with it, so
// no store ever sees an upsert/tombstone pair whose outcome would
// hinge on which it applies first.
let mut folded = cs_with(AssetLockStatus::Built);
folded.merge(removal());
assert!(folded.removed.contains(&outpoint));
assert!(
!folded.asset_locks.contains_key(&outpoint),
"a tombstone folding in must not leave the dead upsert beside it"
);

// The coalesced sweep-then-chainlocked-reinstatement fold: the
// sweep removes the tracked entry and contributes a tombstone, then
// the reinstating record re-inserts through reconstruction at a
// non-Consumed status — in the SAME drain. The accepted upsert must
// cancel the earlier tombstone (the asset-lock mirror of
// `CoreChangeSet::merge`'s reinstated-txid retraction); otherwise
// SQLite — upserts before removals — deletes the row it just
// reinstated while the in-memory wallet keeps it, and the durable
// tracked lock is gone after restart even though its funding
// transaction survived.
let mut folded = removal();
folded.merge(cs_with(AssetLockStatus::RecoveredFromChain));
assert!(
folded.removed.is_empty(),
"a reinstating reconstruction must cancel the folded sweep tombstone"
);
assert_eq!(
folded.asset_locks[&outpoint].status,
AssetLockStatus::RecoveredFromChain
);
}

#[test]
Expand Down
Loading
Loading