Skip to content

fix: proper migration for legacy bls keys to the protx v>=2#7472

Draft
knst wants to merge 19 commits into
dashpay:developfrom
knst:fix-providertx-versions-2
Draft

fix: proper migration for legacy bls keys to the protx v>=2#7472
knst wants to merge 19 commits into
dashpay:developfrom
knst:fix-providertx-versions-2

Conversation

@knst

@knst knst commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

depends on #7302 to prevent conflicts ;

but changes are appliable for develop, issue is reproduced on develop too.

Issue being fixed or feature implemented

Issue has been spotted during reviewing #7302, see original comment:

Blocking: Normalize and re-key the operator key during v1-to-v3 upgrades

A post-v24 ProUpServTx can upgrade a v1 state to v3 without changing pubKeyOperator, and a same-key ProUpRegTx follows the same pattern. SetStateVersion() migrates payouts and netInfo but leaves CBLSLazyPublicKey encoded as legacy; the early return preserves that mismatch. Operator-key uniqueness is indexed by the wrapper's serialized GetHash(), while UpdateUniqueProperty() skips updates when two wrappers represent the same mathematical key. A v3 registration using the basic encoding of that existing key can therefore evade bad-protx-dup-key. Snapshot serialization can later normalize the wrapper without rebuilding the index, making acceptance dependent on reconstruction history. Re-encode the key for the target version, re-key the unique-property map when its serialized hash changes, and test duplicate rejection before and after reload.

What was done?

Keep the operator key's BLS encoding a deterministic function of nVersion, matching the SML and
on-disk serialization, so the unique-property index is re-keyed to the same scheme on every node
whether the list was built online or reloaded from a snapshot. Done before the early return because
callers pre-set nVersion, which would otherwise skip a needed re-encode. Runs only under the
v24-gated version transition, so pre-v24 blocks are untouched.

How Has This Been Tested?

Firstly, added new regression tests.

Secondly, there's mockup python script that reproduces issue (first comment on PR). This script is not meant to be merged, because its functionality is covered by regression tests

Breaking Changes

Fixes Dash Core v24 introduced issue, has not been released yet.

Checklist:

Go over all the following points, and put an x in all the boxes that apply.

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

knst added 18 commits July 14, 2026 23:23
…dation

This commit replaces usage of helpers GetPayload+IsTrivialValid to GetValidatedPayload
It unifies validation between regression tests and production code and
useful for the next commits in PR
CProRegTx and CProUpServTx used to be the only type of protx that have
a different version. It is theoretically acceptable in assumption that
there is no new features or version will ever be introduced for protx
special transaction.

Though, for better compatibility for futher version, unification, simplicity
of documentation and to reduce user's confusions for after-v24 version
of CProUpRegTx and CProUpRevTx are allowed to be "ext addresses" even they
don't have any network related fields _at the moment_

So, since now:
 - version 1: legacy BLS, extended addresses disallowed (pre v19 fork)
 - version 2: basic BLS, extended addresses disallowed (since v19 fork)
 - version 3: basic BLS, extended addresses allowed, multi-payouts allowed (since v24 fork)

NOTE: there are also classes CSimplifiedMNListEntry and CDeterministicMNState
use the same enum for its version; moreover CDeterministicMNState inherits
version directly from CProRegTx. This refactoring doesn't contradict or conflict
this behavior
It helps to drop multiple circular dependencies for providertx <-> validation.h as a side effect
@thepastaclaw

thepastaclaw commented Jul 17, 2026

Copy link
Copy Markdown

⛔ Blockers found — Sonnet deferred (commit 5f66192)
Canonical validated blockers: 1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94f3129802

ℹ️ 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".

Comment thread src/evo/specialtxman.cpp
// v24-gated version transition, so pre-v24 blocks are untouched.
if (state_mn.pubKeyOperator != CBLSLazyPublicKey()) {
const auto pubkey{state_mn.pubKeyOperator.Get()};
state_mn.pubKeyOperator.Set(pubkey, nVersion == ProTxVersion::LegacyBLS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Force diffs to carry BLS re-encodes

When a v24 update keeps the same operator key, this line changes only the CBLSLazyPublicKey encoding; however CDeterministicMNStateDiff still decides whether to include Field_pubKeyOperator via operator!=, which treats legacy/basic encodings of the same point as equal. BuildDiff therefore omits the key, so nodes reconstructing the MN list from stored/P2P diffs set nVersion to v3 but keep the legacy-encoded key and legacy unique-property hash, reintroducing the reconstruction-dependent duplicate-key behavior this patch is trying to remove.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b2ca7817-0945-4544-be93-b4942bdab0b7

📥 Commits

Reviewing files that changed from the base of the PR and between 94f3129 and 5f66192.

📒 Files selected for processing (1)
  • src/rpc/evo.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/rpc/evo.cpp

Walkthrough

The PR centralizes ProTx version selection and owner-payout extraction around ProTxVersion::ExtAddr. It updates payout serialization, JSON/RPC output, payment processing, bloom filtering, and deterministic-node interfaces to use state-based payout access. Validation separates deployment-based payload limits from trivial transaction checks. State transitions migrate legacy payouts and re-encode operator keys while preserving unique-property indexing. Tests cover v19/v24 versioning, payout encoding, collateral reuse, and key re-indexing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RPC
  participant GetValidatedPayload
  participant ProTxState
  participant PayoutConsumers
  RPC->>GetValidatedPayload: construct and validate ProTx payload
  GetValidatedPayload->>ProTxState: apply version and payout state
  ProTxState->>PayoutConsumers: expose state-based owner payouts
  PayoutConsumers-->>RPC: serialize or display payout destinations
Loading

Possibly related PRs

  • dashpay/dash#7340: Both update provider payout extraction in bloom-filter processing and related owner-payout handling.

Suggested reviewers: pastapastapasta

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: migrating legacy BLS keys during ProTx version upgrades.
Description check ✅ Passed The description is directly about the same migration bug, fix, and regression tests described in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/validation.cpp (1)

5831-5838: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer the ChainstateManager overload here. It keeps both deployment checks on the same path and avoids mixing GetConsensus() with chainman.

🤖 Prompt for 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.

In `@src/validation.cpp` around lines 5831 - 5838, Update DeploymentToProtxVersion
to use the ChainstateManager overload of DeploymentActiveAfter for the
DEPLOYMENT_V19 check, passing chainman consistently as already done for
DEPLOYMENT_V24; preserve the existing is_basic_override behavior.
🤖 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`:
- Line 1106: Update the ptx.nVersion assignment in the relevant RPC flow to
retain BasicBLS for legacy service updates after v24 instead of unconditionally
using DeploymentToProtxVersion(). Preserve the higher-version selection for
applicable non-legacy updates so SignAndSendSpecialTx() receives a valid
transaction version.
- Around line 1228-1239: Update the empty operator-key handling in the RPC
method using nVersion so a reused legacy operator key is decoded and re-encoded
with the target version when nVersion is ExtAddr, while preserving the same key.
Do not copy the legacy encoding unchanged; ensure the resulting key satisfies
the existing version invariant.

---

Nitpick comments:
In `@src/validation.cpp`:
- Around line 5831-5838: Update DeploymentToProtxVersion to use the
ChainstateManager overload of DeploymentActiveAfter for the DEPLOYMENT_V19
check, passing chainman consistently as already done for DEPLOYMENT_V24;
preserve the existing is_basic_override behavior.
🪄 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: 150955fa-ddc8-4072-95fa-5096a05accef

📥 Commits

Reviewing files that changed from the base of the PR and between ca2912d and 94f3129.

📒 Files selected for processing (28)
  • src/common/bloom.cpp
  • src/evo/core_write.cpp
  • src/evo/deterministicmns.cpp
  • src/evo/deterministicmns.h
  • src/evo/dmnstate.cpp
  • src/evo/dmnstate.h
  • src/evo/netinfo.cpp
  • src/evo/providertx.cpp
  • src/evo/providertx.h
  • src/evo/providertx_util.cpp
  • src/evo/specialtx_filter.cpp
  • src/evo/specialtxman.cpp
  • src/evo/specialtxman.h
  • src/evo/types.h
  • src/masternode/payments.cpp
  • src/node/interfaces.cpp
  • src/rpc/evo.cpp
  • src/rpc/masternode.cpp
  • src/test/data/trivially_invalid.json
  • src/test/evo_deterministicmns_tests.cpp
  • src/test/evo_trivialvalidation.cpp
  • src/validation.cpp
  • src/validation.h
  • test/functional/feature_masternode_payout_shares.py
  • test/functional/feature_protx_version.py
  • test/functional/rpc_netinfo.py
  • test/functional/test_runner.py
  • test/lint/lint-circular-dependencies.py

Comment thread src/rpc/evo.cpp
Comment thread src/rpc/evo.cpp
@knst

knst commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Extra python script to reproduce issue [it is reproducible on top of develop as well]:

repro_protx_dupkey_reload_divergence.py

not an artefact to merge so far as replaced by regression tests

@UdjinM6

UdjinM6 commented Jul 17, 2026

Copy link
Copy Markdown

I noticed this issue too while reviewing #7302 and started with the same approach initially but this can actually stall block production, pls see #7473 for an alternative solution.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The reused-key RPC path is fixed at this head, but the BLS re-encoding migration is not preserved in deterministic masternode state diffs. Reconstructing a list from a snapshot and persisted diffs can therefore produce a different operator-key uniqueness index and different block-validation results. The three migration commits should also be made atomic before merge.

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 (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 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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/evo/specialtxman.cpp`:
- [BLOCKING] src/evo/specialtxman.cpp:61-63: Persist the encoding-only operator-key change in state diffs
  These lines change a legacy BLS wrapper to basic encoding while preserving the decoded point. `CDeterministicMNStateDiff` uses `operator!=`, which treats wrappers containing the same point as equal, so `BuildDiff()` omits `Field_pubKeyOperator` even though the unique-property hash changed. Replaying the persisted diff from the preceding snapshot updates `nVersion` and `netInfo` but retains the legacy wrapper and legacy uniqueness-map entry. A basic-encoded ProRegTx or ProUpRegTx reusing that point can then pass `HasUniqueProperty()` on the reconstructed node while a node retaining the live list rejects it, making consensus validation depend on cache or restart history. Record encoding changes in the state diff and add a test that reconstructs the migrated list by applying the serialized block diff to a pre-migration snapshot.

In `<commit:48d3735>`:
- [SUGGESTION] <commit:48d3735>:1: Do not place failing regression tests before their fix
  Commit 48d373562f7 adds tests that fail at that commit because operator-key re-encoding and uniqueness-map re-keying arrive only in 94f31298021. The subsequent 5f661926e17 `fixup` commit then completes the same change in the registrar RPC path. Reorder the implementation before the tests or squash these three commits into one atomic migration fix, with the explicit fixup folded into the substantive implementation commit.

Comment thread src/evo/specialtxman.cpp
Comment on lines +61 to +63
if (state_mn.pubKeyOperator != CBLSLazyPublicKey()) {
const auto pubkey{state_mn.pubKeyOperator.Get()};
state_mn.pubKeyOperator.Set(pubkey, nVersion == ProTxVersion::LegacyBLS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Persist the encoding-only operator-key change in state diffs

These lines change a legacy BLS wrapper to basic encoding while preserving the decoded point. CDeterministicMNStateDiff uses operator!=, which treats wrappers containing the same point as equal, so BuildDiff() omits Field_pubKeyOperator even though the unique-property hash changed. Replaying the persisted diff from the preceding snapshot updates nVersion and netInfo but retains the legacy wrapper and legacy uniqueness-map entry. A basic-encoded ProRegTx or ProUpRegTx reusing that point can then pass HasUniqueProperty() on the reconstructed node while a node retaining the live list rejects it, making consensus validation depend on cache or restart history. Record encoding changes in the state diff and add a test that reconstructs the migrated list by applying the serialized block diff to a pre-migration snapshot.

source: ['codex']

UdjinM6 added a commit to UdjinM6/dash that referenced this pull request Jul 18, 2026
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>
@knst
knst marked this pull request as draft July 21, 2026 07:12
@knst

knst commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Likely #7473 to merge instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants