fix: correct masternode operator key BLS scheme handling (desync + cross-scheme uniqueness)#7473
fix: correct masternode operator key BLS scheme handling (desync + cross-scheme uniqueness)#7473UdjinM6 wants to merge 4 commits into
Conversation
Potential PR merge conflictsThis is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order. If this PR merges firstThese open PRs will likely need a rebase:
|
|
⛔ Blockers found — Sonnet deferred (commit 3d2d102) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8b1e5fbc0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // stall reachable across the boundary. This is mempool policy, which is allowed to be stricter | ||
| // than consensus: a node that rejects the second transaction still accepts a block containing it, | ||
| // so no chain can split over this. | ||
| if (m_pool.existsProviderTxCrossSchemeConflict(tx)) { |
There was a problem hiding this comment.
Reject cross-scheme conflicts within packages
In the package path this only checks conflicts against transactions that are already resident in the mempool; AcceptMultipleTransactions runs PreChecks for every package member before any of them are finalized into the mempool, so two low-fee ProRegTx parents that claim the same operator key under opposite encodings can both pass here and then be submitted together. After that, block assembly will see neither key at the tip, select both, and BuildNewListFromBlock will reject the template with bad-protx-dup-key, recreating the miner-stall this policy check is meant to prevent for package submissions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, and the analysis is correct: existsProviderTxCrossSchemeConflict only inspects
mapTx, so within a single AcceptMultipleTransactions package two members can each pass
PreChecks against a pool that does not yet contain the other, both get finalized, and then stall
template construction — the same failure mode the ungated single-tx policy is meant to prevent.
To be precise about what this PR does and doesn't do here:
- The package-admission blind spot itself predates this PR.
existsProviderTxConflictimmediately
above (validation.cpp:991) is equally package-blind — it also only seesmapTx, so two package
members claiming the same-encoding duplicate operator key (or duplicate owner/voting key, or
netInfo entry) already slip through today for the same reason. - What this PR adds is a new conflict category — cross-scheme operator-key duplicates — that
inherits that same blindness. So within a package the new policy can be evaded, exactly as the
existing per-encoding checks already can. The mechanism is shared; the category is new.
That said, it is not reachable outside regtest. AcceptMultipleTransactions is reached only via
ProcessNewPackage, which has exactly two callers, both RPC:
testmempoolaccept(rpc/mempool.cpp:184) passestest_accept=true, so it never inserts into the
mempool and cannot create the resident pair this finding needs.submitpackage(rpc/mempool.cpp:841) is the only one that actually submits, and it is-regtest
only (if (!Params().IsMockableChain()) throw "submitpackage is for regression testing (-regtest mode) only").
There is no p2p package-relay path (net_processing.cpp references neither ProcessNewPackage nor
AcceptMultipleTransactions), so a package cannot be assembled by a peer on any real network.
A proper fix is package-local key-claim tracking inside AcceptMultipleTransactions, and to be
consistent it should cover the existing per-encoding provider conflicts too, not just the new
cross-scheme one. That is a broader mempool/package-policy change that reaches beyond this PR's
scope (v24 operator-key scheme handling), so given the regtest-only reachability I'd prefer to
track it as a follow-up covering both the old and new checks rather than fold it into this change.
WalkthroughThe change adds cross-scheme BLS operator-key detection to deterministic masternode lists, consensus validation, and mempool admission. V24 rules reject duplicate keys across encodings and support legacy-BLS upgrades without key rotation. Block assembly revalidates special transactions against current chain state. RPC handling preserves legacy transition behavior. Unit and functional tests cover activation boundaries, duplicate races, stale transactions, state diffs, and list consistency. Sequence Diagram(s)sequenceDiagram
participant MemPoolAccept
participant CTxMemPool
participant BlockAssembler
participant SpecialTxManager
MemPoolAccept->>CTxMemPool: Check cross-scheme provider conflict
CTxMemPool-->>MemPoolAccept: Accept or reject transaction
BlockAssembler->>SpecialTxManager: Revalidate special transaction
SpecialTxManager-->>BlockAssembler: Include or exclude transaction
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/evo_deterministicmns_tests.cpp`:
- Around line 1840-1867: Strengthen the transaction assertions around
ProcessTransaction so tx_b is rejected specifically with the protx-dup reason,
while confirming tx_a remains in the mempool after the attempted conflicting
submission. After v24 activation, inspect the block template produced by
make_template and assert that tx_a is selected, preserving the existing
template-build checks.
In `@src/txmempool.h`:
- Around line 830-840: Update the documentation for
existsProviderTxCrossSchemeConflict() to reflect that its caller,
MemPoolAccept::PreChecks(), invokes it unconditionally. Remove the requirement
that callers gate it on a deployment, and describe the intended
activation-boundary behavior while preserving the explanation of cross-scheme
key comparison.
In `@src/validation.cpp`:
- Around line 995-1008: Update AcceptMultipleTransactions() to track
operator-key claims across all staged package members, normalizing BLS encodings
so opposite schemes conflict; reject a transaction when its claim conflicts with
an earlier staged member before finalizing or inserting the package, while
preserving the existing mempool conflict check for transactions already in
m_pool.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 81458661-845d-45e2-a112-e73e1f5de4c7
📒 Files selected for processing (10)
src/evo/deterministicmns.hsrc/evo/specialtxman.cppsrc/node/miner.cppsrc/node/miner.hsrc/rpc/evo.cppsrc/test/evo_deterministicmns_tests.cppsrc/txmempool.cppsrc/txmempool.hsrc/validation.cpptest/functional/feature_dip3_v19.py
b8b1e5f to
9366bff
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9366bffff1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The exact checkout matches head 9366bff, and the commit history evidence is accurate. The package-admission finding is confirmed: package members are prechecked before provider indexes are updated, then inserted without rerunning the new cross-scheme conflict check, allowing a pair that makes CreateNewBlock fail cumulative block validation.
Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (failed),gpt-5.6-sol— dash-core-commit-history (failed),gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/validation.cpp`:
- [BLOCKING] src/validation.cpp:1006-1008: Package submission bypasses the cross-scheme mempool check
AcceptMultipleTransactions runs PreChecks for every package member before inserting any member into the real mempool. PackageAddTransaction only exposes package outputs through the temporary coins view; it does not update mapProTxBlsPubKeyHashes, so two package members claiming the same operator key under different encodings cannot detect each other here. AcceptPackage can reach this path when a parent fails individual mempool-fee policy and its CPFP child initially has missing inputs. SubmitPackage then finalizes the validated members sequentially without rerunning provider-conflict checks. The currently exposed submitting RPC is restricted to mockable chains, but the package-admission API still violates the invariant introduced by this PR: both transactions become resident, TestPackageTransactions validates each independently against the chain tip, both are selected as an ancestor package, and cumulative deterministic-list rebuilding rejects the second claim during TestBlockValidity, causing CreateNewBlock to throw. Validate provider conflicts cumulatively across the package, or rerun the checks before each Finalize call after earlier members have updated the mempool indexes.
9366bff to
a693a86
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/evo_deterministicmns_tests.cpp`:
- Around line 1858-1907: Update the exception handling around
RebuildListFromBlock so any thrown exception fails the regression test instead
of only populating thrown and skipping assertions. Keep the diagnostic message
if useful, but require successful completion before proceeding with the existing
rebuilt-state checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e80fc521-562d-40d4-aed6-cd801bd50cea
📒 Files selected for processing (11)
src/evo/deterministicmns.hsrc/evo/dmnstate.hsrc/evo/specialtxman.cppsrc/node/miner.cppsrc/node/miner.hsrc/rpc/evo.cppsrc/test/evo_deterministicmns_tests.cppsrc/txmempool.cppsrc/txmempool.hsrc/validation.cpptest/functional/feature_dip3_v19.py
🚧 Files skipped from review as they are similar to previous changes (5)
- src/txmempool.h
- src/node/miner.cpp
- src/node/miner.h
- src/validation.cpp
- src/txmempool.cpp
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The prior blocking finding c83910ef32bd remains valid at a693a86: package members are prechecked against the unchanged real mempool, allowing cross-scheme operator-key conflicts within one package to bypass the new policy check. The commit-history suggestion is not actionable because the adjacent commits remain coherent, tested design iterations rather than a broken intermediate state.
Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (failed),gpt-5.6-sol— dash-core-commit-history (failed),gpt-5.6-sol— general (failed),gpt-5.6-sol— dash-core-commit-history (failed),gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Operator key uniqueness was enforced per BLS encoding rather than per key. mnUniquePropertyMap is keyed by GetUniquePropertyHash(), which serializes its argument, and a BLS key serializes differently under the legacy and basic schemes. So one public key sits in one of two possible slots, and CheckProRegTx's duplicate check -- which consults that map -- could not see the same key presented under the other encoding. A ProRegTx never proves ownership of the operator key, so anyone could re-register an existing masternode's operator public key for the price of a collateral. Rather than re-key the map canonically, which would apply retroactively -- the map is derived, not stored: Unserialize() clears it and rebuilds via AddMN, so a historical cross-scheme pair would make AddMN throw and nodes fail to sync -- probe both encodings at each point where a key can be claimed. There are exactly two schemes, so this is two O(1) lookups rather than a scan, and the consensus rules are gated on v24, leaving historical reconstruction untouched. Checking only the confirmed list is not enough, so probes are placed at every point a key is claimed: - CheckProRegTx and CheckProUpRegTx, against the previous block's list. - RebuildListFromBlock, against the list as rebuilt so far, since per-transaction checks run against pindexPrev and are blind to each other within a block. This rejects cleanly: AddMN()/UpdateMN() report duplicates by throwing, and that throw would escape block-template assembly. - AcceptToMemoryPool, so two in-flight transactions cannot claim one key under different schemes. This one is deliberately NOT gated on v24. Block assembly does not revalidate special transactions cumulatively -- it checks each candidate against the tip, where neither key is yet present -- so a pair admitted before activation is never evicted and would still be selected together afterwards, leaving an honest miner unable to build any template at all. Keeping the pair out of the mempool is what actually closes that, and mempool policy is allowed to be stricter than consensus: a node rejecting the second transaction still accepts a block containing it, so no chain can split over it. The registrar probes run only when the operator key is actually changing, at every layer. An update that keeps its own key cannot create a duplicate, and probing it anyway would let a cross-scheme pair formed before activation permanently block the affected masternode's registrar updates unless it rotated its key -- making an old squat more harmful rather than less. Pairs that already exist before activation are tolerated: nothing rehashes the map, and they remain upgradable because leaving LegacyBLS rotates the key anyway. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopts the maintainer's preferred approach from dashpay#7472 (re-encode the operator key on a version change) instead of forcing a key rotation to leave LegacyBLS, while keeping the cross-scheme uniqueness guards this branch added so the re-key cannot collide and stall block production. A masternode operator can keep the same BLS private key across the legacy->basic migration; only the serialized encoding of the public key changes. Rather than rejecting a same-key ProUpServTx/ProUpRegTx and forcing a rotation (which also PoSe-bans the masternode), SetStateVersion() now re-encodes the stored key to the scheme its version implies, and UpdateUniqueProperty() re-keys the scheme-dependent unique-property map when the encoding changes. The RPCs build a BasicBLS migration payload for a legacy masternode instead of erroring. Because dashpay#7472's re-encode collides -- and UpdateMN() throws out of block assembly -- when a squatter already holds the same key under the other encoding (the live per-encoding registration hole), migration is guarded: CheckProUpServTx, CheckProUpRegTx and RebuildListFromBlock reject a migration that would collide with another masternode's key under either scheme (bad-protx-dup-key), and only when the key actually changes or the version crosses the scheme boundary, so a grandfathered cross-scheme pair's non-migrating routine update is not blocked. CDeterministicMNStateDiff also has to capture the re-encoding: its field comparison used CBLSLazyPublicKey::operator==, which ignores the scheme, so a same-key migration produced a diff that omitted the key. A node reconstructing the list from evoDB diffs then kept the old encoding while an online-built list had the new one -- a reconstruction split that full-snapshot serialization does not reveal. The diff now compares the scheme-dependent hash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ness Unit tests in evo_deterministicmns_tests.cpp and a functional test extension in feature_dip3_v19.py for the two fixes on this branch: cross-scheme operator-key uniqueness and in-place legacy->basic migration that keeps the same key. Each rejection test was watched fail first by flipping the v24 activation height. Covers the desync and evoDB-diff reconstruction paths, the mempool and same-block cross-scheme pairings, the migration collision guards, the block-template stale special-tx recheck, the HasOperatorKeyUnderAnyScheme helper, and the pre-v24 non-retroactivity guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a693a86 to
3d2d102
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/rpc/evo.cpp`:
- Around line 1259-1275: Add an explicit validation in the update-registrar
flow, before the existing dmn->pdmnState->nVersion == ProTxVersion::LegacyBLS
block, to reject use_legacy when the masternode version is BasicBLS or newer.
Return the established RPC validation error for this invalid request and leave
the existing legacy-key handling unchanged for LegacyBLS masternodes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 162df6bd-a361-4c18-aca8-9a0684a6da3e
📒 Files selected for processing (12)
doc/release-notes-7473.mdsrc/evo/deterministicmns.hsrc/evo/dmnstate.hsrc/evo/specialtxman.cppsrc/node/miner.cppsrc/node/miner.hsrc/rpc/evo.cppsrc/test/evo_deterministicmns_tests.cppsrc/txmempool.cppsrc/txmempool.hsrc/validation.cpptest/functional/feature_dip3_v19.py
🚧 Files skipped from review as they are similar to previous changes (10)
- src/node/miner.h
- src/node/miner.cpp
- src/evo/dmnstate.h
- src/validation.cpp
- src/txmempool.h
- src/evo/deterministicmns.h
- test/functional/feature_dip3_v19.py
- src/txmempool.cpp
- src/evo/specialtxman.cpp
- src/test/evo_deterministicmns_tests.cpp
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
Prior finding c83910ef32bd is STILL_VALID: package submission can admit two cross-scheme operator-key claims because all package members complete PreChecks before any member updates the real mempool provider indexes. The author's reachability reply correctly limits current non-test insertion to the regtest-only submitpackage RPC, but the defect remains in scope because this PR adds the shared conflict check to prevent resident transactions from poisoning block templates, and the reachable package path can still make CreateNewBlock throw. The CodeRabbit report is not actionable because it misreads the payload version selection and otherwise concerns pre-existing legacy-RPC behavior.
Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (failed),gpt-5.6-sol— dash-core-commit-history (failed),gpt-5.6-sol— general (failed),gpt-5.6-sol— dash-core-commit-history (failed),gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Issue being fixed or feature implemented
Two independent bugs in how a masternode's operator BLS key relates to its state
version. Both exist on
developtoday and are reproduced by test; neither isintroduced by any open PR.
(A) Post-v24 operator-key scheme desync → consensus split.
CDeterministicMNStatederives its operator key's serialization fromnVersion(
CBLSLazyPublicKeyVersionWrapper(key, nVersion == LegacyBLS)). But after v24 amasternode's
nVersioncould rise from LegacyBLS (v1) to BasicBLS (v2) while thestored key kept the legacy scheme flag:
ProUpServTxcarries no operator key, yet setnewState->nVersionfrom thepayload.
ProUpRegTxre-submitting the same key basic-encoded leftoperator_changedfalse, because
CBLSLazyPublicKey::operator==compares the public key and ignoresthe scheme, so the re-encode was skipped while the version still rose.
Once a state is
nVersion == 2with a legacy-flagged key, a list rebuilt from blockdiffs and the same list reloaded from disk hash that key differently, so
mnUniquePropertyMapdiverges. Nodes then disagree onHasUniqueProperty()andtherefore on
bad-protx-dup-key— a chain split determined purely by restarthistory.
update_serviceis the routine operation that plants this: any legacymasternode updating its service address after v24 would trigger it, no attacker
required.
(B) Operator-key uniqueness enforced per encoding, not per key (live today).
mnUniquePropertyMapis keyed byGetUniquePropertyHash(), which serializes itsargument, and a BLS key serializes differently under the two schemes, so
H(K, legacy) != H(K, basic).CheckProRegTx's duplicate check consults that map,so it does not see an operator key an existing masternode holds under the other
encoding. A
ProRegTxnever proves ownership of the operator key, so anyone canre-register an existing masternode's operator public key for the price of a
collateral. This is exploitable on mainnet now; it is closed here for post-v24.
What was done?
The approach is migrate, don't force rotation (adopting the maintainer's
preferred model from #7472), while keeping the cross-scheme uniqueness guards that
make the re-encode safe. Every new consensus rule is gated on
DEPLOYMENT_V24.A legacy masternode keeps the same operator private key across the migration; only
the serialized encoding of the public key changes. On a version bump,
SetStateVersion()re-encodes the stored key to the scheme its version implies(
Set(Get(), …), notSetLegacy(), so the cached serialization actually changes),and
UpdateUniqueProperty()re-keys the scheme-dependent unique-property map whenthe encoding changes (it now compares
GetUniquePropertyHash()rather than thescheme-blind
operator==).Re-encoding is only safe if the target slot is free:
UpdateMN()reports a duplicateby throwing, and that throw escapes
BlockAssembler::CreateNewBlock, stalling blockproduction. Because bug B leaves the per-encoding registration hole open, a squatter
can already hold the key under the other encoding — so the migration is guarded at
every layer, and only when the key actually changes or the version crosses the
legacy→basic boundary (a grandfathered cross-scheme pair's non-migrating routine
update is not blocked).
The unique-property map is deliberately not made canonical. It is derived, not
serialized:
CDeterministicMNList::Unserializeclears it and rebuilds viaAddMN. Acanonical hash would apply retroactively to all history and, if any cross-scheme
duplicate pair already exists, make
AddMNthrow and nodes fail to sync past it.Cross-scheme pairs created before activation are therefore tolerated (nothing
rehashes the map) and remain upgradable.
The change is split into two source commits, a test commit, and a release-notes
commit; each source commit builds on its own.
Commit 1 — enforce uniqueness across schemes. A helper
HasOperatorKeyUnderAnySchemeprobes both encodings (two O(1) lookups, not a scan),wired in at every point a key can be claimed:
CheckProRegTxandCheckProUpRegTx, against the previous block's list.RebuildListFromBlock, against the list as rebuilt so far (same-block pairs).AcceptToMemoryPool. This probe is deliberately not v24-gated: block assemblydoes not revalidate special transactions cumulatively, so a pair admitted before
activation is never evicted and would poison every template afterwards. Keeping the
pair out of the mempool is what actually closes that, and mempool policy is allowed
to be stricter than consensus (a node rejecting the second transaction still accepts
a block containing it, so no chain can split over it).
The registrar probes run only when the operator key is actually changing: an update
that keeps its own key cannot create a duplicate, and probing it anyway would let a
pre-activation cross-scheme pair permanently block the affected masternode's
registrar updates.
Commit 2 — allow legacy→basic migration without key rotation. Handles bug A by
migrating in place rather than forcing a rotation (adopting #7472's re-encode model):
SetStateVersion()re-encodes the stored operator key to the scheme its versionimplies (
Set(Get(), …), notSetLegacy(), so the cached serialization actuallychanges), keeping the invariant that a stored key's encoding always matches its
nVersion;UpdateUniqueProperty()re-keys the scheme-dependent map on an encodingchange (comparing
GetUniquePropertyHash()rather than the scheme-blindoperator==).HasOperatorKeyUnderAnySchemeinCheckProUpServTx,CheckProUpRegTxandRebuildListFromBlock— the last againstthe list as rebuilt so far, since per-transaction checks run against
pindexPrevand are blind to an earlier transaction in the same block — so a re-encode that
would collide with another masternode's key cannot throw out of block assembly. The
guard fires only when the operator key changes or the version crosses the
legacy→basic boundary (a shared
IsSchemeMigrationpredicate), so a grandfatheredcross-scheme pair's non-migrating update is not blocked.
CDeterministicMNStateDiffnow compares the scheme-dependent hash for the operatorkey field. Its comparison used
operator==(scheme-blind), so a same-key migrationproduced a diff that omitted the key — a node reconstructing the list from evoDB
diffs kept the old encoding while an online-built list had the new one, a
reconstruction split that full-snapshot serialization does not reveal.
rpc/evo.cpp):protx update_service/protx update_registrarbuild aBasicBLS migration payload for a legacy masternode (keeping the same key, or
supplying a new one), instead of erroring or forcing a rotation. The legacy-BLS RPC
variants keep the masternode on the legacy scheme.
Commit 3 — tests. Unit tests in
evo_deterministicmns_tests.cppand thefunctional-test extension in
feature_dip3_v19.pycovering both fixes (enumeratedbelow).
Commit 4 — release notes.
doc/release-notes-7473.mddocumenting the post-v24per-key uniqueness enforcement and the in-place migration behavior of
protx update_service/protx update_registrar.How Has This Been Tested?
Built with
--enable-werror; the full unit suite (./src/test/test_dash), thetouched functional test, and lints all pass. The two source commits build on their
own; the test commit adds the coverage below, and the full
evo_dip3_activation_testssuite passes.
Unit tests (
src/test/evo_deterministicmns_tests.cpp) — each rejection test waswatched fail first (by flipping the v24 activation height):
proupserv_migrates_legacy,proupreg_migrates_legacy_same_key— the happy-pathmigrations: a legacy masternode raises its version keeping the same operator key,
the stored key re-encodes to basic, and the masternode is not PoSe-banned.
migration_rejected_when_key_squatted— a squatter already holds the key under theother encoding; the migration is rejected
bad-protx-dup-keyat the per-tx layerrather than throwing.
same_mn_same_block_migration_consistent,same_mn_same_block_version_crossing_key_rotation— same-masternode transitionswithin one block reconstruct identically.
same_block_cross_scheme_key_pair_rejected,mempool_rejects_cross_scheme_key_race,pre_v24_cross_scheme_pair_cannot_become_resident— the same-block, mempool, andactivation-boundary pairings that motivated the rebuild-time and ungated mempool
probes.
proregtx_rejects_cross_scheme_key_reuse,proupreg_rejects_cross_scheme_key_reuse— bug B at the registration and registrar paths, both directions, with fresh-key and
self-key non-false-positive cases.
statediff_captures_operator_key_reencoding— the evoDB-diff reconstruction path:a same-key migration must emit the re-encoded key in the diff.
has_operator_key_under_any_scheme— the two-encoding lookup helper.stale_special_tx_does_not_poison_template— a resident-but-invalid special txseeded via
addUnchecked; without the package recheckCreateNewBlockthrows.pre_v24_behaviour_unchanged— the non-retroactivity guard: asserts the new rulesare inert before v24, so nothing on live mainnet/testnet changes.
Functional test (
test/functional/feature_dip3_v19.py): extended to activate v24 andassert that a legacy masternode's
update_servicemigrates it to the basic scheme inplace (v2 payload,
state.version == 2, same operator key re-encoded to a differenthex,
PoSeBanHeight == -1), and that the list reloads from disk identically after themigration.
Breaking Changes
All consensus rules here are gated on
DEPLOYMENT_V24, which isNEVER_ACTIVEonmainnet and testnet, so there is no consensus change on any live network;
pre_v24_behaviour_unchangedasserts this. After v24 activates:bump (
update_serviceorupdate_registrar), keeping the same operator key; thestored public key is re-encoded legacy→basic. No forced key rotation, no PoSe ban.
is rejected regardless of which BLS encoding either side uses, and a migration that
would collide with such a key is rejected
bad-protx-dup-keyrather than stallingblock assembly.
The
AcceptToMemoryPoolcross-scheme check is mempool policy (ungated) rather thanconsensus, so a node may reject a second in-flight transaction that another node's
mempool accepted; both still accept a block containing it.
Not addressed here, and worth stating: cross-scheme duplicate pairs created before
activation are tolerated, not resolved; and bug B is only closed post-v24, so the
registration hole remains open on mainnet until v24 activates.
Checklist: