Skip to content

fix(drive-abci)!: keep the operator identity when Core re-encodes the same operator key - #5461

Open
shumkov wants to merge 1 commit into
v5.0-devfrom
shumkov/keep-operator-identity-on-key-re-encode
Open

shumkov wants to merge 1 commit into
v5.0-devfrom
shumkov/keep-operator-identity-on-key-re-encode

Conversation

@shumkov

@shumkov shumkov commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Once Dash Core's DEPLOYMENT_V24 is active, the first ProUpServTx or ProUpRegTx of a masternode raises its state version. If the state still holds its operator key in the legacy BLS scheme, Core re-serializes the same key in the basic scheme. Only the 48 bytes change, but protx listdiff reports pubKeyOperator as changed.

Platform treated that as a new operator. It derived a new operator identity id from the new bytes (sha256(proTxHash ‖ key bytes)), created that identity, and disabled the BLS and node-id keys of the existing one. The existing identity kept its payout key and balance. Every masternode registered with a legacy-scheme operator key would get a second, empty operator identity the first time it updates after V24.

Nothing breaks or halts, but the operator identity, its keys and its balance end up split across two identities. V24 is not active on mainnet or testnet, so this has not happened on either network.

What was done?

  • update_state_masternode_list v0 and v1 now call a shared helper, drop_unchanged_operator_keys, right after fetching Core's diff (not when the list is built from scratch). It clears pub_key_operator from an updated entry when the reported bytes:

    • are the basic-scheme encoding of the key the stored bytes encode in the legacy scheme, or
    • equal the stored bytes,

    and only while V24 is active at the block's Core height.

  • What this does downstream:

    • The masternode list keeps the stored bytes, which the operator identity id is derived from. That keeps later operator payout, node id and key changes, and removals, reaching the same identity.
    • The operator identity, its keys and its balance stay as they are.
    • Every other field of the entry applies as before, and so does every other key change.
  • V24 gate. It reads the coinbase payload version of the chain-locked block at core_block_height. Core requires version 4 exactly at the blocks where V24 and V20 are active, and V20 is active on every chain Platform runs on.

    • It reads immutable block data.
    • Core is asked only when an entry would be cleared, at most once per block.
    • It uses the new CoreRPCLike::get_coinbase_payload_version, which shares the getblockhash + getspecialtxes fetch of get_credit_pool_balance. Drive already probes that fetch at startup.
    • A coinbase without a payload is an error, not "inactive".
  • Before V24 every reported key is applied exactly as before, so blocks already executed replay to the same result.

  • Known ambiguity, accepted. The bytes do not say which scheme a stored key is in. A basic-scheme key with its sign bit clear also decodes in the legacy scheme, to its negation. So once V24 is active, an owner changing such a key to its negation keeps the operator identity. Whoever holds the secret of one key holds the secret of the other.

  • The stored operator key now depends on the list's history. A list rebuilt from Core's full listing holds Core's current bytes instead. Any future rebuild, such as state sync (feat(drive-abci)!: state sync via ABCI snapshots with reduced platform state (protocol v15) #4648), has to recover the stored bytes.

  • Clients that derive the operator identity id from the key bytes Core currently lists should also try the legacy encoding of the same key. The wallet change is fix(platform-wallet): find a masternode's operator identity after Core re-encodes its operator key #5462.

In-place changes to shipped generations

Generation Selected by Change
update_state_masternode_list v0 (update_masternode_list 0) protocol versions 1-13 (shipped, active on mainnet and testnet) calls drop_unchanged_operator_keys
update_state_masternode_list v1 (update_masternode_list 1) protocol version 14 (unreleased, not active on any network) same

The v0 edit is a deliberate exception to the rule for shipped generations, not an inert change. It changes protocol version 13 consensus for one input: a block whose Core height has V24 active. No such block exists on mainnet or testnet:

  • Mainnet: DEPLOYMENT_V24 is NEVER_ACTIVE in Dash Core v24's chainparams.
  • Testnet: the tip coinbase still carries CbTx version 3; version 4 is required once V24 is active.

So every block either network has executed replays unchanged. Before V24 the helper changes no entry, whatever Core reports; the only addition is a Core call when an entry would be cleared. Every other versioned method (update_operator_identity v0, update_masternode_identities v0/v1, the identity id derivations) is unchanged.

Deployment rule (for the rc.4 notes)

Every node that executes Platform blocks must run this release (5.0.0-rc.4 or later) before DEPLOYMENT_V24 activates on its network's Core. That includes validators, other evonodes and full/DAPI nodes. A node without this change computes a different app hash at the first block where Core re-encodes an operator key. Until V24 activates, old and new binaries agree.

  • Testnet: all nodes on rc.4 or later before testnet V24 can activate. The tip coinbase must still be CbTx version 3 at deploy time.
  • Mainnet: all nodes on a release with this change before a Core release turns V24 on for mainnet.

How Has This Been Tested?

The fixtures are real Core output: protx listdiff recorded from the official Dash Core v24.0.0-rc.3 binary on regtest (tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/).

  • Four masternodes were registered with legacy operator keys before V19, with V24 activating at Core height 400.
  • They cover:
    • a re-encode through a ProUpServTx, legacy sign bit clear;
    • a re-encode through a ProUpRegTx with a new voting address, sign bit set;
    • a real key change;
    • a revocation;
    • all of the above as one diff.

New tests, each at protocol versions 13 and 14 through update_masternode_list, as block execution calls it:

  • should_keep_the_operator_identity_of_a_legacy_key_core_re_encodes, with the recorded steps one per Core height and as one diff. Checks:
    • the stored legacy bytes;
    • the kept identity, with all keys enabled;
    • no identity for the basic bytes;
    • the applied voting address and its voter identity;
    • the real key change and the revocation switching identities as before.
  • should_switch_the_operator_identity_on_a_re_encoded_key_while_v24_is_not_active: the same recorded diff with coinbase version 3 behaves as before.
  • should_reach_the_kept_operator_identity_after_a_re_encode_and_a_restart: a later key change and a removal reach the kept identity. A store/reload of the platform state after the re-encode ends with the same GroveDB root hash.
  • Negated-key gate tests, before and after V24.
  • Unit tests:
    • the predicate, on the recorded pairs and on malformed, null, out-of-field, infinity and wrong-length inputs;
    • the helper: only matching entries are cleared, Core is asked once and only when needed, and a Core error or a missing payload fails;
    • the coinbase payload version reader.

Red → green:

  • Before the fix, 3 of the new tests failed:

    • should_keep_the_operator_identity_of_a_legacy_key_core_re_encodes;
    • should_keep_the_operator_identity_for_a_negated_key_once_v24_is_active;
    • should_reach_the_kept_operator_identity_after_a_re_encode_and_a_restart.

    They failed because the re-encoded bytes were stored and a second identity was created.

  • After the fix they pass.

  • The gate tests cannot fail on the old code by design. Removing the gate from the helper turns 3 of them red, and making the helper clear every keyed entry turns 7 tests red.

Also run:

  • cargo test -p drive-abci --lib: 4279 passed. That run predates the review follow-ups; the masternode list, identity and RPC tests (116) pass on the final code.
  • cargo test -p drive-abci --test strategy_tests with RUST_MIN_STACK=4194304, as in CI.
  • cargo clippy -p drive-abci --all-targets --features mocks -- -D warnings.

Multi-block behaviour is tested at the dispatcher level, not with the strategy harness. The dispatcher is the exact call run_block_proposal makes, and it can run real Core output; the harness would only produce synthetic diffs.

Breaking Changes

Consensus: at protocol versions 13 and 14, a block whose Core height has DEPLOYMENT_V24 active computes different masternode identities than before when Core re-encodes an operator key. See the deployment rule above. There are no API or storage format changes, apart from a new required method on the CoreRPCLike trait.

Checklist:

  • 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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · 026d9eb

  • Bots — coderabbitai skipped after its own rate limit · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it
  • Self-review — post /self-reviewed
  • Build green
  • Approvals — you own every area touched; none needed

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

… same operator key

Once DEPLOYMENT_V24 is active, Dash Core re-serializes a legacy-scheme
operator key in the basic scheme when a masternode's state version is
raised, and protx listdiff reports pubKeyOperator as changed. Platform
created a second operator identity for the same key and disabled the
BLS and node-id keys of the existing one.

update_state_masternode_list v0 and v1 now clear pub_key_operator from
a diff entry whose bytes are the basic encoding of the stored legacy
key, or equal the stored bytes, while V24 is active at the block's Core
height. The gate reads the chain-locked block's coinbase payload version
(4 exactly where V24 and V20 are active) through the new
CoreRPCLike::get_coinbase_payload_version. The list keeps the bytes the
operator identity id is derived from, so the identity, its keys and its
balance stay, and later changes and removals keep reaching it. Before
V24 every reported key is applied exactly as before.

v0 is selected by protocol versions 1 to 13 and is changed in place on
purpose: no block on mainnet or testnet has V24 active, so executed
blocks replay unchanged.

Test would have caught this: on the code before the fix
should_keep_the_operator_identity_of_a_legacy_key_core_re_encodes,
should_keep_the_operator_identity_for_a_negated_key_once_v24_is_active
and should_reach_the_kept_operator_identity_after_a_re_encode_and_a_restart
failed (re-encoded bytes stored, second identity created); after it they
pass. The fixtures are protx listdiff output recorded from Dash Core
v24.0.0-rc.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shumkov shumkov added this to the v5.0.0 milestone Oct 15, 2026
@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a16d4cd9-7fea-44dc-9d63-4fc88f2f9019
📥 Commits

Reviewing files that changed from the base of the PR and between 0f38a69 and 026d9eb.

📒 Files selected for processing: 14
  • packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/operator_key_encoding_tests.rs
  • packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v1/mod.rs
  • packages/rs-drive-abci/src/main.rs
  • packages/rs-drive-abci/src/rpc/core.rs
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/1-399.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/399-400.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/399-404.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/400-401.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/401-402.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/402-403.json
  • packages/rs-drive-abci/tests/supporting_files/core_v24_protx_list_diffs/legacy_operator_key/403-404.json
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@thepastaclaw

