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
52 changes: 26 additions & 26 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 @@ -50,14 +50,14 @@ members = [
]

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

tokio-metrics = "0.5"

Expand Down
79 changes: 74 additions & 5 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3000,20 +3000,38 @@ unsafe fn address_info_from_ffi(
}

/// Restore persisted `AddressInfo` rows into a managed account's
/// `AddressPool`. Plain upsert: a persisted row overwrites the gap-limit
/// `AddressPool`. Upsert: a persisted row overwrites the gap-limit
/// default `ManagedWalletInfo::from_wallet` pre-derived at the same
/// index, and the reverse-lookup maps + `highest_*` watermarks are
/// extended to cover indices past that default gap window.
///
/// The persisted row is authoritative for its typed public key — the
/// [`CoreAddressEntryFFI`] row now carries the full typed key (ECDSA-33 /
/// [`CoreAddressEntryFFI`] row carries the full typed key (ECDSA-33 /
/// BLS-48 / EdDSA-32) with a [`KeyTypeTagFFI`] discriminator, so BLS
/// operator and Ed25519 platform-node keys survive the round-trip in the
/// row itself. No merge against the pre-derived entry is needed or
/// wanted.
/// row itself — with ONE legacy exception: rows persisted before the
/// typed-key column existed carry an empty key (`public_key: None`).
/// Overwriting a pre-derived typed entry with such a row would strip the
/// in-memory BLS operator pubkeys that `from_wallet` derived from the
/// account xpub, silently breaking operator-ownership matching for every
/// pre-typed-key store. So when the incoming row has no key but the
/// pre-derived entry at that index does, the existing typed key is kept
/// (post-migration rows always carry their key, making this a no-op for
/// them). Legacy Ed25519 platform-node rows cannot be recovered this way
/// (hardened-only — no public derivation) nor migrated from the removed
/// account-level batch (its data was dropped by the schema migration);
/// per the pre-release convention those stores re-derive on
/// delete+re-import.
fn restore_address_pool(pool: &mut AddressPool, infos: Vec<AddressInfo>) {
for info in infos {
for mut info in infos {
let idx = info.index;
if info.public_key.is_none() {
if let Some(existing) = pool.addresses.get(&idx) {
if existing.public_key.is_some() {
info.public_key = existing.public_key.clone();
}
}
}
pool.address_index.insert(info.address.clone(), idx);
pool.script_pubkey_index
.insert(info.script_pubkey.clone(), idx);
Expand Down Expand Up @@ -5920,6 +5938,57 @@ mod tests {
}
}

/// 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
/// stores otherwise lose their in-memory BLS operator pubkeys at
/// load and masternode operator-ownership matching silently breaks
/// (post-migration rows always carry their key, so the preservation
/// is a no-op for them). Also pins the inverse: a legacy row at an
/// index with no prederived entry restores key-less rather than
/// inventing anything.
#[test]
fn legacy_keyless_row_keeps_prederived_typed_key() {
let mut pool = AddressPool::new_without_generation(
DerivationPath::from_str("m/9'/1'/3'").expect("static path must parse"),
AddressPoolType::AbsentHardened,
5,
Network::Testnet,
);

// Prederived typed entry, as `ManagedWalletInfo::from_wallet`
// seeds BLS operator pools from the account xpub.
let bls = vec![0xE7u8; 48];
let prederived = typed_key_test_address_info(7, Some(PublicKeyType::BLS(bls.clone())));
pool.addresses.insert(7, prederived);

// Legacy rows: same index key-less, plus one at a fresh index.
let legacy_same_idx = typed_key_test_address_info(7, None);
let legacy_fresh_idx = typed_key_test_address_info(9, None);
restore_address_pool(&mut pool, vec![legacy_same_idx, legacy_fresh_idx]);

match &pool
.addresses
.get(&7)
.expect("entry 7 must exist")
.public_key
{
Some(PublicKeyType::BLS(bytes)) => assert_eq!(
bytes, &bls,
"legacy key-less row must keep the prederived BLS key"
),
other => panic!("prederived BLS key was stripped, got {:?}", other),
}
assert!(
pool.addresses
.get(&9)
.expect("entry 9 must exist")
.public_key
.is_none(),
"a legacy row with no prederived counterpart stays key-less"
);
}

/// `account_xpub` must survive the persist→restore byte round-trip — it is
/// the key the seed-binding self-check (`PlatformWallet::verify_seed_binds`)
/// later compares the resolver-derived xpub against, so a corrupted restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ public final class PersistentCoreAddress {
/// pre-column stores openable: without it, lightweight migration
/// fails with "missing attribute values on mandatory destination
/// attribute" and the container refuses to load — a launch crash on
/// every device that has existing rows. Defaulted rows read as
/// ECDSA until the next Rust address-pool persist pulse re-tags
/// typed entries, which it does on every load.
/// every device that has existing rows. Defaulted legacy rows read
/// as ECDSA with an empty `publicKey` until the next Rust
/// address-pool persist pulse (pool extension / address-used /
/// registration — NOT plain load, which only reads the snapshot)
/// re-emits them with typed keys. On load, Rust's
/// `restore_address_pool` keeps the pre-derived typed key when a
/// legacy row arrives key-less, so in-memory BLS operator matching
/// is unaffected; legacy Ed25519 platform-node keys are hardened-only
/// and re-derivable only via delete+re-import (pre-release
/// convention).
public var keyType: UInt8 = 0
/// `AddressPoolTypeTagFFI` raw value — 0 External, 1 Internal,
/// 2 Absent, 3 AbsentHardened.
Expand Down
Loading