Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" }
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" }

tokio-metrics = "0.5"

Expand Down
10 changes: 9 additions & 1 deletion packages/rs-platform-wallet-ffi/src/core_address_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ pub struct CoreAddressEntryFFI {
pub pool_type_tag: u8,
/// Derivation index within this pool.
pub address_index: u32,
/// `AddressInfo.used` at emit time.
/// Whether funds had been seen at this address at emit time, i.e.
/// `AddressInfo.state == AddressState::Used`.
///
/// This schema has no slot for a reservation, so an
/// `AddressState::Reserved` entry necessarily flattens to `false`
/// and reloads as `Available`. Nothing in platform reserves
/// addresses today, so that lossy case is unreachable; adding a
/// reserving caller requires a schema decision here first (see
/// `persistence::build_core_address_entry_ffi`).
pub is_used: bool,
/// Cached balance in duffs from `AddressInfo.balance`.
pub balance: u64,
Expand Down
94 changes: 81 additions & 13 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use key_wallet::bip32::DerivationPath;
use key_wallet::bip32::ExtendedPubKey;
use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey;
use key_wallet::derivation_slip10::ExtendedEd25519PubKey;
use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, PublicKeyType};
use key_wallet::managed_account::address_pool::{
AddressPool, AddressPoolType, AddressState, PublicKeyType,
};
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;
use key_wallet::wallet::Wallet;
Expand Down Expand Up @@ -3125,7 +3127,28 @@ fn build_core_address_entry_ffi(
key_type_tag,
pool_type_tag,
address_index: info.index,
is_used: info.used,
// `CoreAddressEntryFFI` carries a single `is_used` bool with no
// slot for a reservation, so `Reserved` can only flatten to
// `false` and would reload as `Available` — silently returning a
// handed-out address to the pool. Nothing in platform reserves
// addresses (no caller of `next_receive_address_and_reserve` /
// `next_unused_and_reserve` in the workspace), so that arm is
// unreachable today. It is spelled out rather than folded into a
// catch-all so the first reserving caller shows up as an explicit
// schema decision instead of losing state on the next reload.
is_used: match info.state {
AddressState::Used => true,
AddressState::Available => false,
AddressState::Reserved { .. } => {
tracing::warn!(
index = info.index,
"persist: address pool entry is reserved, but the persisted \
address schema cannot represent a reservation; it will \
reload as available and may be handed out again"
);
false
}
},
balance: info.balance,
address_base58: address_ptr,
derivation_path: path_ptr,
Expand Down Expand Up @@ -3221,9 +3244,14 @@ unsafe fn address_info_from_ffi(
public_key,
index: entry.address_index,
path,
used: entry.is_used,
generated_at: 0,
used_at: if entry.is_used { Some(0) } else { None },
// The persisted entry only carries `is_used`, so state round-trips
// to `Used`/`Available`; a `Reserved` entry is not representable —
// see `build_core_address_entry_ffi`.
state: if entry.is_used {
AddressState::Used
} else {
AddressState::Available
},
tx_count: 0,
total_received: 0,
total_sent: 0,
Expand Down Expand Up @@ -3270,7 +3298,7 @@ fn restore_address_pool(pool: &mut AddressPool, infos: Vec<AddressInfo>) {
pool.script_pubkey_index
.insert(info.script_pubkey.clone(), idx);
pool.highest_generated = Some(pool.highest_generated.map_or(idx, |h| h.max(idx)));
if info.used {
if matches!(info.state, AddressState::Used) {
pool.used_indices.insert(idx);
pool.highest_used = Some(pool.highest_used.map_or(idx, |h| h.max(idx)));
}
Expand Down Expand Up @@ -6248,9 +6276,7 @@ mod tests {
index,
path: DerivationPath::from_str(&format!("m/9'/1'/2'/{}", index))
.expect("static derivation path must parse"),
used: false,
generated_at: 0,
used_at: None,
state: AddressState::Available,
tx_count: 0,
total_received: 0,
total_sent: 0,
Expand Down Expand Up @@ -6370,6 +6396,50 @@ mod tests {
}
}

/// Pin what the persisted address row does to `AddressState`.
/// `CoreAddressEntryFFI` carries a single `is_used` bool, so only
/// `Used` and `Available` survive a save/load cycle intact;
/// `Reserved { .. }` has no representation and comes back
/// `Available`, i.e. a reload silently frees a handed-out address.
/// Nothing in platform reserves addresses today, so this records the
/// boundary rather than a live bug: if a reserving caller is ever
/// added, this test is where the schema decision (a dedicated
/// reservation field on the row) has to be made.
#[test]
fn address_state_round_trip_keeps_used_and_flattens_reserved() {
for (state, expected) in [
(AddressState::Available, AddressState::Available),
(AddressState::Used, AddressState::Used),
(
AddressState::Reserved { at: 1_700_000_000 },
AddressState::Available,
),
] {
let mut info = typed_key_test_address_info(11, None);
info.state = state;

let mut owned: Vec<CString> = Vec::new();
let entry = build_core_address_entry_ffi(
&info,
AddressPoolTypeTagFFI::AbsentHardened as u8,
false,
&mut owned,
)
.expect("build_core_address_entry_ffi must succeed");
// SAFETY: the address / path c-strings live in `owned`, kept
// alive until after this decode.
let restored = unsafe { address_info_from_ffi(&entry, Network::Testnet) }
.expect("address_info_from_ffi must decode the row");
drop(owned);

assert_eq!(
restored.state, expected,
"{:?} must restore as {:?} through the persisted row",
state, expected
);
}
}

/// A LEGACY row (persisted before the typed-key column: empty key,
/// `public_key: None` after decode) must NOT strip the typed key the
/// gap-limit prederivation put at the same index — pre-typed-key
Expand Down Expand Up @@ -7072,9 +7142,7 @@ mod tests {
public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())),
index,
path,
used: true,
generated_at: 0,
used_at: None,
state: AddressState::Used,
tx_count: 0,
total_received: 0,
total_sent: 0,
Expand Down Expand Up @@ -7124,7 +7192,7 @@ mod tests {
entries
.iter()
.flat_map(|e| e.addresses.iter())
.all(|a| a.used),
.all(|a| matches!(a.state, AddressState::Used)),
"every emitted marked-used address must carry used == true"
);
}
Expand Down
6 changes: 2 additions & 4 deletions packages/rs-platform-wallet/src/changeset/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2158,7 +2158,7 @@ mod tests {
index: u32,
) -> key_wallet::transaction_checking::DerivedAddressInfo {
use key_wallet::bip32::{ChildNumber, DerivationPath};
use key_wallet::managed_account::address_pool::{AddressInfo, PublicKeyType};
use key_wallet::managed_account::address_pool::{AddressInfo, AddressState, PublicKeyType};

let pubkey =
dashcore::PublicKey::from_slice(&TEST_PUBKEY_G).expect("generator point is valid");
Expand All @@ -2177,9 +2177,7 @@ mod tests {
public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())),
index,
path,
used: true,
generated_at: 0,
used_at: None,
state: AddressState::Used,
tx_count: 0,
total_received: 0,
total_sent: 0,
Expand Down
8 changes: 4 additions & 4 deletions packages/rs-platform-wallet/src/changeset/core_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use std::sync::Arc;
use dashcore::blockdata::transaction::{txout::TxOut, OutPoint};
use dashcore::ScriptBuf;
use key_wallet::account::AccountType;
use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType};
use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, AddressState};
use key_wallet::managed_account::transaction_record::{OutputRole, TransactionRecord};
use key_wallet::transaction_checking::transaction_router::AccountTypeToCheck;
use key_wallet::transaction_checking::{DerivedAddressInfo, TransactionContext};
Expand Down Expand Up @@ -565,7 +565,7 @@ fn collect_usage_deltas_from_accounts(
touched.insert(*owner_type);
if seen.insert((*owner_type, pool.pool_type, pool_info.index)) {
let mut info = pool_info.clone();
info.used = true;
info.state = AddressState::Used;
marked_used.push(DerivedAddressInfo {
account_type: *owner_type,
pool_type: pool.pool_type,
Expand Down Expand Up @@ -836,7 +836,7 @@ mod usage_delta_tests {
assert_eq!(entry.account_type, bip44_account_0());
assert_eq!(entry.pool_type, AddressPoolType::External);
assert_eq!(entry.info.index, 0);
assert!(entry.info.used);
assert!(matches!(entry.info.state, AddressState::Used));

let watermarks = highest
.get(&bip44_account_0())
Expand Down Expand Up @@ -902,7 +902,7 @@ mod usage_delta_tests {
.find(|d| d.info.address == receive_address)
.expect("spent-input address must be in the marked-used delta");
assert_eq!(entry.pool_type, AddressPoolType::External);
assert!(entry.info.used);
assert!(matches!(entry.info.state, AddressState::Used));
// The foreign output must NOT resolve to any pool.
assert!(
marked.iter().all(|d| d.info.address != {
Expand Down
15 changes: 11 additions & 4 deletions packages/rs-platform-wallet/src/manager/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::sync::Arc;
use dashcore::{OutPoint, Txid};
use dpp::prelude::Identifier;
use key_wallet::account::AccountType;
use key_wallet::managed_account::address_pool::{AddressInfo, AddressPool, AddressPoolType};
use key_wallet::managed_account::address_pool::{
AddressInfo, AddressPool, AddressPoolType, AddressState,
};
use key_wallet::managed_account::transaction_record::TransactionRecord;
use key_wallet::utxo::Utxo;
use key_wallet::WalletCoreBalance;
Expand Down Expand Up @@ -400,8 +402,13 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
.address_pools()
.iter()
.fold((0u32, 0u32), |(used, total), pool| {
let pool_used =
pool.addresses.values().filter(|info| info.used).count() as u32;
// "used" counts only funded addresses; a `Reserved`
// address is handed out but not yet used.
let pool_used = pool
.addresses
.values()
.filter(|info| matches!(info.state, AddressState::Used))
.count() as u32;
let pool_total = pool.addresses.len() as u32;
(used + pool_used, total + pool_total)
});
Expand Down Expand Up @@ -1110,7 +1117,7 @@ fn addr_info_snapshot(info: &AddressInfo) -> AccountAddressInfoSnapshot {
AccountAddressInfoSnapshot {
pubkey_hash,
address_index: info.index,
is_used: info.used,
is_used: matches!(info.state, AddressState::Used),
address,
public_key_bytes,
}
Expand Down
Loading
Loading