Skip to content

Commit b328f0e

Browse files
committed
refactor(tools): read masternode keys from the running wallet
The masternode owner and voting key screens read the running wallet's provider address pool, and `providerKeyAtIndex` derives the keys. The pool a throwaway KeyWallet wallet derived from the phrase is gone. An empty pool now means the wallet has no provider account of that kind, and `providerKeyAtIndex` throws for it. `poolIsLive` becomes a computed `hasProviderPool`, and the voting screen's banner says the wallet has no masternode voting key account instead of asking the user to finish syncing. The banner's catalog entry is English in every locale until it is translated again. `SwiftDashSDKHost.derivationWallet()`, the throwaway key-wallet these screens and CrowdNode used, has no callers left and is deleted.
1 parent 09a1e93 commit b328f0e

48 files changed

Lines changed: 76 additions & 186 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -593,31 +593,6 @@ final class SwiftDashSDKHost {
593593
return persistedMnemonics().contains { Mnemonic.normalizePhrase($0.mnemonic) == typed }
594594
}
595595

596-
/// Throwaway key-wallet stack (manager + wallet) for path-based key
597-
/// derivation, built from the persisted mnemonic of the host's active
598-
/// wallet. Separate from the running `PlatformWalletManager` — key
599-
/// derivation lives in the `WalletManager`/`Wallet` FFI surface, which
600-
/// the runtime handles don't expose. The instances derive keys locally
601-
/// (no networking, no persistence); drop them when done.
602-
///
603-
/// Consumers: the masternode Owner/Voting key deriver and CrowdNode
604-
/// message signing. Returns nil when no wallet is bound/running or the
605-
/// mnemonic can't be read (logged).
606-
func derivationWallet() -> (manager: WalletManager, wallet: Wallet, walletId: Data)? {
607-
guard let hostWalletId = wallet?.walletId, let network = runningNetwork else {
608-
Self.logger.warning("🪺 HOST :: derivationWallet — no bound wallet/network")
609-
return nil
610-
}
611-
guard let mnemonic = try? WalletStorage().retrieveMnemonic(for: hostWalletId),
612-
let manager = try? WalletManager(network: network),
613-
let walletId = try? manager.addWallet(mnemonic: mnemonic),
614-
let derivationWallet = (try? manager.getWallet(id: walletId)) ?? nil else {
615-
Self.logger.error("🪺 HOST :: derivationWallet — key-wallet bootstrap failed")
616-
return nil
617-
}
618-
return (manager: manager, wallet: derivationWallet, walletId: walletId)
619-
}
620-
621596
// MARK: - Lifecycle
622597

623598
enum HostError: LocalizedError {

‎DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoterRegistry.swift‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ final class MasternodeVoterRegistry {
9393
/// Outcome of a votable-node resolution.
9494
///
9595
/// `mayBeIncomplete` says the answer cannot be trusted as exhaustive: the
96-
/// voting address pool was read from the degraded fallback (gap-limit
97-
/// deep) *and* at least one active registration went unmatched, so a node
98-
/// this wallet really can vote with may be missing from `nodes`. The UI
99-
/// says so rather than presenting a short list as the full picture.
96+
/// running wallet has no provider voting account *and* at least one active
97+
/// registration went unmatched, so a node this wallet really can vote with
98+
/// may be missing from `nodes`. The UI says so rather than presenting a
99+
/// short list as the full picture.
100100
struct Resolution {
101101
let nodes: [VoterNode]
102102
let mayBeIncomplete: Bool
@@ -163,11 +163,11 @@ final class MasternodeVoterRegistry {
163163
keySource: .walletIndex(index))
164164
}
165165

166-
// Only a degraded pool read can hide a node we own; if every active
167-
// registration matched, depth did not matter.
168-
let mayBeIncomplete = !resolution.poolIsLive && nodes.count < eligible.count
166+
// Only a missing provider voting account can hide a node we own. If
167+
// every active registration matched, the account did not matter.
168+
let mayBeIncomplete = !resolution.hasProviderPool && nodes.count < eligible.count
169169
Self.logger.info(
170-
"🗳️ VOTING :: votable nodes=\(nodes.count, privacy: .public) of \(eligible.count, privacy: .public) active registrations livePool=\(resolution.poolIsLive, privacy: .public)")
170+
"🗳️ VOTING :: votable nodes=\(nodes.count, privacy: .public) of \(eligible.count, privacy: .public) active registrations providerPool=\(resolution.hasProviderPool, privacy: .public)")
171171
// Exclusion is by the wallet's ELIGIBLE set (not just the votable
172172
// subset): a wallet-registered node stays out of the tracked list
173173
// even when its derived key didn't resolve.

‎DashWallet/Sources/UI/DashPay/Voting/UsernameVotingScreen.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ struct UsernameVotingScreen: View {
5353
if viewModel.nodeListMayBeIncomplete {
5454
VotingBanner(
5555
text: NSLocalizedString(
56-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen.",
56+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.",
5757
comment: "Voting"),
5858
tone: .warning)
5959
}

‎DashWallet/Sources/UI/Menu/Tools/Masternode Keys/DerivationPathKeys/Models/DerivationPathKeysModel.swift‎

Lines changed: 16 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ extension DerivationPathKeysModel {
185185
/// Derives masternode provider Owner/Voting keys (ECDSA) from SwiftDashSDK,
186186
/// replacing DashSync's `DSAuthenticationKeysDerivationPath`.
187187
///
188-
/// The DIP-3 paths — voting `m/9'/<coin>'/3'/1'`, owner `m/9'/<coin>'/3'/2'`
189-
/// (ECDSA, fully hardened account path, soft key index; coin = 5' mainnet /
190-
/// 1' testnet) — are resolved Rust-side by `providerKeyAtIndex`. They are NOT
191-
/// rebuilt here: composing them app-side is what let the account path be
192-
/// applied twice and produced keys off the intended branch.
188+
/// The DIP-3 voting and owner account paths (ECDSA, fully hardened account
189+
/// path, soft key index) are resolved Rust-side: `providerKeyAtIndex` derives
190+
/// every private key, and the addresses come from the running wallet's pool.
191+
/// Composing the account path app-side is what let it be applied twice and
192+
/// produced keys off the intended branch.
193193
///
194194
/// Internal (not private): `MasternodeKeyUsage` reuses `address(at:)` for
195195
/// its owner/voting address join.
@@ -206,32 +206,17 @@ final class MasternodeProviderKeyDeriver {
206206
/// resolver the operator/platform families use — not a second copy of it.
207207
private let keyResolver: ProviderKeyResolver
208208

209-
/// Index → base58 address of the provider pool, snapshotted once at init.
210-
/// Sourced from the running wallet (`loadLiveAddresses`), falling back to
211-
/// the derivation wallet's own pool (`loadDerivationAddresses`) when the
212-
/// running wallet has no provider account yet — e.g. a just-imported
213-
/// wallet whose registration hasn't completed. Empty only when neither
214-
/// source has a pool, in which case there are genuinely no addresses to
215-
/// join against.
209+
/// Index → base58 address of the running wallet's provider pool,
210+
/// snapshotted once at init. Empty when the wallet has no provider account
211+
/// yet, such as a just-imported wallet whose registration hasn't completed.
216212
private let poolAddresses: [UInt32: String]
217213

218214
init?(key: MNKey) {
219-
// The network itself is no longer read here — it selected the coin
220-
// type when this class composed the DIP-3 path app-side. The resolver
221-
// owns that now; the check remains because a host with no running
222-
// network has no wallet to derive from either.
223-
guard SwiftDashSDKHost.shared.runningNetwork != nil else {
224-
return nil
225-
}
226-
227-
let type: AccountType
228215
let kind: ManagedPlatformWallet.ProviderKeyKind
229216
switch key {
230217
case .voting:
231-
type = .providerVotingKeys
232218
kind = .votingECDSA
233219
case .owner:
234-
type = .providerOwnerKeys
235220
kind = .ownerECDSA
236221
case .operator, .evonodeOperator:
237222
// BLS / Ed25519 families have no address rows, so they use
@@ -243,42 +228,17 @@ final class MasternodeProviderKeyDeriver {
243228
return nil
244229
}
245230

246-
// The derivation stack is a THROWAWAY key-wallet built from the
247-
// mnemonic (see `SwiftDashSDKHost.derivationWallet`) — it has never
248-
// processed a transaction, so its pools sit at the freshly-created
249-
// `DEFAULT_SPECIAL_GAP_LIMIT` depth. It backs ONLY the fallback
250-
// address pool below; private keys come from `keyResolver`. Nothing
251-
// outlives this initializer: the wallet is owned by `derivationManager`,
252-
// which is released on return, so holding either past the pool read
253-
// would leave a reference into a freed graph.
254-
guard let (derivationManager, wallet, derivationWalletId) =
255-
SwiftDashSDKHost.shared.derivationWallet() else {
256-
return nil
257-
}
258-
259-
// Ensure the provider account exists so the fallback pool read below
260-
// can resolve it.
261-
_ = try? wallet.getAccount(type: type)
262-
231+
self.poolAddresses = Self.loadLiveAddresses(for: key)
263232
self.keyResolver = keyResolver
264-
265-
let live = Self.loadLiveAddresses(for: key)
266-
self.poolIsLive = !live.isEmpty
267-
self.poolAddresses = live.isEmpty
268-
? Self.loadDerivationAddresses(
269-
key: key, manager: derivationManager, walletId: derivationWalletId)
270-
: live
271233
}
272234

273-
/// `true` when ``poolAddresses`` came from the running wallet's live pool.
235+
/// Whether the running wallet has a provider account of this kind.
274236
///
275-
/// `false` means the degraded derivation-wallet fallback, which only ever
276-
/// holds `DEFAULT_SPECIAL_GAP_LIMIT` entries. Consumers that merely *look
277-
/// up* an index are unaffected, but consumers that **enumerate** the pool
278-
/// to decide what exists — the owner/voting address joins — can silently
279-
/// under-report, so they must surface the incompleteness rather than
280-
/// present a short list as the whole truth.
281-
let poolIsLive: Bool
237+
/// Without one ``poolAddresses`` is empty and `providerKeyAtIndex` throws.
238+
/// Consumers that **enumerate** the pool to decide what exists, the
239+
/// owner/voting address joins, must say so rather than present an empty
240+
/// list as the whole truth.
241+
var hasProviderPool: Bool { !poolAddresses.isEmpty }
282242

283243
/// Snapshot the running wallet's provider address pool — the one SPV
284244
/// extends as ProRegTx/ProUpRegTx matches mark indexes used, and the one
@@ -306,47 +266,6 @@ final class MasternodeProviderKeyDeriver {
306266
return addresses
307267
}
308268

309-
/// Degraded-path pool read from the derivation wallet itself, used when
310-
/// the running wallet can't vend the account. This stack never processes
311-
/// transactions, so its pool is only ever the freshly-created
312-
/// `DEFAULT_SPECIAL_GAP_LIMIT` depth — enough to keep the low indexes
313-
/// resolvable rather than showing nothing at all, but it is NOT a
314-
/// substitute for the live pool (that shallowness is precisely the bug
315-
/// this class's live read exists to fix).
316-
private static func loadDerivationAddresses(
317-
key: MNKey,
318-
manager: WalletManager,
319-
walletId: Data
320-
) -> [UInt32: String] {
321-
guard let collection = manager.getManagedAccountCollection(walletId: walletId) else {
322-
return [:]
323-
}
324-
let account: ManagedAccount?
325-
switch key {
326-
case .voting: account = collection.getProviderVotingKeysAccount()
327-
case .owner: account = collection.getProviderOwnerKeysAccount()
328-
case .operator, .evonodeOperator: account = nil
329-
}
330-
guard let pool = account?.getAddressPool(type: .single)
331-
?? account?.getExternalAddressPool() else {
332-
return [:]
333-
}
334-
335-
// The pool exposes no count, so walk until it stops vending. The cap
336-
// is a runaway guard, not a semantic window: this pool is created at
337-
// gap-limit depth and never grows here.
338-
var addresses: [UInt32: String] = [:]
339-
for index in 0..<Self.derivationPoolProbeCap {
340-
guard let info = try? pool.getAddress(at: index) else { break }
341-
addresses[index] = info.address
342-
}
343-
return addresses
344-
}
345-
346-
/// Runaway guard for the fallback pool walk — far above any gap-limit
347-
/// depth the derivation wallet can hold.
348-
private static let derivationPoolProbeCap: UInt32 = 200
349-
350269
func wif(at index: UInt32) -> String? {
351270
keyResolver.key(at: index)?.privateKeyWIF
352271
}
@@ -355,11 +274,7 @@ final class MasternodeProviderKeyDeriver {
355274
keyResolver.key(at: index)?.privateKeyHex
356275
}
357276

358-
/// The pool address at `index`. Backed by the running wallet's live pool
359-
/// whenever it's available: reading the throwaway derivation wallet as
360-
/// the primary source would cap every consumer at that stack's initial
361-
/// 5-entry pool, hiding every masternode whose owner/voting key sits at
362-
/// a deeper index.
277+
/// The address at `index` of the running wallet's provider pool.
363278
func address(at index: UInt32) -> String? {
364279
poolAddresses[index]
365280
}

‎DashWallet/Sources/UI/Menu/Tools/Masternode Keys/Overview/Model/WalletKeysOverviewModel.swift‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,17 @@ struct MasternodeKeyUsage {
163163
resolveAddressIndexes(family: family, targets: targets).map
164164
}
165165

166-
/// The address join plus whether it was made against the complete pool.
166+
/// The address join plus whether the wallet has a provider account of
167+
/// this family.
167168
///
168-
/// `poolIsLive == false` means the derivation-wallet fallback answered,
169-
/// and that pool is only gap-limit deep — so an address that is genuinely
170-
/// ours but sits at a deeper index is simply not in `map`. Callers that
171-
/// present the result as "these are all of them" must say so; a caller
172-
/// that only labels known rows can ignore it.
169+
/// `hasProviderPool == false` means the running wallet has no provider
170+
/// account to join against, so an address that is genuinely ours is
171+
/// missing from `map`. Callers that present the result as "these are all
172+
/// of them" must say so. A caller that only labels known rows can ignore it.
173173
static func resolveAddressIndexes(
174174
family: MNKey,
175175
targets: Set<String>
176-
) -> (map: [String: UInt32], poolIsLive: Bool) {
176+
) -> (map: [String: UInt32], hasProviderPool: Bool) {
177177
guard !targets.isEmpty,
178178
let deriver = MasternodeProviderKeyDeriver(key: family) else { return ([:], true) }
179179
var map: [String: UInt32] = [:]
@@ -183,6 +183,6 @@ struct MasternodeKeyUsage {
183183
targets.contains(address) else { continue }
184184
map[address] = index
185185
}
186-
return (map, deriver.poolIsLive)
186+
return (map, deriver.hasProviderPool)
187187
}
188188
}

‎DashWallet/ar.lproj/Localizable.strings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4672,7 +4672,7 @@
46724672
/* Maya */
46734673
"Some coins are not available because of the halted chain" = "Some coins are not available because of the halted chain";
46744674

4675-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen." = "قد تكون بعض عقد الماسترنود لديك مفقودة من هذه القائمة — إذ تعذّر على المحفظة قراءة مجموعة مفاتيح التصويت كاملة. أكمل المزامنة ثم افتح هذه الشاشة من جديد.";
4675+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account." = "Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.";
46764676

46774677
/* Usernames */
46784678
"Some usernames can be blocked" = "Some usernames can be blocked";

‎DashWallet/bg.lproj/Localizable.strings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4672,7 +4672,7 @@
46724672
/* Maya */
46734673
"Some coins are not available because of the halted chain" = "Some coins are not available because of the halted chain";
46744674

4675-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen." = "Някои от вашите мастерноди може да липсват в този списък — портфейлът не успя да прочете целия ви набор от ключове за гласуване. Завършете синхронизирането и отворете този екран отново.";
4675+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account." = "Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.";
46764676

46774677
/* Usernames */
46784678
"Some usernames can be blocked" = "Some usernames can be blocked";

‎DashWallet/ca.lproj/Localizable.strings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4672,7 +4672,7 @@
46724672
/* Maya */
46734673
"Some coins are not available because of the halted chain" = "Some coins are not available because of the halted chain";
46744674

4675-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen." = "És possible que alguns dels teus masternodes faltin en aquesta llista: la cartera no ha pogut llegir tot el teu conjunt de claus de vot. Acaba la sincronització i torna a obrir aquesta pantalla.";
4675+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account." = "Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.";
46764676

46774677
/* Usernames */
46784678
"Some usernames can be blocked" = "Some usernames can be blocked";

‎DashWallet/cs.lproj/Localizable.strings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4672,7 +4672,7 @@
46724672
/* Maya */
46734673
"Some coins are not available because of the halted chain" = "Some coins are not available because of the halted chain";
46744674

4675-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen." = "Některé z vašich masternode v tomto seznamu možná chybí — peněženka nedokázala přečíst celou vaši sadu hlasovacích klíčů. Dokončete synchronizaci a otevřete tuto obrazovku znovu.";
4675+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account." = "Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.";
46764676

46774677
/* Usernames */
46784678
"Some usernames can be blocked" = "Some usernames can be blocked";

‎DashWallet/da.lproj/Localizable.strings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4672,7 +4672,7 @@
46724672
/* Maya */
46734673
"Some coins are not available because of the halted chain" = "Some coins are not available because of the halted chain";
46744674

4675-
"Some of your masternodes may be missing from this list — the wallet could not read your full voting-key pool. Finish syncing and reopen this screen." = "Nogle af dine masternoder mangler måske på denne liste — tegnebogen kunne ikke læse hele dit sæt af afstemningsnøgler. Afslut synkroniseringen, og åbn denne skærm igen.";
4675+
"Some of your masternodes may be missing from this list because this wallet has no masternode voting key account." = "Some of your masternodes may be missing from this list because this wallet has no masternode voting key account.";
46764676

46774677
/* Usernames */
46784678
"Some usernames can be blocked" = "Some usernames can be blocked";

0 commit comments

Comments
 (0)