feat(gui): masternode and shared masternode management - #68
Open
PastaPastaPasta wants to merge 24 commits into
Open
feat(gui): masternode and shared masternode management#68PastaPastaPasta wants to merge 24 commits into
PastaPastaPasta wants to merge 24 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add isShared(), the typed collateral share table (amount, owner key, refund/reward scripts), and the early-period penalty terms to interfaces::MnEntry so the GUI can recognize and display shared masternodes. Also add Node::isV24Active() so wallet UI can gate shared-masternode actions on hard-fork activation at the tip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared masternodes now display as their own type, get a dedicated type filter (the Regular filter shows only single-owner masternodes), match the text filter by share owner/refund/reward addresses, and count as owned when the wallet holds any share owner key (keyIDOwner is null for shared masternodes and rewards may be redirected away from the wallet). The details dialog gains the collateral share table with per-share amounts, addresses and 'you' markers, the early-exit penalty terms and the remaining early-period countdown. MasternodeEntry::toTie() now also covers payout/voting/operator key and share-table state so reconcile() refreshes rows after ProUpRegTx, ProUpShareTx and ProUpSharedRegTx updates instead of serving stale entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scaffolding for the masternode management dialogs added in the following commits: executes protx (and related) commands via the in-process RPC dispatcher on a dedicated worker thread (executeRpc is synchronous and takes wallet/validation locks, so it must stay off the GUI thread), routes to the dialog's wallet by percent-encoded wallet URI, dispatches with named arguments so the GUI is independent of RPC parameter order, and maps RPC and consensus-reject errors to actionable user messages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A multi-page wizard on the Masternodes tab covering all three registration paths: funding the collateral from the wallet (protx register_fund/_evo), using an existing exact-denomination UTXO held by the wallet (protx register/_evo), and external collateral held in another wallet or on a hardware device (protx register_prepare -> out-of-band message signing -> protx register_submit). The keys page generates fresh owner/voting addresses from the wallet and either generates a basic-scheme operator BLS keypair or accepts a hosting provider's public key. The result page shows the operator secret exactly once behind a type-back confirmation gate together with the matching masternodeblsprivkey dash.conf line. Evonodes get a platform page whose address-list shape follows v24 activation. Also adds reusable widgets: a fee-source picker with per-address spendable balances (special transactions fund from exactly one address), an operator-key widget and service-address-list validation helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Role-aware maintenance actions in the masternode list context menu. Update Service covers regular, evonode (with platform fields via protx update_service_evo) and shared masternodes; for shared masternodes an explicit fee source is required because they have no payout address to fall back to. Update Registrar requires the owner key in the wallet and is never offered for shared masternodes, which rotate keys unanimously instead; it warns that an operator key change PoSe-bans the node until a new ProUpServTx. Revoke offers the four DIP3 reasons with copy explaining it is mainly needed before re-registering with the same address or operator key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creating a shared masternode is an asynchronous multi-wallet flow, so this is a resumable session dialog rather than a wizard. MnShareSession is the widget-free engine holding the session envelope (a JSON file the participants pass around): the share table and terms with a live mirror of the consensus share-validation rules, the chip-model funding transaction assembly with fixed input sequences, and native consent verification - every imported signature is checked against the recomputed consent hash (CHashSigner::VerifyHashCanonical) at import time, so a signature over a stale draft is flagged with who must re-sign instead of surfacing as a consensus error after broadcast. The dialog walks Draft -> Frozen -> Signing -> Combined -> funding signatures -> Broadcast with the consent check-phrase displayed prominently from freeze onward, locks contributed coins in the wallet, re-checks funding inputs on every import and declares the session dead if one was spent, and requires an explicit unfreeze (discarding the collected signatures) to edit frozen terms. The entry button is gated on v24 activation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Context-menu actions on owned shared rows: Change My Reward Address (protx update_share, the only solo update a share owner can make), Rotate Operator/Voting Keys (the unanimous update_shared_registrar_prepare flow, pinned to the wallet whose fee inputs the prepared transaction spends), and Dissolve with three modes: - Dissolve now: unilateral ProDisTx with a live payout preview that mirrors the consensus math exactly (actor receives share minus penalty minus fee, the others receive principal plus a pro-rata bonus). During the early period the penalty must be explicitly accepted, and when the boundary is near the dialog nudges toward waiting or the unanimous mode. payPenalty is passed explicitly so a boundary crossing between preview and submit hard-errors instead of silently charging. - Unanimous: penalty-free at any height, using a lightweight signature envelope with the same native import-time verification as creation (digest recomputed locally and cross-checked against the RPC's signHash). - Standby: generates the labeled PAIR of never-expiring recovery transactions - one broadcastable immediately that always pays the penalty, one penalty-free that only becomes valid at the early-period boundary - because the penalty is baked into the transaction outputs at build time. The Dissolve menu entry warns until both variants have been saved to a file. Dissolution actions are unavailable until the registration has a confirmation (same-block dissolution is consensus-invalid). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the draft-side mirror of IsShareListTriviallyValid, envelope serialization round-trips with network and stage rejection, the unilateral-dissolution payout preview against the consensus split rules, and the import-time signature path: freeze cross-checks the consent hash against the prepared transaction, a valid consent signature is accepted exactly once, and signatures under a wrong index or over a stale digest are rejected without being counted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The share table and terms a participant reviews (the envelope's shares[]
and terms{}) were never checked against the CProRegTx payload the wallet
actually signs. A malicious coordinator could circulate an envelope that
displays benign terms while its prepared transaction assigns a victim's
owner key a small share refunding to the attacker: every internal check
passed (the consent hash is recomputed from the payload and the check
phrase matches for everyone), so the victim would review the shown terms,
sign the payload digest, fund the inputs, and lose money to the terms
that were actually registered.
freeze(), a non-Draft fromJson() and verifySignature() now decode the
payload and require its share table (amount, owner key, refund and reward
scripts) and terms (voting key, operator key, operator reward, early
period and penalty) to equal the envelope's displayed values, so a
mismatched session can never freeze, import or show a valid signature.
Also hardens the engine against related tampering: mergeEnvelope now
validates an adopted further-advanced transaction against the consent
hash instead of copying it blindly (a forged 'broadcast' copy could
otherwise overwrite a good transaction and fake completion), a Draft
merge compares the full share/term/funding state rather than only the
funding transaction, duplicate and out-of-range signature entries are
rejected on import, PenaltyPreviewFor guards against a zero non-actor
total, fromJson rejects out-of-range input indices and negative change,
and validateShares rejects null owner keys (bad-protx-shares-key-null).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review findings on the session dialog: the session is now declared dead only when a funding input is provably spent (a wallet coin reported spent) rather than merely absent from the confirmed UTXO set, so importing before another participant's chip confirms no longer shows a false dead page; contributed coin locks are released on discard, on adopting a superseding imported copy, and from the dead page, so a dismissed session no longer leaks locks forever (Save keeps them for resuming); freezing gates on client-resolvable funding sufficiency and warns on an excessive implied fee; an imported session's agreed operator key is shown read-only and used for freeze instead of being silently replaced by a locally generated key; and imported participant labels render as plain text so a crafted label cannot inject HTML into a message box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unilateral-dissolution tab derived payPenalty from the block height captured when the dialog opened; a penalty-free boundary crossing while the dialog (or its confirmation prompt) was open could pay an avoidable penalty. It now re-reads the tip height at submit and, if the boundary was crossed, refreshes the preview and asks the user to re-review instead of submitting. The liveness check no longer bricks a just-prepared unanimous/rotation session whose own fee input is still unconfirmed, the dialog can no longer be dismissed via Esc or the window close button while an RPC is running, and the standby 'stored' flag is now persisted per variant so saving the two standby transactions across separate dialog sessions still clears the warning. Also dedupes the repeated status-label, sign/combine, and standby A/B scaffolding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dismissal The fee-source picker summed balances by listCoins' parent-address key, which overstated what FundSpecialTx (which selects only coins whose destination equals the fee address) can actually spend; it now groups by each coin's own destination. Update Service for an evonode was impossible before v24 because the dialog only offered the v3 address-list form; it now shows platform port spinboxes pre-activation, matching the wizard. Update Service no longer prefills only the primary service address (which would silently drop registered secondary addresses on submit) and instead asks for the full list. The maintenance dialogs can no longer be dismissed while their RPC runs, the existing-collateral combo is filtered to the P2PKH outputs protx register accepts, the platform page requires both address fields together, and the discard-prepared warning no longer wrongly claims fee inputs stay locked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v24 gate on the Shared Masternode button was dead code: WalletView calls setWalletModel before the client model is wired, so the branch that consulted clientModel->node().isV24Active() always saw a null client model. The enable state is now computed from both models and re-evaluated on every tip change until activation. MasternodeEntry's change-detection tie now includes the ProUpServTx-mutable network and platform address fields so the list refreshes after a service update instead of showing stale data, and the 'Filter by Owner Address' action is disabled for shared rows (whose owner address is the null-key placeholder shared by every shared masternode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PastaPastaPasta
force-pushed
the
decentralized-mn-ui
branch
from
August 12, 2026 14:49
c175fa0 to
1604a68
Compare
The operator BLS key a wallet generates for a masternode registration existed only on screen: the user had to copy it before closing the dialog or lose it. The wallet can now keep it, so a wallet backup covers it like every other key. The secret is stored keyed by its public key, in the clear only while the wallet itself is unencrypted, and encrypted under the wallet's master key otherwise (refusing to store while locked). EncryptWallet converts any plaintext records as part of the same transaction that encrypts the key managers - without that leg, keys stored before the user ran encryptwallet would stay readable in the wallet file forever. The records are deliberately not key types, so a corrupt one cannot abort a wallet load, and not legacy types, so migration does not wipe them. Owner and voting keys needed nothing here: they are ordinary wallet addresses off the HD seed already. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A masternode registration appeared in the Transactions tab as a "Payment to yourself" whose amount was only the network fee, with nothing to identify it — a user who had just registered could not find the transaction at all. Provider special transactions now get their own record types: Masternode Registration, Masternode Update (the service, registrar, revoke, share and shared-registrar updates, which are metadata-only and indistinguishable to a history reader) and Masternode Dissolution. Classification happens before the CoinJoin and self-payment heuristics that used to swallow them, they are labelled and coloured accordingly, and the type filter gains a Masternode entry. The amount is left alone: for a self-funded registration the collateral stays in the wallet, so the fee is the real change in balance, and the tooltip says so. The CoinJoin filter rows are now looked up by their mask instead of hard-coded indices, so adding a row cannot desynchronize them again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feedback on the running wizard: the pages were cramped and the review was a wall of text. Each choice is now a card whose body only expands when it is selected, so the page spends its height on the option being configured instead of three open bodies at once, and the pages share one spacing rhythm. The review is a set of sections mirroring the wizard's own pages, with a label and value column that line up across every card, values selectable and monospaced where they are addresses or keys, and the 96-character operator key wrapped rather than cut off. The closing line now says what the button does. The voting address gains the same "Use new address" button as the owner address, and the keys page says that both come from this wallet's HD seed. The operator key can be generated and saved in the wallet (the new default when the wallet can store it), generated without saving, or supplied as a public key by whoever runs the node; the result page tells the user which happened and only asks them to confirm a hand-copy when the key exists nowhere else. It also points at the Transactions tab, where the registration now appears under its own name. The collateral picker shows why it is empty instead of an empty dropdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stored random operator key is only as recoverable as the wallet file that holds it. An HD wallet can do better: derive the key, and the recovery phrase alone brings it back. The derivation follows DashSync exactly, so a key made here is the key the mobile wallets expect: m/9'/coin'/3'/3'/index over the BIP39 seed, coin type 5 on mainnet and 1 everywhere else, built with dashbls' ExtendedPrivateKey::FromSeed and legacy child derivation at every level, with an unhardened leaf. The known-answer test pins it to the operator key inside DashSync's own expected ProRegTx, so a change that breaks mobile compatibility fails the build rather than stranding a user's key. Seed access spans both wallet types through a new ScriptPubKeyMan pair: legacy reads its decrypted HD chain, descriptor wallets go through the mnemonic. Wallets with no reachable seed - watch-only, external signer, imported descriptors - keep the existing stored-key path. Only the index is written to the wallet, never the secret, and reading a key back re-derives it and checks the public key matches before handing anything out, so a foreign or corrupted index record cannot make the wallet produce a key under a public key it does not control. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wizard's operator key now defaults to being derived from this wallet's recovery phrase whenever the wallet has a seed, and says so: restoring the phrase restores the key. Generating and saving in the wallet remains for wallets that cannot derive, and generating without saving remains for anyone who wants the key to exist nowhere else. The key is derived on reaching the review, so the key shown is the key registered, and the wizard holds that unlock through registration instead of asking twice. The masternode list gains Show Operator Key, which answers the question the old success page raised and never answered: where the saved key lives afterwards. It matches a masternode's registered operator key against the keys this wallet holds, and after an unlock shows the public key, the secret and the ready-made dash.conf line, each with its own Copy button, and states whether the key came from the recovery phrase or the wallet file. Long keys are shown in fixed-width groups as plain selectable values rather than in a text box that looked editable and clipped its contents; Copy always puts the unbroken key on the clipboard. The confirmation gate now appears only when the key really is shown once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drops the stored-key path entirely. It only ever served wallets that can spend but have no reachable seed - descriptor wallets built from a raw hdseed or from imported descriptors, and pre-HD legacy wallets - and for those, showing the key once and letting the owner save it is enough. Removing it deletes two wallet record types, the secret encryption path and the EncryptWallet leg that existed solely to stop those records sitting in plaintext: the riskiest code in the feature, gone rather than guarded. What remains is derivation, extended to work for an operator running several masternodes and for a wallet restored from its recovery phrase: - deriving takes the set of operator keys already registered on-chain and skips any index that would collide with one, so a restored wallet with no records of its own cannot hand out a key another masternode already uses; - looking a key up no longer depends on the wallet's own records at all: when no index record matches, it walks a bounded range of indexes and records the one it finds, so the recovery phrase alone is enough to get an operator key back. Both bounds are the same constant, which is the invariant - an index is never issued that recovery could not find. Tests cover three registrations getting distinct indexes and keys, the restored-wallet collision case, and recovery with no records at all. The DashSync known-answer test is unchanged, which is what keeps the keys interchangeable with the mobile wallets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the Generate and save in this wallet option, leaving derive from the recovery phrase (the default whenever the wallet has a seed), generate without saving, and use someone else's public key. A wallet that cannot derive now says so on the keys page instead of silently offering a weaker option. When deriving, the wizard passes the operator keys already registered on-chain to the wallet, so a wallet restored from its recovery phrase never derives a key an existing masternode is already using. Show Operator Key uses the wallet's cheap have-key check to decide whether to offer itself and reports plainly when a key turns out not to belong to this wallet, since the lookup can now recover keys that have no record in the wallet file. The masternode model gained an accessor for the registered operator key, so the list no longer re-parses its JSON on every right-click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
dash-qt has never had an action-oriented masternode UI — the legacy "start alias" GUI was removed with the legacy masternode code (dashpay#2600), and today's Masternodes tab is a read-only list. Meanwhile dashpay#7437 adds shared (multi-party) masternodes, whose multi-step, multi-wallet RPC flows (
register_shared_prepare→shared_sign→shared_combine→ funding signatures → broadcast) are impractical to drive by hand from the debug console for the non-expert participants they are designed for.Stacked on dashpay#7437. This PR's base is the dashpay#7437 branch, so it shows only the 14 GUI commits; it will be retargeted to
developonce dashpay#7437 merges.What was done?
GUI for creating and maintaining masternodes, evonodes and shared masternodes, stacked on dashpay#7437:
interfaces+ list):MnEntryexposes the shared state (share table, penalty terms); the masternode list gets a "Shared" type, type filter, share-aware text search and owned-row attribution via share owner keys (keyIDOwneris null for shared MNs); the details dialog shows the share table with per-share amounts/addresses, "(you)" markers, penalty terms and the early-period countdown.ProTxSender: worker-thread bridge that drives the wallet-side protx RPC handlers in-process with named arguments (executeRpcis synchronous and takes wallet/validation locks, so it stays off the GUI thread) and translates consensus reject strings into actionable messages. The write path deliberately reuses the RPC layer as its contract instead of extracting feat!: implement Decentralized Masternode Shares DIP dashpay/dash#7437's file-static tx-assembly helpers into typed interfaces — that refactor would enlarge the base PR's review surface and conflict with every feat!: implement Decentralized Masternode Shares DIP dashpay/dash#7437 push while it is still under review; typedinterfaces::Walletprotx virtuals are the intended follow-up once feat!: implement Decentralized Masternode Shares DIP dashpay/dash#7437 merges.register_fund), existing exact-denomination UTXO (register), and external/hardware-held collateral (register_prepare→ out-of-band message signing →register_submit). Generates owner/voting addresses and the operator BLS keypair (or accepts a hosting provider's public key); the operator secret is shown exactly once behind a type-back confirmation with the matchingmasternodeblsprivkeydash.conf line.IsShareListTriviallyValidlive; funding uses a chip model (each participant contributes one output of exactly their share; the coordinator adds the fee input and takes the only change) so the final transaction is verifiable at a glance; every imported signature is verified natively at import time against the recomputed consent hash, so signing a stale draft is caught immediately with who must re-sign, instead of surfacing asbad-protx-shares-sigat broadcast; contributed coins are locked in the wallet and the session is declared dead if a funding input gets spent.Shared-MN entry points are gated on v24 activation at the tip (
Node::isV24Active()), with explanatory tooltips pre-activation.How Has This Been Tested?
test_dash-qtpasses incl. the newMnShareSessionTests.feature_masternode_shares.pywith v24 active and a live 3-share shared masternode; screenshots below.lint-qt-translation,lint-include-guards,lint-format-strings,lint-whitespace,lint-circular-dependenciesall clean.Breaking Changes
None. GUI-only on top of dashpay#7437; no RPC or consensus changes.
Checklist: