Skip to content

fix(rpc-json)!: resolve Core 23 nested platform addresses (breaking field/accessor renames)#808

Open
lklimek wants to merge 9 commits into
devfrom
fix/rpc-json-core23-platform-addresses
Open

fix(rpc-json)!: resolve Core 23 nested platform addresses (breaking field/accessor renames)#808
lklimek wants to merge 9 commits into
devfrom
fix/rpc-json-core23-platform-addresses

Conversation

@lklimek

@lklimek lklimek commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Breaking change. Public rpc-json API changes on DMNState / DMNStateDiff: the raw platform_p2p_port / platform_http_port fields are renamed legacy_platform_p2p_port / legacy_platform_http_port; the resolver accessors are renamed and now return (host, port) (platform_p2p_address() / platform_http_address() -> Option<(String, u32)>); a new public addresses field is added to both structs. See Breaking changes for the consumer migration.

Why this PR exists

  • Problem: rpc-json's DMNState / DMNStateDiff model only the deprecated top-level platformP2PPort / platformHTTPPort keys. With DEPLOYMENT_V24 active, Dash Core 23.1.x returns ProTx version-3 masternodes whose protx listdiff diff entries (updatedMNs) carry legacy platformP2PPort: 0 / platformHTTPPort: 0, while the real ports live only in the new nested addresses object (core_p2p / platform_p2p / platform_https, each an array of "host:port"). rpc-json ignores addresses and so reports the port as 0.

  • What breaks — confirmed live on devnet: drive-abci builds its masternode list by applying these diffs incrementally; the legacy 0 overwrites the stored port → the masternode's platform_p2p_port becomes 0 → it is captured into the validator at quorum creation (platform_types/validator/v0/mod.rs — a validator is dropped only on None, not on 0, so the 0 survives and is cast to u16 0) → the validator is advertised to Tenderdash with port 0 → validator-set divergence / consensus connectivity failure. Reproduced on devnet paloma (Core 23.1.3):

    proTxHash:        63aa0d6fcbe1229ce844a91f74e913f56b9a700ca281948cd303195e2e05757c
    version:          3
    platformP2PPort:  0          ← legacy top-level, zeroed
    platformHTTPPort: 0          ← legacy top-level, zeroed
    addresses.platform_p2p:   ["68.67.122.87:36656"]   ← real port, present
    addresses.platform_https: ["68.67.122.87:1443"]    ← real port, present
    

    The bad value first enters via a state diff (e.g. a PoSe-revival update); the full-state addedMNs entry still reconstructs the legacy port correctly, which is why only the incremental diff path is affected.

  • Impact by network:

    • Affected now: any platform network with DEPLOYMENT_V24 active — i.e. devnets (paloma, active since 2025-07-01). The development line (v3.1-dev, rust-dashcore pin dev) is actively broken there. The v3.0.2 release (pin v0.41.0) is byte-identical in the DMN region — equally code-vulnerable.
    • Mainnet — latent, not yet triggerable: Core hard-codes mainnet (and testnet) DEPLOYMENT_V24.nStartTime = NEVER_ACTIVE (verified in chainparams.cpp for v23.0.0 and v23.1.3), so no mainnet masternode can be version: 3. Verified empirically: a mainnet protx listdiff version histogram is {v1: 1789, v2: 1152, v3: 0}, all 350 Evo nodes carrying correct non-zero ports. This fix must be live in the mainnet release before a future Core schedules a real mainnet V24 activation, or the first v3 masternode upgrade will start corrupting validator ports.

What was done

