Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.

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

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

tokio-metrics = "0.5"

Expand Down
8 changes: 8 additions & 0 deletions packages/rs-platform-wallet-ffi/src/asset_lock/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ use std::os::raw::c_char;

/// Build an asset lock transaction via an external mnemonic resolver.
///
/// Funding is POOLED across `platform_wallet::ASSET_LOCK_FUNDING_SOURCES`:
/// coin selection draws from the union of the BIP44 and BIP32 accounts at
/// `account_index` plus every DashPay contact-receiving account, and change
/// returns to BIP44. A lock therefore no longer needs its whole amount sitting
/// in one account, and the caller no longer has to sweep accounts together
/// first. `account_index` addresses the standard families only; DashPay
/// accounts span their own indices and are pooled in regardless.
///
/// On success:
/// - `out_tx_bytes`/`out_tx_len`: serialized signed transaction
/// - `out_derivation_path`: NUL-terminated C string with the
Expand Down
4 changes: 3 additions & 1 deletion packages/rs-platform-wallet-ffi/src/asset_lock/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ pub struct TrackedAssetLockFFI {
pub txid: [u8; 32],
/// Outpoint vout.
pub vout: u32,
/// BIP44 account index.
/// Family-independent source index of the pooled funding (BIP44 and
/// BIP32 at this index plus DashPay receiving accounts). Not a
/// BIP44-only selector.
pub account_index: u32,
/// Funding type (0=IdentityRegistration, 1=IdentityTopUp, 2=IdentityTopUpNotBound,
/// 3=IdentityInvitation, 4=AssetLockAddressTopUp, 5=AssetLockShieldedAddressTopUp).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ pub struct AssetLockEntryFFI {
/// for the callback window.
pub transaction_bytes: *const u8,
pub transaction_bytes_len: usize,
/// BIP44 account index that funded this asset lock.
/// Family-independent source index of the pooled funding (BIP44 and
/// BIP32 at this index plus DashPay receiving accounts; change
/// returns to BIP44). Not a BIP44-only selector.
pub account_index: u32,
/// Discriminant of [`key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingType`]:
/// 0 = IdentityRegistration, 1 = IdentityTopUp, 2 = IdentityTopUpNotBound,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ fn existing_asset_lock_funding(

/// Register a new asset-lock-funded identity using an external signer.
///
/// `account_index` selects which BIP44 *standard* account (by BIP44
/// account index) the asset-lock funding UTXOs are drawn from. Only
/// BIP44 standard accounts are supported today; the Swift UI is
/// expected to filter the funding picker accordingly (CoinJoin / BIP32
/// funding for new-identity registration is not yet wired through
/// `create_funded_asset_lock_proof`).
/// `account_index` addresses the *standard* families: the asset-lock
/// funding POOLS the BIP44 and BIP32 accounts at that index together
/// with every DashPay receiving account (change returns to BIP44).
/// The index does not restrict which DashPay receiving accounts
/// contribute, so the UI must not present it as an account-scoped
/// funding or privacy choice. CoinJoin funding remains drain-only and
/// is not reachable here.
///
/// # Safety
/// - `signer_handle` must be a valid, non-destroyed `*mut SignerHandle`
Expand Down
9 changes: 6 additions & 3 deletions packages/rs-platform-wallet-ffi/src/identity_top_up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,12 @@ const MIN_TOP_UP_DUFFS: u64 = 50_500;
/// with [`AssetLockFunding::FromWalletBalance`] — the same L2 orchestrator
/// (funding resolution, IS→CL fallback, asset-lock cleanup) that
/// [`platform_wallet_register_identity_with_funding_signer`] drives for
/// registration. `account_index` selects which BIP44 *standard* account
/// the asset-lock UTXOs are drawn from (only BIP44 standard accounts are
/// supported today, matching registration).
/// registration. `account_index` addresses the *standard* families: the
/// asset lock POOLS the BIP44 and BIP32 accounts at that index together
/// with every DashPay receiving account (change returns to BIP44). The
/// index does not restrict which DashPay receiving accounts contribute —
/// callers must not present this as an account-scoped funding or privacy
/// choice (matching registration).
///
/// Unlike registration this takes NO identity-key signer: the
/// `IdentityTopUp` state-transition is signed entirely by the asset lock's
Expand Down
171 changes: 156 additions & 15 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5657,9 +5657,12 @@ impl UnresolvedRestoreStats {
}

/// Project a slice of [`UnresolvedAssetLockTxRecordFFI`] rows onto the
/// in-memory `transactions()` maps of the matching
/// `standard_bip44_accounts[account_index]` slots on the rebuilt
/// `ManagedWalletInfo`.
/// in-memory `transactions()` maps of the rebuilt `ManagedWalletInfo`,
/// routing each record to the first present family the proof lookup
/// searches: `standard_bip44_accounts[account_index]`, then
/// `standard_bip32_accounts[account_index]`, then
/// `coinjoin_accounts[account_index]`, then any DashPay receival
/// account (the lookup scans those by txid, index-independent).
///
/// See the call site in [`build_wallet_start_state`] for the design
/// rationale on WHY this exists at all (selective bulk-restore for
Expand Down Expand Up @@ -5735,22 +5738,43 @@ fn restore_unresolved_asset_lock_tx_records(
_ => TransactionContext::Mempool,
};

// Asset-lock txs are funded from a BIP44 account; that's
// the only account map the asset-lock recovery flow
// consults (`recover_asset_lock_blocking` reads
// `info.core_wallet.accounts.standard_bip44_accounts.get(
// &account_index)...transactions().get(&out_point.txid)`),
// so restoration goes through the same map. Records for
// other variants would never be reached by that lookup.
let Some(account) = wallet_info
.accounts
// A pooled asset lock can be funded ENTIRELY from a BIP32
// account or a DashPay receiving account — the builder
// deliberately skips absent source families — so
// `standard_bip44_accounts[account_index]` may not exist
// for a perfectly valid record. The recovery lookup
// (`funding_tx_record` in sync::proof) searches BIP44 and
// BIP32 by this index, CoinJoin by index, and DashPay
// receiving accounts by txid regardless of index; restore
// the synthetic record into the FIRST present family that
// lookup searches instead of dropping it — a dropped
// record leaves an already-broadcast lock stuck at
// `Broadcast` after restart, unrecoverable by any later
// promotion path.
let accounts = &mut wallet_info.accounts;
let account = if accounts
.standard_bip44_accounts
.get_mut(&rec.account_index)
else {
.contains_key(&rec.account_index)
{
accounts.standard_bip44_accounts.get_mut(&rec.account_index)
} else if accounts
.standard_bip32_accounts
.contains_key(&rec.account_index)
{
accounts.standard_bip32_accounts.get_mut(&rec.account_index)
} else if accounts.coinjoin_accounts.contains_key(&rec.account_index) {
accounts.coinjoin_accounts.get_mut(&rec.account_index)
} else {
// Any receival account works: the proof lookup scans
// them all by txid, ignoring the tracked source index.
accounts.dashpay_receival_accounts.values_mut().next()
};
let Some(account) = account else {
stats.dropped_no_account += 1;
tracing::warn!(
account_index = rec.account_index,
"load: dropping unresolved-asset-lock tx record — no matching BIP44 account"
"load: dropping unresolved-asset-lock tx record — no account in any \
family the proof lookup searches"
);
continue;
};
Expand Down Expand Up @@ -6642,6 +6666,123 @@ mod tests {
ManagedWalletInfo::from_wallet(&wallet, 0)
}

/// Same construction as `test_managed_wallet_info_with_bip44` but with a
/// single account of the given standard/DashPay `account_type` — the
/// pooled-restore tests need wallets whose ONLY account is a non-BIP44
/// family, because that's exactly the shape the pooled builder produces
/// when it skips absent source families.
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("");
let master = ExtendedPrivKey::new_master(Network::Testnet, &seed)
.expect("master derivation must succeed");
let secp = Secp256k1::new();
let xpub = ExtendedPubKey::from_priv(&secp, &master);
let account = Account::from_xpub(None, account_type, xpub, Network::Testnet)
.expect("Account::from_xpub on a valid xpub must succeed");
let mut accounts = key_wallet::AccountCollection::new();
accounts
.insert(account)
.expect("inserting the single account must succeed");
let wallet = Wallet::new_external_signable(Network::Testnet, [0u8; 32], accounts);
ManagedWalletInfo::from_wallet(&wallet, 0)
}

/// A lock funded EXCLUSIVELY from a BIP32 account (the pooled builder
/// skips absent families, so no BIP44 account exists at the index) must
/// restore into `standard_bip32_accounts` — the pre-fix bridge only
/// consulted BIP44 and dropped the record, leaving an already-broadcast
/// lock unrecoverable after restart.
#[test]
fn restore_routes_to_bip32_when_indexed_bip44_absent() {
let mut wallet_info = test_managed_wallet_info_with_account(AccountType::Standard {
index: 7,
standard_account_type: StandardAccountType::BIP32Account,
});
let tx = synthetic_minimal_tx();
let txid = tx.txid();
let mut tx_buf: Vec<u8> = serialize(&tx);

let rec = UnresolvedAssetLockTxRecordFFI {
account_index: 7,
tx_bytes: tx_buf.as_mut_ptr(),
tx_bytes_len: tx_buf.len(),
context_raw: 2,
block_height: 1475917,
block_hash: [0x42u8; 32],
block_timestamp: 1700000000,
first_seen: 1699999000,
};

let stats = restore_unresolved_asset_lock_tx_records(&mut wallet_info, &[rec])
.expect("restoration should not error");
assert_eq!(
stats.restored, 1,
"the BIP32-only record must restore, not drop"
);
assert!(
wallet_info
.accounts
.standard_bip32_accounts
.get(&7)
.expect("BIP32 account 7 must exist")
.transactions()
.contains_key(&txid),
"the restored record must land in the BIP32 account the proof lookup searches"
);
drop(tx_buf);
}

/// A lock funded EXCLUSIVELY from a DashPay receiving account (no
/// standard account exists at the tracked index at all) must restore
/// into a receival account — the proof lookup scans them by txid,
/// ignoring the tracked source index, so any receival account makes the
/// record findable again after restart.
#[test]
fn restore_routes_to_dashpay_receival_when_indexed_standard_absent() {
let mut wallet_info =
test_managed_wallet_info_with_account(AccountType::DashpayReceivingFunds {
index: 2,
user_identity_id: [0x11u8; 32],
friend_identity_id: [0x22u8; 32],
});
let tx = synthetic_minimal_tx();
let txid = tx.txid();
let mut tx_buf: Vec<u8> = serialize(&tx);

let rec = UnresolvedAssetLockTxRecordFFI {
// Tracked source index that matches NO standard account.
account_index: 3,
tx_bytes: tx_buf.as_mut_ptr(),
tx_bytes_len: tx_buf.len(),
context_raw: 2,
block_height: 1475917,
block_hash: [0x42u8; 32],
block_timestamp: 1700000000,
first_seen: 1699999000,
};

let stats = restore_unresolved_asset_lock_tx_records(&mut wallet_info, &[rec])
.expect("restoration should not error");
assert_eq!(
stats.restored, 1,
"the DashPay-only record must restore, not drop"
);
assert!(
wallet_info
.accounts
.dashpay_receival_accounts
.values()
.any(|account| account.transactions().contains_key(&txid)),
"the restored record must land in a receival account (searched by txid)"
);
drop(tx_buf);
}

/// Same reproducible testnet xpub as `test_managed_wallet_info_with_bip44`,
/// wrapped as a `ProviderOwnerKeys` account so the managed collection
/// ends up with a `provider_owner_keys` account carrying its address
Expand Down
Loading
Loading