Skip to content

Commit 01b89ef

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. A wallet without a provider account yet shows an empty pool, and `poolIsLive` stays false so the joins say the answer may be incomplete. `SwiftDashSDKHost.derivationWallet()`, the throwaway key-wallet these screens and CrowdNode used, has no callers left and is deleted.
1 parent 3665b1d commit 01b89ef

4 files changed

Lines changed: 25 additions & 134 deletions

File tree

‎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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ 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
96+
/// voting address pool could not be read from the running wallet *and*
97+
/// at least one active registration went unmatched, so a node
9898
/// this wallet really can vote with may be missing from `nodes`. The UI
9999
/// says so rather than presenting a short list as the full picture.
100100
struct Resolution {
@@ -163,8 +163,8 @@ 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.
166+
// Only an unread pool can hide a node we own. If every active
167+
// registration matched, the pool did not matter.
168168
let mayBeIncomplete = !resolution.poolIsLive && nodes.count < eligible.count
169169
Self.logger.info(
170170
"🗳️ VOTING :: votable nodes=\(nodes.count, privacy: .public) of \(eligible.count, privacy: .public) active registrations livePool=\(resolution.poolIsLive, privacy: .public)")

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

Lines changed: 17 additions & 100 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,41 +228,18 @@ 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
233+
self.poolIsLive = !poolAddresses.isEmpty
271234
}
272235

273-
/// `true` when ``poolAddresses`` came from the running wallet's live pool.
236+
/// `true` when the running wallet had a provider pool to read.
274237
///
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.
238+
/// `false` means ``poolAddresses`` is empty. Consumers that merely *look
239+
/// up* an index find nothing, and consumers that **enumerate** the pool
240+
/// to decide what exists — the owner/voting address joins — must surface
241+
/// that the answer may be incomplete rather than present an empty list as
242+
/// the whole truth.
281243
let poolIsLive: Bool
282244

283245
/// Snapshot the running wallet's provider address pool — the one SPV
@@ -306,47 +268,6 @@ final class MasternodeProviderKeyDeriver {
306268
return addresses
307269
}
308270

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-
350271
func wif(at index: UInt32) -> String? {
351272
keyResolver.key(at: index)?.privateKeyWIF
352273
}
@@ -355,11 +276,7 @@ final class MasternodeProviderKeyDeriver {
355276
keyResolver.key(at: index)?.privateKeyHex
356277
}
357278

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.
279+
/// The address at `index` of the running wallet's provider pool.
363280
func address(at index: UInt32) -> String? {
364281
poolAddresses[index]
365282
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,10 @@ struct MasternodeKeyUsage {
165165

166166
/// The address join plus whether it was made against the complete pool.
167167
///
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.
168+
/// `poolIsLive == false` means the running wallet had no provider pool to
169+
/// read, so an address that is genuinely ours is missing from `map`.
170+
/// Callers that present the result as "these are all of them" must say
171+
/// so; a caller that only labels known rows can ignore it.
173172
static func resolveAddressIndexes(
174173
family: MNKey,
175174
targets: Set<String>

0 commit comments

Comments
 (0)