This crate keeps RPC structs as a plain data-mapping layer, so deserialization stays a bare #[derive(Deserialize)] with no logic in the deserialize path — the legacy fields and the new addresses object land raw — and the legacy-vs-new resolution lives in explicit accessor methods:

  • New MasternodeAddresses { core_p2p, platform_p2p, platform_https: Vec<String> } (serde-default per field; tolerant of unknown purposes), exposed as addresses on both DMNState and DMNStateDiff.
  • The deprecated raw fields are renamed legacy_platform_p2p_port / legacy_platform_http_port (serde wire renames platformP2PPort / platformHTTPPort intact) so callers reach for the accessors instead of the deprecated fields.
  • New accessors return a full (host, port) pair, new-first / legacy-fallback:
    • DMNState::platform_p2p_address(&self) -> Option<(String, u32)>
    • DMNState::platform_http_address(&self) -> Option<(String, u32)>
    • DMNStateDiff::platform_p2p_address(&self) -> Option<(String, u32)>
    • DMNStateDiff::platform_http_address(&self) -> Option<(String, u32)>
    • Rule: prefer the addresses entry (host + port verbatim) when its port is valid (non-zero, in-range); otherwise, on DMNState, fall back to the legacy port paired with the node IP from service (Dash deploys platform services on the masternode's core IP). A diff carries no node IP, so its accessors do not resolve the legacy port — read it via legacy_platform_*_port.
  • Zero ports never surface: 0 is dropped in both the addresses path and the legacy fallback.
  • Ports parse through u16 then widen to u32, rejecting out-of-range values (e.g. "host:70000"). IPv4/IPv6-safe (rsplit_once(':')). Serialize output unchanged.
  • DMNStateDiff::addresses is Option<Option<MasternodeAddresses>> (mirrors pose_ban_height): None = unchanged, Some(None) = cleared, Some(Some(_)) = set. compare_to_newer_dmn_state encodes a clear as Some(None) and apply_diff applies the inner value, so a Some → None transition survives the compare→apply round-trip. A custom serde helper distinguishes absent / null / object on the wire.

Testing

  • RED-first TDD, assertions go through the accessors: legacy-only → legacy port paired with node IP; legacy 0 + addresses → addresses port (the exact devnet failure shape); legacy 0 + no addressesNone; out-of-range port → None; apply_diff propagates addresses into a stored DMNState; Some → None addresses clear survives the round-trip; raw fields remain the untouched wire values.
  • 13/13 rpc-json lib tests green. cargo fmt + cargo clippy --workspace --all-targets --all-features -- -D warnings clean.

Breaking changes

  • Adds a public addresses field to DMNState (Option<MasternodeAddresses>) and DMNStateDiff (Option<Option<MasternodeAddresses>>). Downstream struct literals must add the field or use ... No wire/serialize change; legacy deserialization is unchanged.
  • The raw fields platform_p2p_port / platform_http_port are renamed legacy_platform_p2p_port / legacy_platform_http_port on both DMNState and DMNStateDiff (serde wire names unchanged).
  • The resolver accessors are renamed and now return an address pair: resolved_platform_p2p_port() / resolved_platform_http_port()platform_p2p_address() / platform_http_address() returning Option<(String, u32)>.
  • Consumer action required: to get the correct port (or full address) for Core-23 (ProTx v3) entries, callers must use the new platform_p2p_address() / platform_http_address() accessors instead of reading the raw legacy_platform_*_port fields. The raw fields intentionally still reflect the wire value (which is 0 for v3 entries).

Checklist

  • RED→GREEN tests, fmt, clippy -D warnings clean
  • Backward-compatible deserialization (legacy wire fields unchanged; plain derive)
  • Root cause confirmed against live devnet data (Core 23.1.3, paloma)
  • Self-review + bot-review comments addressed (accessors return (host, port); clearable nested-addresses diff; zero-port → None; port-range hardening; apply_diff regression test)
  • Reviewer confirmation

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features
    • Added support for Dash Core 23+ nested address format for masternode platform service endpoints
    • Introduced new address resolution helpers for platform peer-to-peer and HTTP services
    • Preserved backward compatibility by automatically falling back to legacy port fields when needed
  • Bug Fixes
    • Improved address/port parsing to reject ambiguous IPv6 and enforce valid port ranges
    • Updated diff handling so address updates and clears behave consistently
  • Tests
    • Added coverage for nested-address resolution, fallback behavior, clearing semantics, and edge cases

Dash Core 23.1 moved the platform P2P/HTTPS ports out of the deprecated
top-level platformP2PPort/platformHTTPPort keys into a nested `addresses`
object whose `platform_p2p`/`platform_https` sub-keys hold "host:port"
arrays. Without reading them, platform_p2p_port deserialized to None on
Core-23 entries, dropping or mis-porting validators downstream.

Add MasternodeAddresses and backfill the existing port fields from the
addresses object when the legacy port is absent or zero, via a private
DMNStateIntermediate (mirroring DMNStateDiffIntermediate) and inside the
existing TryFrom for DMNStateDiff. Legacy pre-23 JSON is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7003548-d303-4af0-846f-392a93204dfc

📥 Commits

Reviewing files that changed from the base of the PR and between 7f1b46b and 01ddd0e.

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

📝 Walkthrough

Walkthrough

This PR extends DMNState, DMNStateDiff, and DMNStateDiffIntermediate in rpc-json/src/lib.rs to support Dash Core 23+ nested addresses for platform port resolution. A new MasternodeAddresses struct and parse_host_port helper are introduced. Existing flat platform port fields are deprecated in favour of the nested structure. Diff compare/apply logic and serde deserialization are updated accordingly, and a comprehensive test suite is added.

Changes

Masternode Platform Address Resolution

Layer / File(s) Summary
MasternodeAddresses struct and parse_host_port
rpc-json/src/lib.rs
Defines MasternodeAddresses holding per-purpose Vec<String> arrays and implements parse_host_port rejecting unbracketed IPv6 and out-of-range ports.
DMNState field changes and address accessors
rpc-json/src/lib.rs
Replaces flat platform port fields with deprecated legacy fields plus addresses: Option<MasternodeAddresses>; adds platform_p2p_address() and platform_http_address() with nested-first, legacy-fallback resolution.
DMNStateDiff, DMNStateDiffIntermediate, and serde wiring
rpc-json/src/lib.rs
Updates diff structs with deprecated legacy fields and three-state addresses: Option<Option<MasternodeAddresses>>; adds deserialize_addresses_2opt for wire-level absent/null/object semantics; propagates fields through TryFrom conversion and diff accessors.
compare_to_newer_dmn_state and apply_diff
rpc-json/src/lib.rs
Emits diff entries for legacy port and addresses field changes; apply_diff destructures and persists incoming legacy ports and resolved nested addresses.
Test suite
rpc-json/src/lib.rs
Covers Core 23+ resolution, diff-only resolution, legacy fallback, zero ports, out-of-range ports, addresses: null clearing, compare/apply round-trips, and parse_host_port IPv6 rules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested labels

ready-for-review

Suggested reviewers

  • QuantumExplorer
  • ZocoLini

Poem

🐇 Hop, hop! The ports have nested deep,
In addresses arrays they now sleep.
Legacy fields marked with #[deprecated] flair,
IPv6 brackets checked with extra care.
Three-state diffs applied with glee —
The masternode state flows, wild and free! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: resolving Dash Core 23 nested platform addresses with a breaking change to field/accessor names.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rpc-json-core23-platform-addresses

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 and usage tips.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.37662% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.13%. Comparing base (4d529e4) to head (01ddd0e).

Files with missing lines Patch % Lines
rpc-json/src/lib.rs 98.37% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #808      +/-   ##
==========================================
+ Coverage   72.94%   73.13%   +0.18%     
==========================================
  Files         323      323              
  Lines       71989    72283     +294     
==========================================
+ Hits        52511    52861     +350     
+ Misses      19478    19422      -56     
Flag Coverage Δ
core 76.75% <ø> (ø)
ffi 46.99% <ø> (ø)
rpc 33.05% <98.37%> (+13.04%) ⬆️
spv 90.21% <ø> (-0.11%) ⬇️
wallet 71.64% <ø> (ø)
Files with missing lines Coverage Δ
rpc-json/src/lib.rs 64.61% <98.37%> (+24.28%) ⬆️

... and 5 files with indirect coverage changes

…erialize backfill

Match the plain-derive style of the rest of the crate: deserialize DMNState and
DMNStateDiff with no logic in the deserialize path. The nested Core 23 `addresses`
object now lands as a plain public field instead of being folded into the legacy
port fields during deserialization.

Port resolution moves into accessor methods on both DMNState and DMNStateDiff:
resolved_platform_p2p_port / resolved_platform_http_port. They check the new
`addresses` object first (first non-zero, parseable port) and fall back to the
deprecated top-level platformP2PPort/platformHTTPPort — new-first, legacy-fallback.

Removed: the DMNStateIntermediate mirror struct and its `#[serde(from)]`, the
`backfill_port` helper, and the in-deserialize backfill blocks in both From/TryFrom
impls. DMNState regains its plain `#[derive(Deserialize)]`; DMNStateDiff keeps its
pre-existing `try_from` with `addresses` flowing through as raw data. apply_diff now
propagates `addresses` so a stored DMNState stays resolvable after diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lklimek lklimek marked this pull request as ready for review June 12, 2026 09:44
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

PR cleanly addresses Core 23+ nested platform port resolution via a new MasternodeAddresses mirror plus new-first/legacy-fallback accessors on DMNState/DMNStateDiff. Two in-scope suggestions: the addresses diff round-trip cannot clear nested addresses (compare emits None, apply ignores None), and there is no end-to-end test for apply_diff propagating addresses — the very path that motivates the PR. One nitpick on parse_port port-range validation.

🟡 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 `rpc-json/src/lib.rs`:
- [SUGGESTION] rpc-json/src/lib.rs:2375-2446: Diff round-trip cannot clear nested `addresses`
  `compare_to_newer_dmn_state` sets `has_diff = true` and stores `newer.addresses.clone()` whenever `self.addresses != newer.addresses` (lines 2375-2380). When the transition is `Some(_) -> None`, the resulting `DMNStateDiff.addresses` is `None`, which is indistinguishable from "no change". `apply_diff` then guards with `if addresses.is_some()` (lines 2444-2446), so the stored `addresses` is never cleared and the compare→apply round-trip silently loses the transition. Because `resolved_platform_p2p_port()`/`resolved_platform_http_port()` prefer nested addresses over legacy fields, stale ports can persist after the round-trip. The same `Option<T>` pattern is reused for `platform_p2p_port`/`platform_http_port` and has the same limitation, but it is more impactful now that port resolution depends on `addresses`. Either model `addresses` as `Option<Option<MasternodeAddresses>>` (matching `pose_ban_height` / `operator_payout_address`) so absence and clearing are distinct, or document the limitation and add a test pinning the chosen behavior.
- [SUGGESTION] rpc-json/src/lib.rs:2389-2447: No test exercises `apply_diff` propagating `addresses` (the path the PR exists to fix)
  The PR's stated production failure is `protx listdiff updatedMNs` entries with `platformP2PPort: 0` + nested `addresses` being merged into a long-lived stored `DMNState`. The added tests at lines 3540-3653 cover (1) `DMNState` deserialization, (2) `DMNStateDiff` deserialization, and (3) accessor resolution rules in isolation — but none call `DMNState::apply_diff` with a diff carrying `addresses` and then assert `resolved_platform_p2p_port()` on the merged state. Add a regression test: start with a stored `DMNState` (legacy ports `Some(0)`, `addresses: None`), apply a `DMNStateDiff { addresses: Some(...platform_p2p: ["host:36656"]...), .. }`, and assert `resolved_platform_p2p_port() == Some(36656)`. Without it, a future refactor of `apply_diff` could silently drop `addresses` propagation again and reintroduce the exact devnet incident.

Comment thread rpc-json/src/lib.rs
Comment thread rpc-json/src/lib.rs Outdated
Comment thread rpc-json/src/lib.rs Outdated
lklimek and others added 2 commits June 12, 2026 12:42
…learable diff

Address self-review on the Core 23 platform-address work. Rename the raw legacy
fields on DMNState and DMNStateDiff to legacy_platform_p2p_port /
legacy_platform_http_port (serde wire renames intact) so callers reach for the
accessors instead of the deprecated fields.

The resolver accessors become platform_p2p_address / platform_http_address and
return Option<(String, u32)> instead of a bare port. On DMNState the legacy
fallback pairs the port with the node IP from `service`, since Dash deploys
platform services on the masternode's core IP; on DMNStateDiff there is no node
IP to pair with, so the legacy path is not resolved. Zero ports never surface:
they are dropped in both the addresses path and the legacy fallback. Ports parse
through u16 then widen to u32, rejecting out-of-range values like "host:70000".

DMNStateDiff::addresses becomes Option<Option<MasternodeAddresses>>, mirroring
pose_ban_height: None = unchanged, Some(None) = cleared, Some(Some(_)) = set. A
custom serde helper distinguishes absent / null / object on the wire.
compare_to_newer_dmn_state now encodes a Some->None clear as Some(None) and
apply_diff applies the inner value, so the clear survives the round-trip.

Tests cover diff addresses propagation through apply_diff, the Some->None clear
round-trip, zero-port-to-None, and out-of-range rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…resses' into fix/rpc-json-core23-platform-addresses
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 12, 2026
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Jun 12, 2026
@lklimek lklimek changed the title fix: read Core 23 nested platform addresses in rpc-json DMNState fix(rpc-json)!: resolve Core 23 nested platform addresses (breaking field/accessor renames) Jun 12, 2026
@github-actions github-actions Bot removed the ready-for-review CodeRabbit has approved this PR label Jun 12, 2026
@lklimek

lklimek commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the list of changed files in this PR
gh pr view 808 --repo dashpay/rust-dashcore --json files --jq '.files[].path'

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Get the diff for this PR
gh pr diff 808 --repo dashpay/rust-dashcore

Length of output: 23990



@lklimek — Full review of PR #808 below.


Overall Assessment

This is a well-structured fix. The root cause is clearly identified, the design (bare-data deserialization layer + explicit accessor resolution) is appropriate, the three-state Option<Option<_>> diff encoding is correct, and the tests exercise every meaningful case including the exact devnet failure shape. A few minor items are worth addressing before merging.


Issues Found

1. Legacy port fallback has no range validation (minor)

File: rpc-json/src/lib.rsDMNState::legacy_platform_address

fn legacy_platform_address(&self, port: Option<u32>) -> Option<(String, u32)> {
    port.filter(|&p| p != 0).map(|p| (self.service.ip().to_string(), p))
}

The addresses path rejects out-of-range ports via port.parse::<u16>(), but the legacy path only filters 0. A wire value of, say, 70000 would be accepted and returned. This is not a regression (the old code was the same), but the inconsistency is now visible because the two paths sit next to each other. Suggest adding p <= u32::from(u16::MAX) to the filter, or reusing the same u16-bounded parse helper:

fn legacy_platform_address(&self, port: Option<u32>) -> Option<(String, u32)> {
    port.filter(|&p| p != 0 && p <= u32::from(u16::MAX))
        .map(|p| (self.service.ip().to_string(), p))
}

2. DMNStateDiff direct Deserialize can't distinguish null (clear) from absent (unchanged)

File: rpc-json/src/lib.rsDMNStateDiff

DMNStateDiff derives Deserialize without the deserialize_addresses_2opt helper:

#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
pub struct DMNStateDiff {
    // ...
    pub addresses: Option<Option<MasternodeAddresses>>,  // no deserialize_with here
}

Under the plain derived Deserialize, a JSON null for addresses yields None (outer), not Some(None) — the three-state semantics are lost. The production path goes through DMNStateDiffIntermediate which has the correct custom deserializer, so there is no runtime bug. But the #[derive(Deserialize)] on DMNStateDiff is misleading — a caller who deserializes a DMNStateDiff directly from wire JSON (e.g., a future test or integration path) will silently get wrong semantics for a null clear.

Options:

  • Add #[serde(default, deserialize_with = "deserialize_addresses_2opt")] to DMNStateDiff.addresses as well, or
  • Add a // NOTE: deserialize via DMNStateDiffIntermediate comment on the type to document the intended path, or
  • If direct deserialization is never intended, remove Deserialize from DMNStateDiff entirely.

3. first_valid_host_port is a static method but doesn't use the receiver (nitpick)

impl MasternodeAddresses {
    fn first_valid_host_port(addrs: &[String]) -> Option<(String, u32)> { ... }
}

This is an associated function (no self) that only uses its argument. Moving it to a free function alongside parse_host_port would be more idiomatic, and the two parsing utilities would sit together. Not a blocking concern.


Positive Notes

  • rsplit_once(':') for IPv4/bracketed-IPv6 safety is the right approach and is well-documented.
  • Some(newer.addresses.clone()) in compare_to_newer_dmn_state correctly wraps None as Some(None) for the clear case.
  • if let Some(addresses) = addresses { self.addresses = addresses; } in apply_diff cleanly handles all three states of Option<Option<_>>.
  • #[deprecated] on the raw DMNStateDiff fields guides consumers toward the accessors without a hard break — good ergonomics.
  • The deserialize_addresses_2opt helper is the canonical double-Option serde pattern and is correctly paired with #[serde(default)].
  • Test coverage is excellent: all nine cases, including the exact devnet failure shape (platformP2PPort: 0 + addresses present) and the Some → None clear round-trip.
  • No zero port ever surfaces from any accessor — both paths are guarded.

Summary Table

Item Severity Required?
Legacy port not range-validated in legacy_platform_address Minor Suggested
DMNStateDiff direct Deserialize loses three-state semantics for null Minor Suggested
first_valid_host_port as static vs free function Nitpick Optional

The first two are worth addressing for correctness/clarity. The rest is approval-ready.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Targeted, well-scoped fix for the Core 23 nested-addresses regression. The accessor-based design correctly preserves the wire shape and the three-state Option<Option<MasternodeAddresses>> round-trips through compare/apply. Verified findings are all non-blocking: a #[deprecated] annotation asymmetry between DMNState and DMNStateDiff (introduced by c427684), a legacy-port accessor that bypasses the documented in-range invariant, a missing wire-level test for addresses: null, and a doc/behavior mismatch in parse_host_port for unbracketed IPv6.

🟡 3 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 `rpc-json/src/lib.rs`:
- [SUGGESTION] rpc-json/src/lib.rs:2121-2124: Mirror `#[deprecated]` on DMNState legacy port fields
  Commit c4276848 added `#[deprecated(note = "...")]` to `DMNStateDiff::legacy_platform_p2p_port`/`legacy_platform_http_port` (2181-2184) but the same-named fields on `DMNState` (2121-2124) are left bare. Both hold the identical wire value and require the same migration (call `platform_p2p_address()` / `platform_http_address()` instead). The asymmetry is the worst of both worlds: a consumer reading `state.legacy_platform_p2p_port` gets no compiler nudge while a consumer reading the equivalent diff field does. `DMNState` is the more commonly-held type for downstream callers, so this defeats the migration signal where it matters most. Either deprecate both or neither.
- [SUGGESTION] rpc-json/src/lib.rs:2159-2161: Legacy port fallback bypasses the accessor's in-range invariant
  `platform_p2p_address` / `platform_http_address` document that they return non-zero in-range ports, and the nested-addresses path enforces this by parsing through `u16`. The legacy fallback at `legacy_platform_address` only filters out `0`, so a wire value like `platformP2PPort: 70000` is returned as `Some((service_ip, 70000))`. Callers that trust the accessor contract and narrow the result to `u16` would silently truncate, reintroducing exactly the class of bad endpoint values these accessors are meant to prevent. Fold the legacy port through `u16::try_from` so both paths share one invariant.
- [SUGGESTION] rpc-json/src/lib.rs:3388-3400: Add a wire-level test for `addresses: null` deserialization
  `deserialize_addresses_2opt` exists specifically to distinguish absent (None = unchanged), JSON `null` (Some(None) = cleared), and an object (Some(Some(_)) = set). The added tests cover absent (`dmn_state_legacy_*`) and object (`dmn_state_core23_*`), and exercise `Some(None)` only programmatically through `compare_to_newer_dmn_state` → `apply_diff` (`dmn_state_diff_clears_addresses`). The wire-format path the helper was written for — a `DMNStateDiff` JSON payload with `"addresses": null` — is not asserted. A future refactor (e.g. swapping the custom helper for plain `#[serde(default)]`, which would regress to None = unchanged) would not fail any test, and the bug would only surface when stored `addresses` silently stayed populated. A one-shot deserialize assertion locks the contract.

Comment thread rpc-json/src/lib.rs
Comment thread rpc-json/src/lib.rs
Comment thread rpc-json/src/lib.rs
Comment thread rpc-json/src/lib.rs
…DMNState fields

Address a fresh review pass on the Core 23 platform-address work.

- Mirror the `#[deprecated]` annotation onto DMNState::legacy_platform_p2p_port /
  legacy_platform_http_port so both DMNState and DMNStateDiff signal the same
  deprecation; add `#[allow(deprecated)]` to the two accessor methods, the
  apply_diff write sites, and the tests that read the fields to keep
  `clippy -D warnings` green.
- legacy_platform_address now validates through `u16::try_from`, so an
  out-of-range legacy port (e.g. platformP2PPort: 70000) yields None instead of
  Some((ip, 70000)), matching the addresses path and the accessor's documented
  in-range contract.
- parse_host_port rejects unbracketed multi-colon (bare IPv6) hosts rather than
  silently mangling them via rsplit_once; bracketed IPv6 and IPv4 still parse.
  The doc now states the supported forms accurately.

Tests: wire-level `addresses: null` -> Some(None) (and absent -> None) round
trip, out-of-range legacy port rejection, and parse_host_port IPv6 handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
rpc-json/src/lib.rs (1)

3032-3035: ⚡ Quick win

Mirror the legacy-port deprecation on the public intermediate type.

DMNStateDiffIntermediate is public, so consumers can still use these raw legacy fields without the warning they get on DMNState/DMNStateDiff. Add the same #[deprecated] notes here, with scoped #[allow(deprecated)] at internal conversion sites if needed.

Proposed update
+    #[deprecated(note = "Core 23+ nested addresses.platform_p2p should be used instead")]
     #[serde(default, rename = "platformP2PPort")]
     pub legacy_platform_p2p_port: Option<u32>,
+    #[deprecated(note = "Core 23+ nested addresses.platform_https should be used instead")]
     #[serde(default, rename = "platformHTTPPort")]
     pub legacy_platform_http_port: Option<u32>,
🤖 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 `@rpc-json/src/lib.rs` around lines 3032 - 3035, Add deprecation warnings to
the legacy port fields in `DMNStateDiffIntermediate` to match the deprecation
already applied to similar fields in `DMNState`/`DMNStateDiff`. Add
`#[deprecated]` attributes to both `legacy_platform_p2p_port` and
`legacy_platform_http_port` fields with appropriate deprecation messages. Then
identify any internal code that constructs or uses these fields from
`DMNStateDiffIntermediate` and wrap those usages with `#[allow(deprecated)]`
scoped attributes to suppress the warnings at conversion sites.
🤖 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 `@rpc-json/src/lib.rs`:
- Around line 2398-2418: The legacy_platform_p2p_port and
legacy_platform_http_port fields in the diff comparison (at rpc-json/src/lib.rs
lines 2398-2418 and 2479-2490) are storing None when values differ, but
apply_diff treats None as unchanged, leaving stale legacy ports in the state. To
fix this, either adopt the same clearable diff shape used for the addresses
field by changing the type to Option<Option<u32>> with matching serde handling
to support wire-level null clearing, or if legacy-port clears are intentionally
unsupported, add an explicit regression test documenting this limitation. Apply
the chosen solution consistently across both affected locations.
- Around line 2088-2094: The `parse_host_port` function currently accepts empty
host strings (e.g., ":36656" returns Some(("", 36656))), which should be
rejected as invalid input. Add a validation check after extracting the host from
the rsplit_once call to ensure the host is not empty, returning None if it is.
Place this check alongside the existing IPv6 bracket validation logic in the
function to treat empty hosts the same as malformed input.

---

Nitpick comments:
In `@rpc-json/src/lib.rs`:
- Around line 3032-3035: Add deprecation warnings to the legacy port fields in
`DMNStateDiffIntermediate` to match the deprecation already applied to similar
fields in `DMNState`/`DMNStateDiff`. Add `#[deprecated]` attributes to both
`legacy_platform_p2p_port` and `legacy_platform_http_port` fields with
appropriate deprecation messages. Then identify any internal code that
constructs or uses these fields from `DMNStateDiffIntermediate` and wrap those
usages with `#[allow(deprecated)]` scoped attributes to suppress the warnings at
conversion sites.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69ac54fd-1649-46a7-adfc-d0764dc3d36f

📥 Commits

Reviewing files that changed from the base of the PR and between 4d529e4 and 7f1b46b.

📒 Files selected for processing (1)
  • rpc-json/src/lib.rs

Comment thread rpc-json/src/lib.rs
Comment thread rpc-json/src/lib.rs
…ediate legacy ports

Addresses CodeRabbit review on PR #808:
- parse_host_port now rejects empty hosts (":36656" -> None)
- DMNStateDiffIntermediate legacy_platform_*_port mirror the #[deprecated] of DMNState/DMNStateDiff

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants