chore(drive-abci): update to nested address in SML#3936
Conversation
…h fix) Bumps all rust-dashcore workspace deps from 5c0113e7 to 64d9267d, picking up: - key-wallet#813 — honor the account index in the DashPay derivation path (additive/backward-compatible; no platform-side change needed). - rpc-json#808 — Core 23 restructured masternode platform ports: the flat platform_p2p_port / platform_http_port are now legacy_* and a nested `addresses` (MasternodeAddresses) was added. #808 migration (behavior-preserving): drive-abci's masternode/validator state reads the legacy flat port pair, exactly as before — MasternodeStateV0 keeps its own field names, so platform's serialized state is unchanged (no state migration, no consensus change). The Core 23 nested `addresses` are intentionally left unconsumed (DMNState -> MasternodeStateV0 drops them; the reverse sets addresses: None); adopting per-protocol addresses is a separate, reviewable change. Deprecated-field reads are scoped under #[allow(deprecated)] with rationale. Workspace compiles clean (all targets, 0 warnings); masternode identity unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
|
✅ Review complete (commit 7641204) |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v3.1-dev #3936 +/- ##
=============================================
+ Coverage 72.78% 87.20% +14.41%
=============================================
Files 22 2633 +2611
Lines 3054 328155 +325101
=============================================
+ Hits 2223 286152 +283929
- Misses 831 42003 +41172
🚀 New features to boost your workflow:
|
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 `@packages/rs-drive-abci/tests/strategy_tests/masternodes.rs`:
- Around line 539-545: The test fixture code increments port values using plain
addition operators on legacy_platform_p2p_port and legacy_platform_http_port,
which can overflow when many updates are applied to the same masternode. Replace
the `*port += 1` operations with saturating or checked arithmetic to prevent
overflow and maintain fixture stability across larger test ranges. Use
saturating_add(1) to safely cap the port values at their maximum rather than
allowing wrapping or panicking.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6cb0cc74-e28a-4349-94c1-3296986b9180
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
Cargo.tomlpackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rspackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rspackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rspackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rspackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rspackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rspackages/rs-drive-abci/src/platform_types/masternode/v0/mod.rspackages/rs-drive-abci/src/platform_types/validator/v0/mod.rspackages/rs-drive-abci/tests/strategy_tests/masternode_list_item_helpers.rspackages/rs-drive-abci/tests/strategy_tests/masternodes.rs
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Dependency bump pulls in Core 23 DMNState/DMNStateDiff changes where platform ports may live only in a nested addresses field (legacy fields = None) or carry sentinel 0 legacy ports. drive-abci consumes the deprecated raw legacy_platform_*_port fields and ignores addresses, which the upstream test suite at the pinned commit explicitly demonstrates is unsafe: HPMNs are silently dropped from validator sets or assigned port 0. The deferral noted in the PR description is not behavior-preserving against Core 23 RPC output and needs the new accessors (platform_p2p_address() / platform_http_address()) wired in before merge, not after.
🔴 3 blocking | 💬 2 nitpick(s)
2 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 `packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs:28-53: Core 23 HPMNs with nested-only addresses are silently dropped from validator sets
`new_validator_if_masternode_in_state` destructures only `legacy_platform_p2p_port` / `legacy_platform_http_port` and early-returns `None` if either is absent. The bumped rust-dashcore (rev `64d9267`) documents in `rpc-json/src/lib.rs` (test `dmn_state_core23_addresses_resolve_platform_ports`, ~L3606) that a Core 23 HPMN deserializes with both `legacy_platform_*_port = None` and the real ports under `addresses.platform_p2p` / `addresses.platform_https`. Combined with `try_from_quorum_info_result` filter-mapping `None`, any Core 23 v3 HPMN that registers with only nested addresses disappears from the platform's quorum view — a consensus-critical state divergence the moment any quorum member is in the new format. This is exactly what the upstream accessors `DMNState::platform_p2p_address()` / `platform_http_address()` exist to prevent. The PR description scopes nested-address adoption to a follow-up, but the upstream change is breaking semantics, not syntax: the deferral is not safe. Either consume the accessors here (and store the resolved host/port pair, since the nested `platform_p2p` can publish a host different from `service.ip()`), or hard-gate the rev bump until the consumer migration lands.
In `packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs:140-220: `DMNState` ↔ `MasternodeStateV0` conversion silently drops Core 23 nested addresses
The forward conversion destructures `addresses: _` and the reverse rebuilds `addresses: None`, so any masternode whose platform ports arrived under the Core 23 `addresses` object is persisted into `MasternodeStateV0` with `platform_p2p_port = None` / `platform_http_port = None`. After a restart or any later refresh, the masternode is then treated by `new_validator_if_masternode_in_state` as missing platform ports and excluded from validator sets. The on-disk `MasternodeStateV0` layout does not have to change to fix this — `From<DMNState>` just needs to fall back to `value.platform_p2p_address()` / `value.platform_http_address()` when the legacy fields are `None`, and (because nested addresses can resolve to a non-service host) the conversion should store the resolved port, ideally with a note that the host pairing is delegated to the validator path. Without this fallback, the PR pins a rust-dashcore that demonstrably loses platform-port information for Core 23 entries (see upstream test `dmn_state_core23_addresses_resolve_platform_ports`).
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:57-160: Core 23 address-only diffs neither refresh validator entries nor propagate port updates
Two related problems in this file:
1. The refresh trigger at lines 150–152 only fires on `pose_ban_height || service || legacy_platform_p2p_port.is_some()`. The upstream test `dmn_state_diff_core23_addresses_resolve_platform_ports` (rpc-json/src/lib.rs ~L3641) shows that an `updatedMNs` entry whose only change is the nested `addresses` deserializes with both `legacy_platform_*_port = None`, so the trigger never fires and validator entries keep stale endpoints after a Core 23 platform port/IP change.
2. `update_masternode_in_validator_sets` reads `dmn_state_diff.legacy_platform_p2p_port` / `legacy_platform_http_port` and writes `port as u16` straight into the validator. The transitional case in the upstream test `dmn_state_zero_legacy_port_resolves_to_addresses` (rpc-json/src/lib.rs ~L3699) confirms Core 23 can emit `platformP2PPort: 0` alongside real ports in `addresses`. That sentinel currently flows through as `validator.platform_p2p_port = 0`, advertised verbatim to Tenderdash as `tcp://...:0` — a connectivity break the operator did not request. The `as u16` cast also silently truncates ports outside `u16` (upstream test `dmn_state_zero_legacy_port_no_addresses_resolves_to_none` and the 70000-port fixture demonstrate the explicit upstream contract is to reject these).
Fix should consume `DMNStateDiff::platform_p2p_address()` / `platform_http_address()` (and equivalents on `DMNState`) and gate the refresh trigger on `addresses.is_some()` as well, so v3 ProTx churn cannot inject a port-0 endpoint or skip a needed refresh.
Make the DashPay design docs match what actually shipped on this branch: - SYNC_CORRECTNESS_SPEC (Spec 0): REVIEWED→IMPLEMENTED — both stages shipped (paginated/high-water request sync + id-keyed contact-profile cache), durably persisted to both the SQLite persister and SwiftData. - CONTACTINFO_FORMAT_SPEC (Spec 1): the §4 contactInfo reject/block field marked DEFINED-but-NOT-ADOPTED — R1 resolved ignore as local-only, so it is not carried on contactInfo; the shipped codec is alias/note/displayHidden/acceptedAccounts. - BLOCK_SPEC: PAUSED→SUPERSEDED — replaced by the implemented per-sender, reversible, local-only Ignore (Spec 2); the contactInfo cross-device route was rejected (R1); cross-device deferred to a future encrypted profile field (contract track). - SPEC.md: added a 2026-06-18 status block mapping the Part-0 gap table (G1–G15) to its resolution, plus accountReference (keep-ours) and the account-index path fix (upstream rust-dashcore#813 via dashcore bump PR #3936). Points at TODO.md as the authoritative item-by-item status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ectation #3886 (2026-06-14) raised the per-network protocol floors (min_protocol_version: Mainnet=11, others=12). The build-time clamp applies them as max(DEFAULT_INITIAL_PROTOCOL_VERSION, min_protocol_version(network)), which silently invalidated this test (added earlier by #3809): it still asserted the unpinned mock SDK seeds to DEFAULT_INITIAL_PROTOCOL_VERSION (10), but new_mock() defaults to Mainnet, so the seed is now 11. v3.1-dev has been red on this test since #3886; the DashPay merge pulled the breakage in. Assert the SDK's documented post-clamp contract instead (max(DEFAULT_INITIAL_PROTOCOL_VERSION, PROTOCOL_VERSION_11) == 11). Verified locally: left:11 right:10 ✖ before, 1 passed ✔ after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…conversion Addresses the blocking review on #3936. The legacy-only port mapping read only DMNState.legacy_platform_*_port, which are `None` for Core 23 masternodes (Core 23 moved the ports into the nested `addresses` object). That stored `platform_p2p_port = None`, and new_validator_if_masternode_in_state then excluded those nodes from validator sets — a consensus bug, not just "multi-address unsupported". Fix: resolve the port through DMNState's `platform_p2p_address()` / `platform_http_address()` accessors (prefer nested `addresses`, fall back to the legacy flat field) in both From<DMNState> and new_validator_if_masternode_in_state. Core 22 entries map byte-identically; Core 23 entries now yield their port. MasternodeStateV0 still stores only a port — no on-disk layout change. The two sites no longer touch deprecated fields, so their #[allow(deprecated)] is dropped. Tests (red→green): from_dmn_state_resolves_core23_nested_platform_ports would read None and fail under the old legacy-only path; from_dmn_state_falls_back_to_legacy_ports pins the Core 22 path unchanged. Review nits also addressed: saturating_add in the port-bump strategy-test helper (overflow guard); corrected the "below"→"above" helper-location comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
new_validator_if_masternode_in_state resolves ports via the DMNState accessors instead of destructuring the struct, so the DMNState type import is unused. CI denies warnings, so remove it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Cumulative review at 3530df8. The PR successfully resolves prior blocking findings on validator/masternode conversion: nested-only Core 23 platform ports are now resolved via DMNState::platform_p2p_address()/platform_http_address() accessors with covering unit tests, and the prior nitpicks on comment direction and saturating_add are also fixed. The masternode-list diff path remains the one carried-forward blocking gap: address-only Core 23 diffs still do not refresh cached validator_sets, and the new inline rationale is factually wrong — DMNStateDiff at rev 64d9267 does expose nested-address accessors (rpc-json/src/lib.rs:2303,2316). The validator host being sourced from service.ip() rather than the nested platform host is recorded as an out-of-scope follow-up since it is pre-existing behavior the PR explicitly defers.
🔴 1 blocking
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 `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:85-162: Carried-forward prior #3 STILL VALID: Core 23 address-only diffs do not refresh validator_sets; deferral rationale is factually wrong
Two issues compound on this code path at the bumped rev:
1) The refresh trigger at lines 152-154 only fires on pose_ban_height/service/legacy_platform_p2p_port. With rust-dashcore 64d9267, DMNStateDiff carries Core 23 platform endpoint changes in `addresses: Option<Option<MasternodeAddresses>>`, and apply_diff propagates them into hpmn_masternode_list. A Core 23 HPMN whose only changed field is the nested platform address is therefore applied to state but never reaches update_masternode_in_validator_sets, so cached ValidatorV0.node_ip/platform_*_port stay stale until an unrelated legacy field changes or the next quorum rotation rebuilds from state. This is deterministic across nodes (no consensus split) but causes bounded liveness lag for cross-validator P2P after Core 23 port-only changes — and contradicts the PR's stated goal of resolving Core 23 nested platform ports in masternode conversion.
2) The new inline comment at lines 85-88 justifies the deferral by asserting "the diff type has no nested-address accessor." That is false at the pinned rev: rust-dashcore rpc-json/src/lib.rs defines DMNStateDiff::platform_p2p_address() at line 2303 and DMNStateDiff::platform_http_address() at line 2316, mirroring the DMNState accessors this PR already adopts in validator/v0 and masternode/v0. Future maintainers will read this as upstream-not-yet-ready, when in reality the accessors exist and the deferral is a scoping choice.
Resolution options: (a) extend the trigger to also fire on state_diff.platform_p2p_address().is_some() / platform_http_address().is_some() and consume them inside update_masternode_in_validator_sets; or (b) keep the deferral but rewrite the comment to honestly state that the accessor exists but adoption is deferred to a follow-up, so the rationale is not load-bearing on a false claim.
…f path too Follow-up to the From<DMNState> accessor fix: update_masternode_in_validator_sets and its trigger guard still read DMNStateDiff.legacy_platform_*_port directly, which is None for Core 23 nodes — so a Core 23 platform-port change carried in the diff's nested `addresses` would not propagate to the validator until the next full validator-set rebuild. Unlike DMNState's accessor, DMNStateDiff::platform_p2p_address() does NOT fall back to the legacy field (it reads only the nested addresses), so OR the accessor with the legacy field rather than replacing it — otherwise a Core 22 port change (in the legacy flat field) would be missed. The change-detection guard is widened the same way so a Core-23-only address change still triggers the validator-set update. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Carried-forward prior finding #1 is FIXED at d119dd1: the refresh trigger now includes state_diff.platform_p2p_address().is_some() and update_masternode_in_validator_sets reads via the DMNStateDiff::platform_p2p_address()/platform_http_address() accessors with legacy fallback. The codex agents re-frame the same code path as still-valid blockers around host discard and addresses-cleared diffs, but those concerns are explicitly deferred by this PR's scope (MasternodeStateV0 only stores the legacy port pair, host pairing is delegated to the validator path per the documented comment) and they are behavior-preserving relative to Core 22 — they do not break consensus, are not regressions introduced by this PR, and adopting nested platform hosts requires a versioned state migration that is out of scope for a dependency bump. One minor latest-delta suggestion remains: the refresh trigger only checks the p2p axis, so a Core 23 diff that updates only platform_https or carries Some(None) addresses won't fire validator refresh.
🟡 1 suggestion(s)
🤖 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`:
- [SUGGESTION] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs:162-166: Validator refresh trigger ignores http-only and cleared-addresses diffs
The refresh predicate at lines 162-166 only consults the p2p axis (state_diff.platform_p2p_address().is_some() and state_diff.legacy_platform_p2p_port.is_some()). With the bumped rust-dashcore 64d9267, DMNStateDiff::addresses is three-state (None / Some(None) / Some(Some(_))): a Core 23 diff that updates only platform_https (empty platform_p2p), or that clears addresses entirely (Some(None)), leaves platform_p2p_address() returning None and the trigger never fires — so the cached validator entry keeps a stale platform_http_port and/or stale p2p port until the next pose-ban/service/p2p change. In practice today's Core 23 RPC ships p2p and https together so the dominant case is covered, but the cleanest fix is to also check the http accessor, the legacy http field, and the raw addresses presence so any addresses-bearing diff refreshes the validator entry. Validator_sets is local node cache (not consensus state), so impact is wrong-port routing/liveness rather than consensus divergence, hence suggestion-level.
…iff-port resolution Addresses the review suggestion that the validator-refresh trigger only consulted the p2p axis: a Core 23 diff changing only platform_https, or clearing `addresses` (Some(None)), left platform_p2p_address() == None and the legacy field None, so the trigger never fired and the cached validator kept a stale http/p2p port. The guard now also checks the http resolver and the raw `addresses` presence (three-state: None / Some(None) / Some(Some)). Extracted the diff port resolution into `diff_platform_p2p_port` / `diff_platform_http_port` (prefer the Core 23 nested-addresses accessor — which on DMNStateDiff, unlike DMNState, has NO legacy fallback — then fall back to the legacy flat field). This makes the resolution unit-testable and consolidates the deprecated legacy reads into the two helpers, so the #[allow(deprecated)] drops off both update_masternode_in_validator_sets and update_state_masternode_list_v0. Tests: nested-Core-23 (both ports), http-only Core-23 (the missed case), Core-22 legacy fallback, and no-change → None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The current head is 1199b87, and the prior refresh-trigger issue is fixed by the new http/legacy/raw-address predicate. One in-scope validator-cache correctness issue remains: address-clearing or empty Core 23 address diffs update the stored HPMN state but leave existing cached validator ports unchanged.
🔴 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 `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:71-77: Clear cached validators when platform ports disappear
The new refresh predicate now reaches this helper for any `state_diff.addresses.is_some()`, but the helper only writes ports when `diff_platform_p2p_port` or `diff_platform_http_port` returns `Some`. For Core 23 diffs such as `addresses: Some(None)` or an `addresses` object whose platform port list is empty/invalid, upstream `DMNState::apply_diff` removes the resolvable platform address from the already-updated HPMN state, and `ValidatorV0::new_validator_if_masternode_in_state` would return `None` because both platform ports are mandatory. The existing cached `ValidatorV0` in each validator set is still retained with its old `platform_p2p_port` and `platform_http_port`, so Tenderdash can continue receiving stale endpoint data that no longer matches Core state. Rebuild the validator entry from the updated HPMN state, or remove the cached member when the updated state no longer resolves the required platform ports.
|
Coverage note for my review at 1199b87: Claude reviewers and the Claude verifier could not run because the Claude ACP account hit its usage limit (resets 8am America/Chicago). Codex general, security-auditor, rust-quality, and Codex verifier ran successfully; CodeRabbit had 0 inline findings and was rate-limited. |
…when ports vanish
Two Core 23 masternode-diff correctness fixes (rust-dashcore#808 context):
1. Zeroed legacy port must not surface. Core 23 (ProTx v3) entries zero the
deprecated flat platformP2PPort/HTTPPort (the real port lives in the nested
`addresses`). The diff accessor already drops a zero addresses port, but the
legacy fallback in diff_platform_{p2p,http}_port read the raw legacy field — so
an edge diff (addresses unresolvable + legacy 0) would set a validator's
platform port to 0, the exact failure #808 fixed. The fallback now drops 0.
2. Remove cached validators when platform ports disappear. The widened refresh
guard reaches update_masternode_in_validator_sets for any addresses delta, but
the helper only writes ports when they resolve — so a Core 23 diff clearing
`addresses` (Some(None)) or zeroing the ports left the cached ValidatorV0 with
its stale ports, and Tenderdash kept getting a dead endpoint. After apply_diff
we now check the updated state via resolves_platform_validator_ports() (both
ports mandatory, matching new_validator_if_masternode_in_state) and drop the
validator from the sets when it no longer resolves them.
Tests: zeroed-legacy-dropped; resolves-for-core23-addresses / for-legacy;
does-not-resolve-when-ports-disappear (cleared + http-only). 10 tests green,
clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Prior 1199b87 finding (stale cached validators when platform ports vanish) is resolved on this head by the new resolves_platform_validator_ports predicate that branches between update and remove. Remaining in-scope concerns: the new accessor-based code throws away the host string returned by platform_p2p_address()/platform_http_address(), and the persistence roundtrip collapses Core 23 addresses into legacy ports in a way that can mask a later addresses-clear diff after restart. No new blocking defects.
🟡 2 suggestion(s) | 💬 1 nitpick(s)
🤖 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/platform_types/validator/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/platform_types/validator/v0/mod.rs:30-36: Validator construction discards the platform host returned by the Core 23 accessor
`platform_p2p_address()` / `platform_http_address()` return `(host, port)`, but both the validator constructor here and `update_masternode_in_validator_sets` (update_state_masternode_list/v0/mod.rs:73-79) bind `_host` and copy only the port, then use `state.service.ip()` for `node_ip`. `ValidatorSetV0::to_update` advertises Tenderdash endpoints as `node_id@node_ip:platform_p2p_port`, so if a Core 23 ProTx v3 entry ever carries a platform host that differs from the Core service host, Platform will advertise the validator on the wrong host. The pre-existing convention has been service.ip() == platform host, so this likely does not fire in current devnet deployments — but now that the accessor explicitly surfaces a host, please either propagate it into `node_ip` (and refresh on `addresses` diffs) or document why the service IP is intentionally authoritative over the Core 23 platform host.
In `packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs:149-165: Persisted-state roundtrip can mask a Core 23 addresses-clear diff after restart
`MasternodeStateV0 -> DMNState` (packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs:217-219) reconstructs persisted Core 23 state as `addresses: None` with the resolved port written into `legacy_platform_p2p_port`/`legacy_platform_http_port`. After a restart, a subsequent Core 23 diff with `addresses: Some(None)` (explicit clear) and no legacy-port update applies as `addresses: Some(None)` + `legacy_*: Some(<old>)`. `state.platform_p2p_address()` then falls back to the synthesized legacy port, so `resolves_platform_validator_ports` returns true and the cached validator is retained even though Core has cleared the platform endpoint. Either treat an explicit `state_diff.addresses` delta as authoritative for the validity check (`Some(Some(_))` → check diff accessors; `Some(None)` → false; `None` → full-state fallback), or change the persistence shape to remember address provenance.
…ulti-reviewer) Self-review pass (3 reviewers) over the #808 Core-23 handling. All findings are P2P-connectivity, not consensus (validator_sets live in the non-hashed `aux` CF and Tenderdash's committed ValidatorSet hash excludes node_address/ports/membership), but each leaves a validator advertising a wrong/stale/dead platform endpoint: F1 — Persisted round-trip masked an `addresses` clear after restart. The reverse `From<MasternodeStateV0> for DMNState` stuffed the resolved port into `legacy_*` with `addresses: None`; after restart an `addresses: Some(None)` clear left the stale legacy port, the full-state accessor fell back to it, and the de-platformed validator was wrongly retained. Now reconstructs the Core-23 `addresses` shape (host = service IP), `legacy_*: None`, so a later clear is honored. F2 — Two sources of truth. Validity was judged on the post-apply full state but the validator was patched from the raw diff; since `apply_diff` wholesale-replaces `addresses`, a partial Core-23 diff could clobber the unchanged axis. Replaced `resolves_platform_validator_ports` + diff-patching `update_masternode_in_validator_sets` with a single `validator_refresh_from_state` (Some → `apply_validator_refresh`, None → remove), deriving every field once from the post-apply full state. F3 — `validator_refresh_from_state` now requires `platform_node_id` (matching `new_validator_if_masternode_in_state`); an HPMN that lost it is no longer valid. F4 — Validator `node_ip` now uses the accessor's platform p2p host (= service IP for a legacy node, the Core-23 platform host otherwise) instead of always `service.ip()`. F5 — Port resolution uses `u16::try_from` (drop out-of-range) instead of `as u16` truncation, in both the diff resolvers and the full-state derivation — an out-of-range legacy port can no longer truncate to 0 (the #808 shape). Tests (red→green where applicable): reverse-From reconstructs addresses + a clear now drops the endpoint (RED on old legacy-shaped round-trip); refresh resolves Core-23 with the platform host / legacy with service IP; refresh None on ports-disappear, missing node-id; diff resolver drops zero + out-of-range legacy ports. 180 masternode/validator lib tests green, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Latest delta resolves all three prior 8e19e2a findings (validator node_ip now propagates Core 23 platform host; persisted-state round-trip reconstructs addresses with legacy fields cleared so a Some(None) clear is honored; refresh predicate replaced by validator_refresh_from_state that mirrors the full validity gate including platform_node_id). Two narrow gaps remain in the refresh-trigger ↔ refresh-predicate symmetry (zeroed legacy ports and platform_node_id deltas), plus the persisted-state path still loses the Core 23 platform host across restart. None are exploitable in current Core 23 deployments (collocated hosts; Core does not emit standalone zero-port diffs), so they are suggestions rather than blockers.
🟡 2 suggestion(s) | 💬 1 nitpick(s)
🤖 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`:
- [SUGGESTION] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs:141-146: Refresh trigger misses zeroed/legacy-only platform-port deltas the predicate would drop
The removal branch in apply_diff fires only when `diff_platform_p2p_port`/`diff_platform_http_port` return Some or addresses is in the diff. Both helpers intentionally filter `0` and out-of-range legacy ports (line 211/222). The code comment at lines 155–159 explicitly lists "a zeroed legacy port with no addresses" as one of the removal cases, but a state_diff carrying only `legacy_platform_p2p_port: Some(0)` (or an out-of-range value) with no addresses delta and no service/pose_ban change does not trip the guard, so `validator_refresh_from_state` is never consulted and the stale validator entry stays advertised. In practice Core 23 diffs that zero the legacy field also carry an `addresses` delta, but the trigger should not depend on that — the refresh predicate is the source of truth and the trigger should mirror its full gate. Same class of inconsistency this PR is paying down elsewhere. Easiest fix: include `state_diff.legacy_platform_p2p_port.is_some() || state_diff.legacy_platform_http_port.is_some()` (and optionally `state_diff.platform_node_id.is_some()`) in the guard so any field that can change validity wakes up the refresh.
In `packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs:211-224: Persisted Core 23 state loses the platform host across restart
`MasternodeStateV0` stores only the resolved platform ports, so this reverse conversion synthesizes `addresses` using `service.ip()` as the host. The live validator path (validator/v0/mod.rs:42 and validator_refresh_from_state at update_state_masternode_list/v0/mod.rs:244) now treats the Core 23 `addresses.platform_p2p` host as authoritative — the PR's own test `refresh_resolves_core23_addresses_with_platform_host` (line 385) deliberately uses a platform host distinct from the service IP. After a Platform restart, any Core 23 HPMN whose platform host differs from its core service IP is reconstructed with the wrong host; subsequent quorum/validator-set construction will advertise `node_id@service_ip:platform_p2p_port` to Tenderdash until the next `addresses` diff overwrites it. Today's deployments collocate the two hosts so this is latent, but the PR ships and tests the distinct-host path, leaving the persistence layer inconsistent with the live path's claimed support. Either persist the platform host in `MasternodeStateV0` (versioned schema change) or document the post-restart re-converge behavior and the collocated-host invariant.
…eStateV1) Core 23 (DEPLOYMENT_V24) decouples a masternode's platform endpoints from its core service address via the nested `addresses` object. The live validator path already advertises the platform host, but persisted state (MasternodeStateV0) stored only the ports, so a normal restart — which loads the masternode list from disk and applies incremental Core diffs — reconstructed the host as service.ip(), silently advertising the wrong endpoint for a split-host node until the next `addresses` diff. Add a versioned MasternodeStateV1/MasternodeV1/Masternode::V1 that retains the platform host, gated to protocol v12 via a new DRIVE_ABCI_STRUCTURE_VERSIONS_V2 (masternode: 1). MasternodeStateV0 stays byte-identical (the persisted format is released; a regression test deserializes real testnet-v0/devnet-v8 state); v1-v11 keep writing V0. The read path is variant-tag driven (no version gate), so any binary decodes either variant. Consensus-safe: the serialized platform state lives in the non-hashed aux CF (put_aux), not grovedb's hashed tree, so it is never in app_hash; fingerprint() is diagnostic-only; and no ABCI state-sync handler ships the blob between nodes. A binary downgrade past V1 fails loud at startup, never a silent misdecode. Also close a refresh-trigger gap introduced by the earlier zero/range port filter: extract diff_triggers_validator_refresh as a superset of validator_refresh_from_state's inputs so a diff carrying only a zeroed legacy port or a platform_node_id rotation still wakes the refresh (the port helpers drop those). Tests (red->green embedded in each): - v1 round-trip preserves a distinct platform host where V0 collapses it to service.ip() - legacy/Core-22 node falls back to service.ip() (matches V0) - MasternodeListItem save/load round-trip keeps the host - guard fires on zeroed-legacy-port and node-id-rotation diffs the port helpers drop Design + consensus-safety analysis (4-agent reviewed): docs/CORE23_SPLIT_HOST_PERSISTENCE_SPEC.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 `@packages/rs-drive-abci/src/platform_types/masternode/v1/mod.rs`:
- Around line 155-158: The platform_host derivation at line 157 only extracts
the host from platform_p2p_address(), which causes data loss when only
platform_http_address() resolves. Refactor the platform_host extraction to use a
fallback pattern: first attempt to extract the host from platform_p2p (the
current approach), and if that yields None, then extract the host from
platform_http_address() as a fallback. This ensures the host information is
preserved regardless of whether the P2P or HTTP address is the one that
resolves, maintaining a lossless round-trip conversion.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: fec25ade-3134-4113-9314-1758540cc1a7
📒 Files selected for processing (8)
docs/CORE23_SPLIT_HOST_PERSISTENCE_SPEC.mdpackages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rspackages/rs-drive-abci/src/platform_types/masternode/accessors.rspackages/rs-drive-abci/src/platform_types/masternode/mod.rspackages/rs-drive-abci/src/platform_types/masternode/v1/mod.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_structure_versions/mod.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_structure_versions/v2.rspackages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (2)
- packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_structure_versions/v2.rs
- packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_structure_versions/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs
Address review notes on update_state_masternode_list: fold the apply-or-remove decision into a single function parameterized by the post-apply_diff masternode state rather than a pre-built ValidatorRefresh. Validator reconciliation is now `refresh_validator_in_sets_from_state(pro_tx_hash, &DMNState, validator_sets)`, which resolves the validity predicate internally and either rewrites the cached validator's advertised fields or removes it when the node is no longer a valid HPMN platform validator. The caller clones the updated state so the hpmn-list borrow ends before the validator-set borrow. No behavior change; masternode/validator/update_state unit tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit finding on MasternodeStateV1: derive `platform_host` from the http address when only it resolves, so an http-only entry round-trips its host instead of collapsing to service.ip() on the reverse conversion. The http host is never advertised (validators use the p2p host, and an http-only entry is not a valid platform validator), but this keeps the V1 round-trip lossless. Pinned by `v1_http_only_node_preserves_host`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
All three prior findings are resolved by the latest delta: validator-refresh trigger now includes zeroed legacy ports + platform_node_id deltas, and MasternodeStateV1 persists the Core 23 platform host. However, the platform_node_id portion of the trigger fix is incomplete — the refresh struct and write path were not extended to carry node_id, so a node-id rotation takes the refresh path but leaves validator.node_id stale. One additional suggestion: the new Core 23 reverse conversions emit unbracketed IPv6 host:port strings that the upstream parser rejects, breaking the persisted round-trip for IPv6 HPMN nodes.
🟡 1 suggestion(s)
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 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/platform_types/masternode/v1/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/platform_types/masternode/v1/mod.rs:221-234: Reverse conversion emits unparsable IPv6 platform addresses
The new Core-23-shaped reverse conversions in v1/mod.rs (line 221-234) and v0/mod.rs (line 211-224) rebuild platform_p2p / platform_https entries via `format!("{host}:{port}")` where host is either `service.ip()` (an IpAddr) or `platform_host` (a String that for legacy nodes is captured from `service.ip().to_string()`). For an IPv6 service such as `2001:db8::1`, IpAddr's Display impl produces the bare unbracketed form, so the stored string becomes `2001:db8::1:36656`. The upstream nested-address parser rejects unbracketed multi-colon hosts, so DMNState::platform_p2p_address() returns None after a save/load round-trip and new_validator_if_masternode_in_state drops the HPMN from validator sets until a later Core addresses diff. This breaks the persisted round-trip exactly for IPv6 HPMN operators. Fix by routing IPv6 hosts through SocketAddr::new(ip, port).to_string() (which brackets correctly) when the host is an IpAddr, or by wrapping any String host containing ':' but not already starting with '[' in brackets before formatting. The same fix should be applied to v0/mod.rs lines 211-224.
… addresses
thepastaclaw/codex finding: the reverse MasternodeState{V0,V1} -> DMNState conversions
rebuilt platform_p2p / platform_https via `format!("{host}:{port}")`, which for an IPv6
host emits an unparsable `2001:db8::1:443`. The upstream nested-address parser rejects
unbracketed multi-colon hosts, so `platform_p2p_address()` returned None after a save/load
round-trip and the HPMN was dropped from validator sets until the next Core addresses diff —
breaking the persisted round-trip for IPv6 HPMN operators.
Add a shared `format_platform_address` helper that brackets an unbracketed IPv6 host (IPv4
and hostnames are emitted as-is; already-bracketed hosts are left alone) and route both v0
and v1 reverse conversions through it.
Tests (red->green): `format_tests::brackets_only_unbracketed_ipv6`, and IPv6 round-trips
`reverse_from_brackets_ipv6_host` (v0) / `v1_ipv6_host_round_trips` (v1) — the reconstructed
address re-parses where the unbracketed form would resolve to None.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shumkov
left a comment
There was a problem hiding this comment.
Addressed all open review threads in a05da4d36c … 2979a26613 — see inline replies. Summary: split-host persistence (MasternodeStateV1, protocol-v12-gated, non-consensus aux-CF state), refresh-trigger completeness (diff_triggers_validator_refresh superset), IPv6 platform-host bracketing, and the validator-reconcile refactor (now a function of state). Design + 4-agent consensus-safety analysis: docs/CORE23_SPLIT_HOST_PERSISTENCE_SPEC.md.
Mechanical `cargo fmt` — the new IPv6/host test assertions exceeded the line width. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Reconciled two prior findings against head 073d68c — both remain STILL VALID. The latest delta refactored validator reconciliation to derive from full post-apply_diff state, which is semantically equivalent and a clean cleanup, but did NOT touch (1) the ValidatorRefresh field set (still missing platform_node_id) nor (2) the IPv6 host:port formatting in v0/v1 reverse conversions. claude-general also surfaces a real but lower-impact V0 round-trip issue that masks Core-22 legacy port changes after restart by eagerly synthesizing an addresses field; kept as a suggestion.
🔴 1 blocking | 🟡 1 suggestion(s)
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 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/platform_types/masternode/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs:211-224: Reverse conversions emit unparsable IPv6 platform addresses (v0 always, v1 legacy fallback)
Both reverse conversions rebuild the Core 23 nested `addresses` entries via `format!("{host}:{port}")`. In v0/mod.rs:211, `host = service.ip()` is an `IpAddr`; for IPv6 its `Display` impl produces a bare unbracketed string, so the persisted entry becomes e.g. `2001:db8::1:36656`. The bumped rust-dashcore `parse_host_port` (rpc-json/src/lib.rs:2088-2098) explicitly rejects unbracketed multi-colon hosts: `if host.contains(':') && !(host.starts_with('[') && host.ends_with(']')) { return None; }` — verified by the `parse_host_port_ipv6` test. After a save/load round-trip on an IPv6 HPMN, `DMNState::platform_p2p_address()` returns `None` and `new_validator_if_masternode_in_state` drops that HPMN from validator sets until a later Core addresses diff (which Core 22 never emits). v1/mod.rs:221 has the same defect on the fallback path: `host = platform_host.unwrap_or_else(|| service.ip().to_string())` — for a Core 22 legacy IPv6 node the captured `platform_host` itself comes from `legacy_platform_address` (rust-dashcore lib.rs:2174) which uses `self.service.ip().to_string()` (unbracketed). Note: the Core 23 nested-addresses path for v1 is safe because `first_valid_host_port` retains the brackets it parsed in. Fix by wrapping IPv6 hosts in brackets before formatting, e.g. `SocketAddr::new(ip, port).to_string()` when starting from an `IpAddr`, or bracketing a `String` host that contains `:` and is not already bracketed.
- [SUGGESTION] packages/rs-drive-abci/src/platform_types/masternode/v0/mod.rs:184-243: V0 reverse synthesizes `addresses` from a Core-22 port, masking subsequent legacy port-change diffs after restart
`From<MasternodeStateV0> for DMNState` unconditionally builds `addresses = Some(Some(MasternodeAddresses{ ..host:stored_port.. }))` whenever either port is present, and leaves `legacy_platform_*_port = None`. That is the right shape for a Core-23 origin (so an `addresses: Some(None)` clear diff can drop the endpoint, per the `reverse_from_reconstructs_core23_addresses_then_honors_a_clear` test). But it is also applied to persisted state that originally came from a Core-22 node — i.e. every HPMN on mainnet/testnet today, where the protocol version is pre-v12 and V0 is the only persistence variant in use. After a drive-abci restart on Core 22: the reloaded DMNState has `addresses = Some(<host:old_port>)` and `legacy_*_port = None`; the next Core-22 diff carries `legacy_platform_p2p_port = Some(Some(new_port))` with `addresses = None`; after `apply_diff` the state has `addresses = Some(<host:old_port>)` AND `legacy_platform_p2p_port = Some(new_port)`. Per `DMNState::platform_p2p_address()` (rust-dashcore lib.rs:2147-2152, which prefers `addresses` over the legacy fallback), the resolver returns the stale host:old_port, and `new_validator_if_masternode_in_state` / `validator_refresh_from_state` advertise the old port until a later addresses-bearing diff — which Core 22 never emits. Pre-PR the platform port was a flat field that the legacy path updated directly. Two reasonable fixes: (a) leave `legacy_*_port = Some(stored_port)` and `addresses = None` in the reverse when there is no captured platform host (V0 has none), since the legacy path remains correct for Core-22 environments and a Core-23 transition would emit an addresses diff anyway; or (b) consult `platform_version` and only synthesize `addresses` when the active protocol expects Core 23.
| let host = service.ip(); | ||
| let addresses = (platform_p2p_port.is_some() || platform_http_port.is_some()).then(|| { | ||
| MasternodeAddresses { | ||
| core_p2p: vec![], | ||
| platform_p2p: platform_p2p_port | ||
| .map(|port| format!("{host}:{port}")) | ||
| .into_iter() | ||
| .collect(), | ||
| platform_https: platform_http_port | ||
| .map(|port| format!("{host}:{port}")) | ||
| .into_iter() | ||
| .collect(), | ||
| } | ||
| }); |
There was a problem hiding this comment.
🔴 Blocking: Reverse conversions emit unparsable IPv6 platform addresses (v0 always, v1 legacy fallback)
Both reverse conversions rebuild the Core 23 nested addresses entries via format!("{host}:{port}"). In v0/mod.rs:211, host = service.ip() is an IpAddr; for IPv6 its Display impl produces a bare unbracketed string, so the persisted entry becomes e.g. 2001:db8::1:36656. The bumped rust-dashcore parse_host_port (rpc-json/src/lib.rs:2088-2098) explicitly rejects unbracketed multi-colon hosts: if host.contains(':') && !(host.starts_with('[') && host.ends_with(']')) { return None; } — verified by the parse_host_port_ipv6 test. After a save/load round-trip on an IPv6 HPMN, DMNState::platform_p2p_address() returns None and new_validator_if_masternode_in_state drops that HPMN from validator sets until a later Core addresses diff (which Core 22 never emits). v1/mod.rs:221 has the same defect on the fallback path: host = platform_host.unwrap_or_else(|| service.ip().to_string()) — for a Core 22 legacy IPv6 node the captured platform_host itself comes from legacy_platform_address (rust-dashcore lib.rs:2174) which uses self.service.ip().to_string() (unbracketed). Note: the Core 23 nested-addresses path for v1 is safe because first_valid_host_port retains the brackets it parsed in. Fix by wrapping IPv6 hosts in brackets before formatting, e.g. SocketAddr::new(ip, port).to_string() when starting from an IpAddr, or bracketing a String host that contains : and is not already bracketed.
source: ['claude', 'codex']
There was a problem hiding this comment.
Resolved in 2979a26 — Reverse conversions emit unparsable IPv6 platform addresses (v0 always, v1 legacy fallback) no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
| @@ -191,6 +208,21 @@ impl From<MasternodeStateV0> for DMNState { | |||
| platform_http_port, | |||
| } = value; | |||
|
|
|||
| let host = service.ip(); | |||
| let addresses = (platform_p2p_port.is_some() || platform_http_port.is_some()).then(|| { | |||
| MasternodeAddresses { | |||
| core_p2p: vec![], | |||
| platform_p2p: platform_p2p_port | |||
| .map(|port| format!("{host}:{port}")) | |||
| .into_iter() | |||
| .collect(), | |||
| platform_https: platform_http_port | |||
| .map(|port| format!("{host}:{port}")) | |||
| .into_iter() | |||
| .collect(), | |||
| } | |||
| }); | |||
|
|
|||
| Self { | |||
| service, | |||
| registered_height, | |||
| @@ -203,8 +235,133 @@ impl From<MasternodeStateV0> for DMNState { | |||
| pub_key_operator, | |||
| operator_payout_address, | |||
| platform_node_id, | |||
| platform_p2p_port, | |||
| platform_http_port, | |||
| legacy_platform_p2p_port: None, | |||
| legacy_platform_http_port: None, | |||
| addresses, | |||
| } | |||
| } | |||
| } | |||
There was a problem hiding this comment.
🟡 Suggestion: V0 reverse synthesizes addresses from a Core-22 port, masking subsequent legacy port-change diffs after restart
From<MasternodeStateV0> for DMNState unconditionally builds addresses = Some(Some(MasternodeAddresses{ ..host:stored_port.. })) whenever either port is present, and leaves legacy_platform_*_port = None. That is the right shape for a Core-23 origin (so an addresses: Some(None) clear diff can drop the endpoint, per the reverse_from_reconstructs_core23_addresses_then_honors_a_clear test). But it is also applied to persisted state that originally came from a Core-22 node — i.e. every HPMN on mainnet/testnet today, where the protocol version is pre-v12 and V0 is the only persistence variant in use. After a drive-abci restart on Core 22: the reloaded DMNState has addresses = Some(<host:old_port>) and legacy_*_port = None; the next Core-22 diff carries legacy_platform_p2p_port = Some(Some(new_port)) with addresses = None; after apply_diff the state has addresses = Some(<host:old_port>) AND legacy_platform_p2p_port = Some(new_port). Per DMNState::platform_p2p_address() (rust-dashcore lib.rs:2147-2152, which prefers addresses over the legacy fallback), the resolver returns the stale host:old_port, and new_validator_if_masternode_in_state / validator_refresh_from_state advertise the old port until a later addresses-bearing diff — which Core 22 never emits. Pre-PR the platform port was a flat field that the legacy path updated directly. Two reasonable fixes: (a) leave legacy_*_port = Some(stored_port) and addresses = None in the reverse when there is no captured platform host (V0 has none), since the legacy path remains correct for Core-22 environments and a Core-23 transition would emit an addresses diff anyway; or (b) consult platform_version and only synthesize addresses when the active protocol expects Core 23.
source: ['claude']
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Cumulative re-validation at HEAD 7641204: Prior #1 (validator refresh ignores rotated platform_node_id) is STILL VALID — ValidatorRefresh (v0/mod.rs:209-214) carries only node_ip/ports/is_banned and validator_refresh_from_state discards state.platform_node_id after gating on it, so a node-id rotation fires the trigger but writes a stale node_id. Prior #2 (IPv6 bracketing in reverse conversions) is FIXED by commit 2979a26 via format_platform_address (masternode/mod.rs:62-68), wired into both V0 and V1 reverse paths and covered by brackets_only_unbracketed_ipv6. Prior #3 (V0 reverse synthesizes nested addresses from a Core-22 port, masking later legacy port diffs after restart) is STILL VALID — From<MasternodeStateV0> for DMNState (v0/mod.rs:184-242) unconditionally builds addresses = Some(..) whenever any platform port is present and clears legacy fields. No genuinely new latest-delta findings beyond the carry-forwards.
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 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:62-72: Carried-forward (STILL VALID): validator refresh never writes the rotated platform_node_id (or service core_port)
Re-validated at HEAD 7641204b. `diff_triggers_validator_refresh` correctly fires on `diff.platform_node_id.is_some()` (line 257) and `diff.service.is_some()` (line 251), and `refresh_trigger_fires_on_platform_node_id_change` asserts the trigger. The write path is incomplete:
- `ValidatorRefresh` (lines 209-214) only carries `node_ip`, `platform_p2p_port`, `platform_http_port`, `is_banned`.
- `validator_refresh_from_state` (lines 224-234) gates on `state.platform_node_id?` but discards the value rather than returning it.
- `refresh_validator_in_sets_from_state` (lines 62-71) rewrites only those same four fields.
But `ValidatorV0` (packages/rs-dpp/src/core_types/validator/v0/mod.rs) also has `node_id: PubkeyHash` and `core_port: u16`, both initialized from `state.platform_node_id` / `state.service.port()` in `new_validator_if_masternode_in_state` and both used in the Tenderdash-advertised `node_id@host:port` triple.
Consequences:
1. A `platform_node_id`-only diff fires the trigger, `validator_refresh_from_state` returns `Some` (because `state.platform_node_id` is still set), and the validator's `node_id` retains its stale pre-rotation value. Tenderdash advertises `old_node_id@host:port` until a full validator-set rebuild — exactly the bug the new trigger was added to prevent.
2. A Core-service-port change (`DMNStateDiff::service.is_some()`) similarly fires the trigger but the cached `core_port` is never rewritten.
The existing `refresh_trigger_fires_on_platform_node_id_change` test only asserts the trigger predicate; it does not exercise the write path. Fix shape: include `node_id: PubkeyHash` and `core_port: u16` in `ValidatorRefresh`, populate them in `validator_refresh_from_state` (mirroring `new_validator_if_masternode_in_state`), write them in `refresh_validator_in_sets_from_state`, and add a write-path test asserting the updated `node_id`/`core_port` on the validator entry.
| validator_sets | ||
| .iter_mut() | ||
| .for_each(|(_quorum_hash, validator_set)| { | ||
| if let Some(validator) = validator_set.members_mut().get_mut(pro_tx_hash) { | ||
| if let Some(maybe_ban_height) = dmn_state_diff.pose_ban_height { | ||
| // the ban_height was changed | ||
| validator.is_banned = maybe_ban_height.is_some(); | ||
| } | ||
| if let Some(address) = dmn_state_diff.service { | ||
| validator.node_ip = address.ip().to_string(); | ||
| } | ||
|
|
||
| if let Some(p2p_port) = dmn_state_diff.platform_p2p_port { | ||
| validator.platform_p2p_port = p2p_port as u16; | ||
| } | ||
|
|
||
| if let Some(http_port) = dmn_state_diff.platform_http_port { | ||
| validator.platform_http_port = http_port as u16; | ||
| } | ||
| validator.is_banned = refresh.is_banned; | ||
| validator.node_ip = refresh.node_ip.clone(); | ||
| validator.platform_p2p_port = refresh.platform_p2p_port; | ||
| validator.platform_http_port = refresh.platform_http_port; | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: Carried-forward (STILL VALID): validator refresh never writes the rotated platform_node_id (or service core_port)
Re-validated at HEAD 7641204. diff_triggers_validator_refresh correctly fires on diff.platform_node_id.is_some() (line 257) and diff.service.is_some() (line 251), and refresh_trigger_fires_on_platform_node_id_change asserts the trigger. The write path is incomplete:
ValidatorRefresh(lines 209-214) only carriesnode_ip,platform_p2p_port,platform_http_port,is_banned.validator_refresh_from_state(lines 224-234) gates onstate.platform_node_id?but discards the value rather than returning it.refresh_validator_in_sets_from_state(lines 62-71) rewrites only those same four fields.
But ValidatorV0 (packages/rs-dpp/src/core_types/validator/v0/mod.rs) also has node_id: PubkeyHash and core_port: u16, both initialized from state.platform_node_id / state.service.port() in new_validator_if_masternode_in_state and both used in the Tenderdash-advertised node_id@host:port triple.
Consequences:
- A
platform_node_id-only diff fires the trigger,validator_refresh_from_statereturnsSome(becausestate.platform_node_idis still set), and the validator'snode_idretains its stale pre-rotation value. Tenderdash advertisesold_node_id@host:portuntil a full validator-set rebuild — exactly the bug the new trigger was added to prevent. - A Core-service-port change (
DMNStateDiff::service.is_some()) similarly fires the trigger but the cachedcore_portis never rewritten.
The existing refresh_trigger_fires_on_platform_node_id_change test only asserts the trigger predicate; it does not exercise the write path. Fix shape: include node_id: PubkeyHash and core_port: u16 in ValidatorRefresh, populate them in validator_refresh_from_state (mirroring new_validator_if_masternode_in_state), write them in refresh_validator_in_sets_from_state, and add a write-path test asserting the updated node_id/core_port on the validator entry.
source: ['claude', 'codex']
Issue being fixed or feature implemented
Bumps all
rust-dashcoreworkspace deps5c0113e7→64d9267dto pick up rust-dashcore#813 — the DashPay friendship-path fix that honors the account index (m/9'/5'/15'/account'/…) instead of hardcoding0'. That fix is the prerequisite for non-zero / multi-account DashPay contacts in the wallet stack.Bumping two commits past the current pin also pulls in rpc-json#808 (Core 23 nested masternode platform addresses) — a breaking change to
DMNState/DMNStateDiffthat required correctly resolving masternode platform ports in drive-abci.What was done?
rust-dashcoredeps (dashcore,key-wallet*,dash-spv,dashcore-rpc, …) +Cargo.lock.DEPLOYMENT_V24, live on devnets) returns ProTx v3 masternodes whoseprotx listdiffentries zero the deprecated flatplatformP2PPort/platformHTTPPortand carry the real ports only in the new nestedaddressesobject. The flat fields were renamedlegacy_*and resolver accessorsplatform_p2p_address()/platform_http_address()added. drive-abci now resolves ports through the accessors (prefer nestedaddresses, fall back to the non-zero legacy field) everywhere it reads masternode platform ports:From<DMNState> for MasternodeStateV0andnew_validator_if_masternode_in_stateuseplatform_*_address()(Core 22 maps byte-identically; Core 23 entries are no longer dropped from validator sets). The advertised validatornode_ipis taken from the platform p2p host, which is the Core 23 platform host (may differ from the core service IP) and the service IP for a legacy node.update_state_masternode_list_v0): the refresh trigger isdiff_triggers_validator_refresh, an intentional superset of the validity predicatevalidator_refresh_from_state— it fires on a p2p/http port change, anyaddressesdelta (three-stateNone/Some(None)/Some(Some)), and the zeroed-legacy-port /platform_node_iddeltas the port resolvers deliberately drop (a false-positive refresh is idempotent; a false-negative leaves a stale validator advertised). The per-validator update is rebuilt from the post-apply_difffull state viavalidator_refresh_from_state(single source of truth), and the port resolversdiff_platform_{p2p,http}_portuse the nested-first, non-zero legacy fallback so a zeroed v3 legacy port never sets a validator to port0.addresses(or zeroes the ports with no addresses) and the updated state resolves no platform ports (or has noplatform_node_id), the now-invalid HPMN validator is removed from the validator sets rather than left advertising a stale/dead endpoint.MasternodeStateV1) — Core 23 decouples the platform endpoints from the core service address, so the platform host can differ fromservice.ip().MasternodeStateV0stored only the ports, so a normal restart (which loads the masternode list from disk and applies incremental Core diffs) reconstructed the host asservice.ip()and advertised the wrong endpoint for a split-host node until the nextaddressesdiff. A new versionedMasternodeStateV1/MasternodeV1/Masternode::V1persists the platform host, gated to protocol v12 via a newDRIVE_ABCI_STRUCTURE_VERSIONS_V2(masternode: 1).MasternodeStateV0stays byte-identical (v1–v11 keep writing V0); the read path is variant-tag driven (no version gate), so any binary decodes either variant. The serialized platform state lives in the non-hashed aux CF (put_aux), so this format change is never part ofapp_hash— local, non-consensus state that self-heals on the next Coreaddressesdiff.#[allow(deprecated)]is scoped to the few sites that still name alegacy_*field directly (the two diff-port resolvers, the reverseMasternodeState{V0,V1} → DMNStateconstruction, and test fixtures).How Has This Been Tested?
drive-abciunit tests:from_dmn_state_*); reverse-conversion clear (reverse_from_reconstructs_core23_addresses_then_honors_a_clear).diff_*); validity predicate (refresh_resolves_*,refresh_none_*); and the refresh-trigger superset (refresh_trigger_fires_on_zeroed_legacy_port,refresh_trigger_fires_on_platform_node_id_change,refresh_trigger_none_on_unrelated_diff).v1_round_trip_preserves_distinct_platform_host— host preserved through V1 where V0 collapses it to the service IP;v1_legacy_node_falls_back_to_service_ip;v1_masternode_list_item_round_trip_preserves_host).sdk_builder_default_seeds_atomic_to_floorexpectation fix (PR fix(sdk): refresh SDK protocol version to the network's on startup and network switch #3886 floor raise, was feature-branch-only onv3.1-dev).cargo check --workspace --all-targetsclean;clippy -D warningsclean;fmtclean.The split-host design + consensus-safety analysis (4-agent reviewed) is at
docs/CORE23_SPLIT_HOST_PERSISTENCE_SPEC.md.Breaking Changes
None for platform consumers. Masternode platform-port behavior is byte-identical to the current pin for Core 22 (legacy ports). The serialized
MasternodeStateV0format is unchanged; protocol v12 additionally writes a newMasternodeStateV1that persists the Core 23 platform host — this is local, non-consensus aux-CF state (not inapp_hash), readable by any binary that knows the V1 tag, and v1–v11 keep writing V0. Core 23 (v3) entries — which the previous pin mishandled (port0) — now resolve correctly.Notes for reviewers
3f650020, predating the currentv3.1-devpin and not containing feat: make verification functions public #813). Both touch the sameCargo.tomlrev line, so they will conflict — this PR should be the canonical dashcore bump.0→ consensus connectivity failure. Mainnet/testnet are latent (V24NEVER_ACTIVE) until a future Core activation, so the split-hostMasternodeStateV1path is exercised today only on devnets running protocol v12.Checklist
🤖 Generated with Claude Code
Summary by CodeRabbit