Skip to content

Commit a843592

Browse files
fix(platform-wallet): keep prederived typed keys for legacy key-less rows + land the #893 crash-fix pin (#4132)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f744652 commit a843592

4 files changed

Lines changed: 118 additions & 42 deletions

File tree

Cargo.lock

Lines changed: 26 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ members = [
5050
]
5151

5252
[workspace.dependencies]
53-
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
54-
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
55-
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
56-
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
57-
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
58-
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
59-
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
60-
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "38c689d97ddce0483d4ca455ade32efb019eb68d" }
53+
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
54+
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
55+
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
56+
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
57+
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
58+
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
59+
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
60+
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "19690d31b37ba12adc00f6bf2f8413d97e2b669d" }
6161

6262
tokio-metrics = "0.5"
6363

packages/rs-platform-wallet-ffi/src/persistence.rs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,20 +3000,38 @@ unsafe fn address_info_from_ffi(
30003000
}
30013001

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

5941+
/// A LEGACY row (persisted before the typed-key column: empty key,
5942+
/// `public_key: None` after decode) must NOT strip the typed key the
5943+
/// gap-limit prederivation put at the same index — pre-typed-key
5944+
/// stores otherwise lose their in-memory BLS operator pubkeys at
5945+
/// load and masternode operator-ownership matching silently breaks
5946+
/// (post-migration rows always carry their key, so the preservation
5947+
/// is a no-op for them). Also pins the inverse: a legacy row at an
5948+
/// index with no prederived entry restores key-less rather than
5949+
/// inventing anything.
5950+
#[test]
5951+
fn legacy_keyless_row_keeps_prederived_typed_key() {
5952+
let mut pool = AddressPool::new_without_generation(
5953+
DerivationPath::from_str("m/9'/1'/3'").expect("static path must parse"),
5954+
AddressPoolType::AbsentHardened,
5955+
5,
5956+
Network::Testnet,
5957+
);
5958+
5959+
// Prederived typed entry, as `ManagedWalletInfo::from_wallet`
5960+
// seeds BLS operator pools from the account xpub.
5961+
let bls = vec![0xE7u8; 48];
5962+
let prederived = typed_key_test_address_info(7, Some(PublicKeyType::BLS(bls.clone())));
5963+
pool.addresses.insert(7, prederived);
5964+
5965+
// Legacy rows: same index key-less, plus one at a fresh index.
5966+
let legacy_same_idx = typed_key_test_address_info(7, None);
5967+
let legacy_fresh_idx = typed_key_test_address_info(9, None);
5968+
restore_address_pool(&mut pool, vec![legacy_same_idx, legacy_fresh_idx]);
5969+
5970+
match &pool
5971+
.addresses
5972+
.get(&7)
5973+
.expect("entry 7 must exist")
5974+
.public_key
5975+
{
5976+
Some(PublicKeyType::BLS(bytes)) => assert_eq!(
5977+
bytes, &bls,
5978+
"legacy key-less row must keep the prederived BLS key"
5979+
),
5980+
other => panic!("prederived BLS key was stripped, got {:?}", other),
5981+
}
5982+
assert!(
5983+
pool.addresses
5984+
.get(&9)
5985+
.expect("entry 9 must exist")
5986+
.public_key
5987+
.is_none(),
5988+
"a legacy row with no prederived counterpart stays key-less"
5989+
);
5990+
}
5991+
59235992
/// `account_xpub` must survive the persist→restore byte round-trip — it is
59245993
/// the key the seed-binding self-check (`PlatformWallet::verify_seed_binds`)
59255994
/// later compares the resolver-derived xpub against, so a corrupted restore

packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentCoreAddress.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,16 @@ public final class PersistentCoreAddress {
2828
/// pre-column stores openable: without it, lightweight migration
2929
/// fails with "missing attribute values on mandatory destination
3030
/// attribute" and the container refuses to load — a launch crash on
31-
/// every device that has existing rows. Defaulted rows read as
32-
/// ECDSA until the next Rust address-pool persist pulse re-tags
33-
/// typed entries, which it does on every load.
31+
/// every device that has existing rows. Defaulted legacy rows read
32+
/// as ECDSA with an empty `publicKey` until the next Rust
33+
/// address-pool persist pulse (pool extension / address-used /
34+
/// registration — NOT plain load, which only reads the snapshot)
35+
/// re-emits them with typed keys. On load, Rust's
36+
/// `restore_address_pool` keeps the pre-derived typed key when a
37+
/// legacy row arrives key-less, so in-memory BLS operator matching
38+
/// is unaffected; legacy Ed25519 platform-node keys are hardened-only
39+
/// and re-derivable only via delete+re-import (pre-release
40+
/// convention).
3441
public var keyType: UInt8 = 0
3542
/// `AddressPoolTypeTagFFI` raw value — 0 External, 1 Internal,
3643
/// 2 Absent, 3 AbsentHardened.

0 commit comments

Comments
 (0)