thepastaclaw commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 1 blocking finding(s) (commit 026d9eb) · triage: critical

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 15, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Final validation — Phase 1 + Phase 2

Verified the supplied findings against head 026d9eb and consolidated the overlapping reports into one blocking consensus-activation issue. The Core-height gate preserves pre-V24 behavior, but the normalization changes a shipped Platform generation without a Platform protocol activation boundary. Validation was static only; Rust workspace tests were still running in the supplied CI snapshot from 2026-10-15T16:29:45Z.

🔴 1 blocking

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 10: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 11: gemini-3.8-flash-high (agent: phase1-reviewer, role: rust-quality); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The intricate drop_unchanged_operator_keys logic in update_state_masternode_list/mod.rs changes consensus-visible operator key handling, identity continuity, and persisted masternode state, including an activation-gated modification to shipped v0 behavior.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 90% left, 5h 65% left
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — general (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort xhigh); agent phase2-reviewer
  • Model comparison: every Phase-2 reviewer also ran on gpt-6-astra; the verifier weighed both sets without knowing which model wrote which
🤖 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 `packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs:130-138: Activate operator-key normalization through a new Platform generation
  This call changes consensus behavior for shipped protocol versions 1–13, whose method tables still select `update_masternode_list = 0`. With protocol 13 active and a V24-active Core height, the previous implementation applies the re-encoded bytes, derives another operator identity, and disables the previous identity's operator and node-id keys. This implementation clears that key change before both list mutation and identity processing, retaining the original identity and producing different committed state. The new fixture test explicitly exercises retention at protocol 13. The PR's evidence that neither network has reached the triggering Core activation establishes historical replay compatibility, but not consensus equivalence for the shipped generation. Requiring every executing node to upgrade before Core V24 is an external rollout requirement, not the PlatformVersion boundary required by `book/src/contributing/coding-conventions.md`; mixed binaries can disagree under the same active Platform protocol. Restore v0's behavior and confine normalization to an unreleased `update_masternode_list` generation selected through the protocol-14 method tables. Keep coverage showing protocol 13 retains the original identity-switch behavior even with V24-active input, while the unreleased version preserves the identity, and coordinate that Platform activation before Core V24.
Out-of-scope follow-up suggestions (2)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Complete legacy-encoding operator identity lookup in the wallet — tracked.rs derives a single operator identity ID from the current listed key, and locator.rs::platform_roles_from_identity recognizes only that same derivation. When this patch retains a legacy-derived identity while Core lists basic-encoded bytes, wallet refresh can miss that identity and role recognition can miss its operator-payout role. This is a concrete client integration consequence, explicitly assigned to the separate wallet change #5462 rather than this server-side PR.
    • Follow-up: Address in #5462 with shared candidate-ID handling and tests that discover and recognize a legacy-derived identity from a basic-encoded Core listing.
  • Preserve historical operator bytes during state-sync reconstruction — The incremental update now retains historical operator bytes, whereas the from-scratch path skips normalization and accepts Core's current bytes. Reconstructing the list solely from a current Core listing can therefore produce an operator identity reference inconsistent with the restored identity tree. Ordinary persistence is covered here; reconstruction is a concrete integration requirement for the separate state-sync work in #4648.
    • Follow-up: Track in #4648 and validate reconstructed state with subsequent operator-payout changes, genuine key rotation, and masternode removal.

Comment on lines +130 to +138
if !start_from_scratch {
// Protocol versions 1 to 13 select this generation. The call changes no entry unless
// DEPLOYMENT_V24 is active at `core_block_height`, so blocks at earlier Core heights
// execute unchanged.
self.drop_unchanged_operator_keys(
state.full_masternode_list(),
core_block_height,
&mut masternode_diff.updated_mns,
)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Activate operator-key normalization through a new Platform generation

This call changes consensus behavior for shipped protocol versions 1–13, whose method tables still select update_masternode_list = 0. With protocol 13 active and a V24-active Core height, the previous implementation applies the re-encoded bytes, derives another operator identity, and disables the previous identity's operator and node-id keys. This implementation clears that key change before both list mutation and identity processing, retaining the original identity and producing different committed state. The new fixture test explicitly exercises retention at protocol 13. The PR's evidence that neither network has reached the triggering Core activation establishes historical replay compatibility, but not consensus equivalence for the shipped generation. Requiring every executing node to upgrade before Core V24 is an external rollout requirement, not the PlatformVersion boundary required by book/src/contributing/coding-conventions.md; mixed binaries can disagree under the same active Platform protocol. Restore v0's behavior and confine normalization to an unreleased update_masternode_list generation selected through the protocol-14 method tables. Keep coverage showing protocol 13 retains the original identity-switch behavior even with V24-active input, while the unreleased version preserves the identity, and coordinate that Platform activation before Core V24.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor); gpt-6-astra (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor)

@github-actions

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw requested changes on this head; dismiss the review or push a fix; thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 15, 2026

This branch has not been deployed

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

Labels

bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. waiting-self-review Waiting for the author to post /self-reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants