Skip to content

fix(dpp)!: make platform/orchard address decoders network-agnostic#3781

Open
Claudius-Maginificent wants to merge 4 commits into
v3.1-devfrom
fix/platform-address-hrp-network-decode-p
Open

fix(dpp)!: make platform/orchard address decoders network-agnostic#3781
Claudius-Maginificent wants to merge 4 commits into
v3.1-devfrom
fix/platform-address-hrp-network-decode-p

Conversation

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator

@Claudius-Maginificent Claudius-Maginificent commented Jun 2, 2026

Issue being fixed or feature implemented

PlatformAddress::from_bech32m_string / OrchardAddress::from_bech32m_string returned (Self, Network), deriving Network from the bech32m HRP. The HRP tdash is shared by Testnet/Devnet/Regtest (hrp_for_network is non-injective), so the decoder could not truthfully determine the network and always returned Network::Testnet for tdash.

On devnet (paloma) a valid devnet platform address decoded as Testnet, tripping the wrong-network guard in the wallet's shielded_unshield_to and blocking unshield-to-transparent.

Note — strict network comparison is an easy trap. Because the bech32m HRP tdash is shared by Testnet/Devnet/Regtest, code that decodes a platform address to a specific Network and then compares it for strict equality against the wallet's network silently rejects valid devnet/regtest addresses — they decode as Testnet, so Testnet != Devnet fails and the address is refused. Example of this pattern on v3.1-dev (the strict guard this PR removes): platform_wallet.rs L628–L640. Option P avoids the trap entirely: the decoder no longer derives a network from the HRP, and the wallet compares HRP class instead — so a tdash address is accepted by any non-mainnet wallet.

What was done?

Option P — network-agnostic decoders. A PlatformAddress/OrchardAddress carries no network internally (network is supplied only at to_bech32m_string(network) encode time), so:

  • from_bech32m_string(s) -> Result<Self, ProtocolError> validates the HRP is a recognized platform HRP (dash/tdash) but makes no network claim.
  • The wrong-network safety check moved to the one caller that needs it, shielded_unshield_to, via a private testable helper check_recipient_hrp(recipient, network) that compares the recipient's HRP class against the wallet network (PlatformAddress::hrp_for_network): rejects mainnet dash1… on a tdash wallet, accepts devnet tdash1… on a devnet wallet, and reports a clear "not a platform address" error for non-platform HRPs (e.g. bc1…).
  • No new public API.
  • Also included: test(dpp): feature-gate signing_tests behind shielded-client — fixes a pre-existing v3.1-dev compile failure where signing_tests imported crate::shielded::builder (only present under shielded-client), which broke cargo test -p dpp.

How Has This Been Tested?

  • cargo test -p dpp --lib address_funds → 161 passed (incl. test_bech32m_invalid_hrp_fails)
  • cargo test -p dpp --lib --features shielded-client address_funds::orchard → 8 passed
  • cargo test -p platform-wallet --features shielded check_recipient_hrp_tests → 9 passed (mainnet→testnet/devnet REJECT, devnet→devnet ACCEPT, testnet→testnet ACCEPT, tdash cross-acceptance, uppercase ACCEPT, bc1… not-a-platform-address, no-separator/empty Err)
  • cargo check + cargo clippy -- -D warnings clean on dpp, platform-wallet (--features shielded), wasm-dpp2 (changed files)
  • Live (paloma devnet): devnet platform address now accepted by shielded_unshield_to (previously rejected as wrong-network).

Breaking Changes

from_bech32m_string return type changes (Self, Network)Self (hence fix(dpp)!). It is not in any prelude — external consumers reach it only via dpp::address_funds::PlatformAddress/OrchardAddress, so the break is a loud compile error, never silent.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • Refactor

    • Address decoding is now network-agnostic; wallets perform network validation before use.
    • Shielded transaction flow updated to validate recipient network class earlier, with clearer error messages.
  • New Features

    • Utility to classify platform addresses as mainnet vs non-mainnet from their encoding.
  • Tests

    • Added and adjusted tests for address classification, case handling, and shielded transaction guards.

lklimek and others added 2 commits June 2, 2026 15:44
`signing_tests` for `ShieldFromAssetLockTransition` imports
`crate::shielded::builder`, which only exists under `shielded-client`,
but the module was gated on `state-transition-signing` + `core_key_wallet`
only. With `core_key_wallet` on but `shielded-client` off, the dpp lib-test
harness failed to compile (`unresolved import crate::shielded::builder`).

Add `feature = "shielded-client"` to the module's gate (both the `mod`
declaration and the file's inner `#![cfg]`) so the test only compiles when
its `builder` dependency is available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`PlatformAddress::from_bech32m_string` and
`OrchardAddress::from_bech32m_string` previously returned `(Self, Network)`,
deriving the Network from the bech32m HRP. That value was untruthful: the
`tdash` HRP is shared by Testnet, Devnet, and Regtest, so the decoder
hardcoded `tdash → Testnet` and mis-reported devnet/regtest addresses as
Testnet — which broke devnet (a devnet platform address tripped a
wrong-network guard).

A `PlatformAddress`/`OrchardAddress` is network-agnostic internally; the
network is supplied only at `to_bech32m_string(network)` encode time. Both
decoders now make no network claim: they validate the HRP is a recognized
platform HRP (`dash`/`tdash`) and return just `Self`.

The wrong-network safety check moves to the one caller that needs it,
`platform-wallet::shielded_unshield_to`, as an HRP-class comparison: the
recipient's HRP (segment before the final bech32 `1` separator) is matched
case-insensitively against `hrp_for_network(wallet.network)`, catching e.g.
a mainnet `dash1…` address pasted into a `tdash` wallet. `shielded_withdraw_to`
uses a separate Base58 core-address parser and is unaffected.

BREAKING: `from_bech32m_string` return type changed `(Self, Network)` → `Self`.

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

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Address parsing was changed to be network-agnostic: OrchardAddress and PlatformAddress parsers now return only decoded addresses and validate HRPs; PlatformAddress gained an HRP-based mainnet classifier. Wallet code now enforces network checks before decoding. Tests and WASM bindings updated; shielded test gates tightened.

Changes

Network-agnostic address parsing with wallet-level validation

Layer / File(s) Summary
Address parsing refactor and HRP classifier
packages/rs-dpp/src/address_funds/orchard_address.rs, packages/rs-dpp/src/address_funds/platform_address.rs
OrchardAddress and PlatformAddress from_bech32m_string now return only the decoded address and validate the HRP as a recognized platform HRP; removed HRP→Network inference. Added PlatformAddress::is_mainnet_bech32m. Updated roundtrip and edge-case tests to match new return types.
Wallet-level HRP/network validation
packages/rs-platform-wallet/src/wallet/platform_wallet.rs
shielded_unshield_to runs check_recipient_hrp (uses PlatformAddress::is_mainnet_bech32m) before decoding and errors on mainnet/non-mainnet mismatch; documentation updated and feature-gated tests added for acceptance/rejection and malformed inputs.
Test feature gate updates
packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs, packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
Shielded signing test modules now require the shielded-client feature in addition to existing feature flags for compilation.
WASM binding updates
packages/wasm-dpp2/src/platform_address/address.rs
WASM PlatformAddressWasm TryFrom/fromBech32m updated to accept the new parse return type and removed unused network tuple destructuring.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • dashpay/platform#3753: Overlapping changes to shield_from_asset_lock_transition tests and related test wiring.

Suggested labels

ready for final review

Suggested reviewers

  • QuantumExplorer
  • shumkov

"🐰
I hop through HRPs with nimble feet,
Parsing clean where networks meet.
Decode the bits, let wallets check,
No tuple baggage left to wreck.
A jaunty hop — the ledger's neat!"

🚥 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 directly and accurately describes the main change: making platform and orchard address decoders network-agnostic by removing Network derivation from bech32m HRP parsing.
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/platform-address-hrp-network-decode-p

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

lklimek and others added 2 commits June 2, 2026 16:32
…ctor

Restores the truthful subset of the network information that the
network-agnostic `from_bech32m_string` decoder no longer returns. Per
DIP-0018, a bech32m platform address carries no network byte and the prefix
`tdash` is shared by Testnet/Devnet/Regtest, so the only network fact
recoverable from an address string is mainnet vs non-mainnet.

`PlatformAddress::is_mainnet_bech32m(s) -> Result<bool, ProtocolError>`
classifies by HRP alone (no payload decode): `dash` → Ok(true), `tdash` →
Ok(false), a non-platform HRP or a malformed/separator-less string → Err.
Comparison is case-insensitive (bech32m permits all-uppercase).

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

`check_recipient_hrp` no longer inlines the bech32 HRP extraction and
HRP-class comparison; it delegates network classification (and
malformed/non-platform rejection) to `PlatformAddress::is_mainnet_bech32m`,
making the dpp detector the single source of truth. The guard reduces to
`addr_is_mainnet != (network == Network::Mainnet)` — exactly the previous
HRP-class check (mainnet wallet requires `dash`, any non-mainnet wallet
requires `tdash`).

Behavior-preserving: the existing `check_recipient_hrp_tests` (network
mismatch, tdash cross-acceptance, uppercase, non-platform, missing-separator,
empty) pass unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)))
}
};
if hrp_lower != PLATFORM_HRP_MAINNET && hrp_lower != PLATFORM_HRP_TESTNET {
Copy link
Copy Markdown
Contributor

@lklimek lklimek Jun 2, 2026

Choose a reason for hiding this comment

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

Looks like we use the same validation multiple times. DRY. Maybe just call is_mainnet_bech32m() ?

)))
}
};
if hrp_lower != PLATFORM_HRP_MAINNET && hrp_lower != PLATFORM_HRP_TESTNET {
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.

Looks like we use the same validation multiple times. DRY. Maybe just call is_mainnet_bech32m() ?

/// [`Self::to_bech32m_string`] encode time. The HRP is validated to be a
/// recognized platform HRP (`dash`/`tdash`), but no network is inferred —
/// `tdash` is shared by Testnet/Devnet/Regtest, so the HRP cannot identify
/// the network. Callers needing a network guard must enforce it themselves.
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.

Make the comment shorter, references to to_bench32m_string() are not needed here, and add reference to is_mainnet_bech32m()

/// NOTE: This expects bech32m type bytes (0xb0/0x80) in the encoded string,
/// NOT the storage type bytes (0x00/0x01) used in GroveDB keys.
///
/// A `PlatformAddress` is network-agnostic: the network is supplied only at
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.

Make the comment shorter, references to to_bench32m_string() are not needed here, and add reference to is_mainnet_bech32m()

@lklimek lklimek marked this pull request as ready for review June 2, 2026 15:16
@lklimek lklimek requested a review from QuantumExplorer as a code owner June 2, 2026 15:16
@lklimek
Copy link
Copy Markdown
Contributor

lklimek commented Jun 2, 2026

@coderabbitai review all

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

@lklimek Sure, I'll kick off a full review of all the changes in this PR now!

✅ Actions performed

Full review triggered.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Jun 2, 2026

✅ Review complete (commit 71052a4)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates platform/orchard bech32m address decoding to be network-agnostic (since tdash is shared by Testnet/Devnet/Regtest), and moves the network-class guard to the wallet caller that needs it.

Changes:

  • Change PlatformAddress::from_bech32m_string / OrchardAddress::from_bech32m_string to return only the decoded address (no derived Network) and validate only that HRP is a recognized platform HRP.
  • Add PlatformAddress::is_mainnet_bech32m and use it in rs-platform-wallet to enforce mainnet vs non-mainnet recipient HRP class in shielded_unshield_to, with targeted unit tests.
  • Feature-gate shield_from_asset_lock_transition signing tests behind shielded-client; update wasm wrapper call sites for the new decoder return type.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/wasm-dpp2/src/platform_address/address.rs Update wasm bindings to handle from_bech32m_string returning only PlatformAddress.
packages/rs-platform-wallet/src/wallet/platform_wallet.rs Move wrong-network enforcement to wallet via check_recipient_hrp + add tests.
packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs Gate signing tests behind shielded-client feature to avoid compile failures.
packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs Gate inclusion of signing tests behind shielded-client feature.
packages/rs-dpp/src/address_funds/platform_address.rs Make platform bech32m decoder network-agnostic; add is_mainnet_bech32m helper + tests; update parsing/tests for new return type.
packages/rs-dpp/src/address_funds/orchard_address.rs Make orchard bech32m decoder network-agnostic; update tests for new return type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1139 to +1141
let addr_is_mainnet = PlatformAddress::is_mainnet_bech32m(recipient).map_err(|e| {
PlatformWalletError::ShieldedBuildError(format!("invalid platform address: {e}"))
})?;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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-dpp/src/address_funds/platform_address.rs`:
- Around line 318-338: The is_mainnet_bech32m function currently only splits on
the last '1' and checks HRP, allowing malformed strings to pass; update
is_mainnet_bech32m to fully validate the bech32m string before classifying the
HRP: attempt to decode the input using the bech32 decoder (or the project’s
existing bech32/bech32m utility) and ensure the variant is Bech32m and the data
part is non-empty, then check hrp.eq_ignore_ascii_case(PLATFORM_HRP_MAINNET) or
PLATFORM_HRP_TESTNET and return Ok(true/false); on any decode/validation failure
return ProtocolError::DecodingError (keep check_recipient_hrp behavior unchanged
except it will now see validated addresses).
🪄 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: e5e0798f-49eb-4fcc-a3f2-093fc94f6e9f

📥 Commits

Reviewing files that changed from the base of the PR and between e01b6a3 and 71052a4.

📒 Files selected for processing (6)
  • packages/rs-dpp/src/address_funds/orchard_address.rs
  • packages/rs-dpp/src/address_funds/platform_address.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
  • packages/rs-platform-wallet/src/wallet/platform_wallet.rs
  • packages/wasm-dpp2/src/platform_address/address.rs

Comment on lines +318 to +338
pub fn is_mainnet_bech32m(s: &str) -> Result<bool, ProtocolError> {
let hrp = s
.rsplit_once('1')
.map(|(hrp, _)| hrp)
.filter(|h| !h.is_empty())
.ok_or_else(|| {
ProtocolError::DecodingError(
"invalid platform address: missing bech32 separator".to_string(),
)
})?;

if hrp.eq_ignore_ascii_case(PLATFORM_HRP_MAINNET) {
Ok(true)
} else if hrp.eq_ignore_ascii_case(PLATFORM_HRP_TESTNET) {
Ok(false)
} else {
Err(ProtocolError::DecodingError(format!(
"not a platform address: HRP '{hrp}' is neither \
'{PLATFORM_HRP_MAINNET}' nor '{PLATFORM_HRP_TESTNET}'"
)))
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the string before classifying it as bech32m.

This helper currently just splits on the last 1 and checks the HRP, so malformed inputs like dash1! still return Ok(true). That makes the public *_bech32m contract too permissive, and downstream check_recipient_hrp() can end up surfacing a misleading network-mismatch error before the actual decoder rejects the address.

Proposed fix
 pub fn is_mainnet_bech32m(s: &str) -> Result<bool, ProtocolError> {
-    let hrp = s
-        .rsplit_once('1')
-        .map(|(hrp, _)| hrp)
-        .filter(|h| !h.is_empty())
-        .ok_or_else(|| {
-            ProtocolError::DecodingError(
-                "invalid platform address: missing bech32 separator".to_string(),
-            )
-        })?;
-
-    if hrp.eq_ignore_ascii_case(PLATFORM_HRP_MAINNET) {
+    let (hrp, _) =
+        bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{e}")))?;
+
+    if hrp.as_str().eq_ignore_ascii_case(PLATFORM_HRP_MAINNET) {
         Ok(true)
-    } else if hrp.eq_ignore_ascii_case(PLATFORM_HRP_TESTNET) {
+    } else if hrp.as_str().eq_ignore_ascii_case(PLATFORM_HRP_TESTNET) {
         Ok(false)
     } else {
         Err(ProtocolError::DecodingError(format!(
-            "not a platform address: HRP '{hrp}' is neither \
+            "not a platform address: HRP '{}' is neither \
                  '{PLATFORM_HRP_MAINNET}' nor '{PLATFORM_HRP_TESTNET}'"
+            , hrp
         )))
     }
 }
🤖 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 `@packages/rs-dpp/src/address_funds/platform_address.rs` around lines 318 -
338, The is_mainnet_bech32m function currently only splits on the last '1' and
checks HRP, allowing malformed strings to pass; update is_mainnet_bech32m to
fully validate the bech32m string before classifying the HRP: attempt to decode
the input using the bech32 decoder (or the project’s existing bech32/bech32m
utility) and ensure the variant is Bech32m and the data part is non-empty, then
check hrp.eq_ignore_ascii_case(PLATFORM_HRP_MAINNET) or PLATFORM_HRP_TESTNET and
return Ok(true/false); on any decode/validation failure return
ProtocolError::DecodingError (keep check_recipient_hrp behavior unchanged except
it will now see validated addresses).

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 91.20879% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.19%. Comparing base (e01b6a3) to head (71052a4).

Files with missing lines Patch % Lines
...ckages/rs-dpp/src/address_funds/orchard_address.rs 60.00% 4 Missing ⚠️
...kages/rs-dpp/src/address_funds/platform_address.rs 95.06% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           v3.1-dev    #3781   +/-   ##
=========================================
  Coverage     87.18%   87.19%           
=========================================
  Files          2624     2624           
  Lines        321014   321066   +52     
=========================================
+ Hits         279892   279940   +48     
- Misses        41122    41126    +4     
Components Coverage Δ
dpp 87.74% <91.20%> (+<0.01%) ⬆️
drive 86.05% <ø> (ø)
drive-abci 89.54% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 47.85% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Pinned commit 71052a4 is a small, behavior-preserving refactor that routes check_recipient_hrp through the existing PlatformAddress::is_mainnet_bech32m helper to centralize network classification. The diff is a clean inversion (17 added / 29 removed) and the existing check_recipient_hrp_tests continue to cover mismatch, tdash cross-acceptance, uppercase, non-platform HRP, missing-separator and empty cases. The convergent finding from Codex/Claude about is_mainnet_bech32m doing HRP-only classification rather than full bech32m decoding targets a function added in an earlier commit (7a02728) whose docstring explicitly documents the HRP-only contract — the pinned commit does not introduce that behavior and the address is still rejected by the subsequent from_bech32m_string decode for any HRP-class-matching but malformed input.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/rs-dpp/src/address_funds/platform_address.rs (1)

318-338: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the full address before classifying its HRP.

This helper only splits on the last 1, so malformed inputs like dash1! or dash1 still classify as mainnet. Downstream, shielded_unshield_to can then return a bogus network-mismatch error before the real decoder gets a chance to reject the invalid address. Reuse the strict bech32m parser here instead of HRP-only string splitting. (docs.rs)

🤖 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 `@packages/rs-dpp/src/address_funds/platform_address.rs` around lines 318 -
338, The is_mainnet_bech32m helper currently only splits on the last '1' which
misclassifies malformed inputs; replace the HRP-only split with a strict bech32
decoder (use bech32::decode) inside is_mainnet_bech32m to fully validate the
address, ensure the variant is Bech32m, extract the decoded HRP, then compare it
against PLATFORM_HRP_MAINNET and PLATFORM_HRP_TESTNET and return
Ok(true)/Ok(false) or a ProtocolError::DecodingError on invalid variant/decoding
failures; keep error messages descriptive and use the existing function name and
HRP constants to locate the change.
🤖 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-dpp/src/address_funds/orchard_address.rs`:
- Around line 100-102: OrchardAddress::from_bech32m_string currently uses
bech32::decode which accepts both Bech32 and Bech32m checksums; change it to use
the strict Bech32m API (e.g., bech32::primitives::CheckedHrpString /
decode_bech32m or equivalent decode_bech32m helper) so only Bech32m-checked
inputs succeed, map primitive decode errors into ProtocolError::DecodingError
like the current code, and update/add a unit test ensuring a Bech32
(non-Bech32m) encoded address is rejected by from_bech32m_string.

In `@packages/rs-dpp/src/address_funds/platform_address.rs`:
- Around line 259-262: PlatformAddress::from_bech32m_string currently calls
bech32::decode(s) which accepts both Bech32 and Bech32m checksums; change the
implementation to explicitly validate the checksum is Bech32m (rather than any
Bech32 variant) before proceeding. Replace the bech32::decode call in
from_bech32m_string with the bech32 primitives API that enforces Bech32m (e.g.,
the CheckedHrpstring/Bech32m-specific decode path) and map any non-Bech32m or
decode failures to ProtocolError::DecodingError so plain Bech32-checked strings
are rejected.

---

Duplicate comments:
In `@packages/rs-dpp/src/address_funds/platform_address.rs`:
- Around line 318-338: The is_mainnet_bech32m helper currently only splits on
the last '1' which misclassifies malformed inputs; replace the HRP-only split
with a strict bech32 decoder (use bech32::decode) inside is_mainnet_bech32m to
fully validate the address, ensure the variant is Bech32m, extract the decoded
HRP, then compare it against PLATFORM_HRP_MAINNET and PLATFORM_HRP_TESTNET and
return Ok(true)/Ok(false) or a ProtocolError::DecodingError on invalid
variant/decoding failures; keep error messages descriptive and use the existing
function name and HRP constants to locate the change.
🪄 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: a96225bb-7095-4d18-8312-24685dbb767e

📥 Commits

Reviewing files that changed from the base of the PR and between e01b6a3 and 71052a4.

📒 Files selected for processing (6)
  • packages/rs-dpp/src/address_funds/orchard_address.rs
  • packages/rs-dpp/src/address_funds/platform_address.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
  • packages/rs-platform-wallet/src/wallet/platform_wallet.rs
  • packages/wasm-dpp2/src/platform_address/address.rs

Comment on lines +100 to 102
pub fn from_bech32m_string(s: &str) -> Result<Self, ProtocolError> {
let (hrp, data) =
bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the current from_bech32m_string implementation and surrounding code
sed -n '1,220p' packages/rs-dpp/src/address_funds/orchard_address.rs | nl -ba | sed -n '1,220p'

# Narrow to the specific function region (around lines 80-130 and any helper usage)
sed -n '70,150p' packages/rs-dpp/src/address_funds/orchard_address.rs | nl -ba

# Find bech32 usage in this file (and nearby modules, if any)
rg -n "bech32::|bech32m|from_bech32m_string" packages/rs-dpp/src/address_funds/orchard_address.rs
rg -n "bech32" packages/rs-dpp/Cargo.toml packages/rs-dpp/Cargo.lock Cargo.toml 2>/dev/null || true

# If Cargo.lock exists at repo root, print the bech32 version entry
if [ -f Cargo.lock ]; then
  rg -n "name = \"bech32\"" -n Cargo.lock -C 2 || true
fi
if [ -f packages/rs-dpp/Cargo.lock ]; then
  rg -n "name = \"bech32\"" -n packages/rs-dpp/Cargo.lock -C 2 || true
fi

Repository: dashpay/platform

Length of output: 103


🌐 Web query:

bech32 crate version 0.11 bech32::decode accepts bech32 or bech32m strict api

💡 Result:

In the bech32 crate version 0.11 and later, the top-level bech32::decode function does not provide a strict API that allows you to specify or enforce a single checksum algorithm (such as Bech32 or Bech32m) directly within the function call [1][2]. Instead, bech32::decode is designed to handle strings that include a valid checksum according to either the BIP-173 (Bech32) or BIP-350 (Bech32m) specifications [3][4]. For strict validation or to enforce a specific checksum algorithm, you should use the primitives::decode module [5][6]. Specifically, the CheckedHrpstring type provides a strict API where you can explicitly specify the expected checksum algorithm as a generic parameter (e.g., CheckedHrpstring::new:: or CheckedHrpstring::new::) [6]. If your application requires parsing modern Bitcoin SegWit addresses, it is recommended to use the crate::segwit API, which handles the necessary version and checksum validations according to the relevant BIPs [5][1].

Citations:


🌐 Web query:

docs.rs bech32 0.11 decode returns variant or detects checksum type

💡 Result:

In the bech32 crate version 0.11, the top-level decode function does not return a variant or explicitly indicate which checksum type (Bech32 or Bech32m) was detected [1][2]. The function signature is: pub fn decode(s: &str) -> Result<(Hrp, Vec), DecodeError> [1][2] According to the documentation, this function succeeds if the input string is well-formed and contains either a valid bech32m checksum or a valid bech32 checksum [1][2]. It returns the human-readable part (HRP) and the data bytes with the checksum removed [2]. If you require explicit control over the checksum algorithm or need to distinguish between them, you should use the primitives::decode::CheckedHrpstring type [3][2]. This type allows you to specify the checksum algorithm (e.g., Bech32, Bech32m, or NoChecksum) during construction [2]. The library explicitly states that for scenarios needing such control, CheckedHrpstring should be used instead of the top-level decode function [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the current from_bech32m_string implementation and surrounding code
sed -n '1,220p' packages/rs-dpp/src/address_funds/orchard_address.rs | nl -ba | sed -n '1,220p'

# Narrow to the specific function region (around lines 80-130 and any helper usage)
sed -n '70,150p' packages/rs-dpp/src/address_funds/orchard_address.rs | nl -ba

# Find bech32 usage in this file (and nearby modules, if any)
rg -n "bech32::|bech32m|from_bech32m_string" packages/rs-dpp/src/address_funds/orchard_address.rs
rg -n "bech32" packages/rs-dpp/Cargo.toml packages/rs-dpp/Cargo.lock Cargo.toml 2>/dev/null || true

# If Cargo.lock exists at repo root, print the bech32 version entry
if [ -f Cargo.lock ]; then
  rg -n "name = \"bech32\"" -n Cargo.lock -C 2 || true
fi
if [ -f packages/rs-dpp/Cargo.lock ]; then
  rg -n "name = \"bech32\"" -n packages/rs-dpp/Cargo.lock -C 2 || true
fi

Repository: dashpay/platform

Length of output: 103


🌐 Web query:

bech32 crate version 0.11 bech32::decode accepts bech32 or bech32m strict api

💡 Result:

In bech32 crate 0.11.x, bech32::decode accepts both bech32 and bech32m checksummed strings; i.e., it is not “strict” to only one checksum variant. The crate’s docs explicitly say the input address “MUST include a valid bech32 or bech32m checksum” and that for individual specific checksum algorithms you should use decode_bech32 or decode_bech32m instead. It also notes that the primitives::decode module provides control over “valid checksum for specific checksum algorithm”. So: - bech32::decode: accepts bech32 OR bech32m (auto-detects which checksum is valid), and requires a valid checksum overall [1]. - Strict behavior (only bech32 OR only bech32m): use decode_bech32 or decode_bech32m (and/or the primitives::decode API that checks “valid checksum for specific checksum algorithm”) [1][2]. Sources: [1] and [2].

Citations:


🌐 Web query:

docs.rs bech32 0.11 decode returns variant or detects checksum type

💡 Result:

In bech32 crate 0.11, bech32::decode(s: &str) returns Result<(Hrp, Vec), DecodeError> (not a “variant” that tells you which checksum was used). It will accept either a valid bech32 checksum (BIP-173) or a valid bech32m checksum (BIP-350) and succeeds as long as one of them matches [1][2]. How it detects checksum type: - Internally, decode first constructs an UncheckedHrpstring and then tries to validate the checksum as Bech32m; if that fails with residue that matches a bech32 checksum, it continues, otherwise it returns DecodeError::Checksum [2]. - After it determines that “one of the checksums was valid”, it removes the checksum using remove_checksum:: (the code comments note Bech32m is only used there for its length because Bech32 and Bech32m are the same length; the function doesn’t return which algorithm matched) [2]. So: decode detects whether the input has a valid bech32 or bech32m checksum only to decide whether decoding succeeds (and to remove the checksum), but it does not surface the checksum algorithm as a return “variant”/tag to the caller [1][2]. If you need to explicitly select/identify which checksum algorithm to use, the docs suggest using primitives::decode::CheckedHrpstring / the constructor that allows selecting the checksum algorithm explicitly [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/rs-dpp/src/address_funds/orchard_address.rs"

# Show the from_bech32m_string function body
sed -n '80,140p' "$FILE" | sed -e 's/\t/  /g'

# Show the relevant tests region if present around the cited lines
sed -n '160,260p' "$FILE" | sed -e 's/\t/  /g'

# Report all bech32-related calls in this file
rg -n "bech32|decode_bech32m|decode_bech32|CheckedHrpstring|from_bech32m_string" "$FILE"

Repository: dashpay/platform

Length of output: 8177


Enforce Bech32m-only checksum validation in OrchardAddress::from_bech32m_string.

from_bech32m_string calls bech32::decode, which accepts valid Bech32 or Bech32m checksums; since the function then validates only HRP/payload length/type, non-canonical Bech32-checked inputs could still decode successfully. Switch to bech32’s primitives::decode strict API (e.g., CheckedHrpstring/decode_bech32m) to require Bech32m checksum, and add a test that a Bech32-encoded address is rejected.

🤖 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 `@packages/rs-dpp/src/address_funds/orchard_address.rs` around lines 100 - 102,
OrchardAddress::from_bech32m_string currently uses bech32::decode which accepts
both Bech32 and Bech32m checksums; change it to use the strict Bech32m API
(e.g., bech32::primitives::CheckedHrpString / decode_bech32m or equivalent
decode_bech32m helper) so only Bech32m-checked inputs succeed, map primitive
decode errors into ProtocolError::DecodingError like the current code, and
update/add a unit test ensuring a Bech32 (non-Bech32m) encoded address is
rejected by from_bech32m_string.

Comment on lines +259 to 262
pub fn from_bech32m_string(s: &str) -> Result<Self, ProtocolError> {
// Decode the bech32m string
let (hrp, data) =
bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/rs-dpp/src/address_funds/platform_address.rs"

echo "== File exists =="
ls -la "$FILE"

echo
echo "== from_bech32m_string (around 240-290) =="
nl -ba "$FILE" | sed -n '230,290p'

echo
echo "== is_mainnet_bech32m (around 300-360) =="
nl -ba "$FILE" | sed -n '300,360p'

echo
echo "== Any bech32 decode usage in this file =="
rg -n "bech32::decode|bech32m|decode_check|Checksum" "$FILE" || true

echo
echo "== Search for from_bech32m_string and is_mainnet_bech32m usage across repo =="
rg -n "from_bech32m_string\(|is_mainnet_bech32m\(" -S . || true

echo
echo "== bech32 crate version(s) =="
# Prefer workspace Cargo.toml
ls -ლა Cargo.toml >/dev/null || true
rg -n "bech32\s*=" -S Cargo.toml **/Cargo.toml || true
rg -n "name\s*=\s*\"bech32\"" Cargo.lock || true

Repository: dashpay/platform

Length of output: 288


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/rs-dpp/src/address_funds/platform_address.rs"

echo "== from_bech32m_string (approx 240-290) =="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '230,290p'

echo
echo "== is_mainnet_bech32m (approx 300-360) =="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '300,360p'

echo
echo "== Any bech32/bech32m decode usage in this file =="
rg -n "bech32::decode|bech32m|decode_check|Checksum|checksum" "$FILE" || true

echo
echo "== Search for checksum-specific decode API in this file =="
rg -n "decode_check|decode_with|verify|validate.*checksum|ChecksumVariant|Variant" "$FILE" || true

echo
echo "== Find from_bech32m_string definition exactly =="
rg -n "fn\s+from_bech32m_string" "$FILE"

echo
echo "== Find is_mainnet_bech32m definition exactly =="
rg -n "fn\s+is_mainnet_bech32m" "$FILE"

echo
echo "== Usage of is_mainnet_bech32m across repo =="
rg -n "is_mainnet_bech32m\(" -S . || true

echo
echo "== bech32 crate version =="
rg -n "name\s*=\s*\"bech32\"" -S Cargo.toml **/Cargo.toml || true
ls -la Cargo.lock >/dev/null 2>&1 && rg -n "name\s*=\s*\"bech32\"" Cargo.lock || true
rg -n "bech32\s*=" -S Cargo.toml **/Cargo.toml || true

Repository: dashpay/platform

Length of output: 16185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== bech32 entries in Cargo.lock =="
rg -n "name = \"bech32\"" Cargo.lock | head
# Print nearby lines for first entry
line1=$(rg -n "name = \"bech32\"" Cargo.lock | head -n1 | cut -d: -f1)
start=$((line1-8))
end=$((line1+25))
awk -v s=$start -v e=$end 'NR>=s && NR<=e {print NR ":" $0}' Cargo.lock | sed -n '1,40p'

echo
echo "== Search for bech32::decode::< usage =="
rg -n "bech32::decode::<" -S . || true
rg -n "bech32::decode\s*\::<" -S . || true

echo
echo "== Search for bech32::decode\( usage patterns in rs-dpp =="
rg -n "bech32::decode\(" packages/rs-dpp/src/address_funds/platform_address.rs

echo
echo "== Wallet call site around platform_wallet.rs:1139 =="
FILE2="packages/rs-platform-wallet/src/wallet/platform_wallet.rs"
awk '{print NR "\t" $0}' "$FILE2" | sed -n '1110,1180p'

Repository: dashpay/platform

Length of output: 4541


🌐 Web query:

bech32 crate 0.11.1 bech32::decode accepts both checksum variants bech32 and bech32m

💡 Result:

Yes, the bech32 crate version 0.11.1's top-level bech32::decode function accepts and successfully validates strings using either the Bech32 (BIP-173) or Bech32m (BIP-350) checksum variants [1]. According to the official documentation, if the bech32::decode function succeeds, it indicates that the input string is well-formed—consisting of a valid human-readable part (HRP), a separator, and bech32 characters—and possesses a valid checksum under either the Bech32m or Bech32 algorithm [1]. If you require specific control over which checksum algorithm to enforce (e.g., to only allow Bech32m or only Bech32), you should use the primitives::decode::CheckedHrpstring type instead, which allows for explicit selection of the checksum algorithm [1][2].

Citations:


🌐 Web query:

bech32 0.11.1 decode generic parameter how to require Bech32m checksum (example bech32::decode::<Bech32m>)

💡 Result:

In version 0.11.1 of the bech32 crate, the top-level bech32::decode function attempts to validate both Bech32 and Bech32m checksums automatically, rather than allowing a generic parameter to restrict it [1][2]. To explicitly require a Bech32m checksum, you should use the bech32::primitives::decode::CheckedHrpstring type [3][4]. This type's new constructor accepts a generic parameter that implements the necessary checksum trait, allowing you to enforce Bech32m validation specifically [3][2]. Example usage for requiring Bech32m: use bech32::{Bech32m, primitives::decode::CheckedHrpstring}; let s = "your_bech32m_string_here"; // Explicitly require Bech32m checksum validation let p = CheckedHrpstring::new::(s).expect("failed to parse string with Bech32m checksum"); // Access the result let hrp = p.hrp; let data: Vec = p.byte_iter.collect; By using CheckedHrpstring::new::<Bech32m>, the library will reject any input that does not conform to the Bech32m checksum algorithm [4]. If you do not require an allocator, you can iterate over the data directly from the CheckedHrpstring instance using p.byte_iter() [3][5].

Citations:


Make PlatformAddress::from_bech32m_string reject plain Bech32 encodings (enforce Bech32m checksum).

PlatformAddress::from_bech32m_string uses bech32::decode(s) (platform_address.rs lines 259-262), and with bech32 v0.11.1 that validator accepts either Bech32 or Bech32m checksum variants—so a Bech32-checked string can pass as long as HRP/payload/type-byte checks succeed. Require Bech32m explicitly (e.g., bech32::primitives::decode::CheckedHrpstring::new::<Bech32m>(s)).

🤖 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 `@packages/rs-dpp/src/address_funds/platform_address.rs` around lines 259 -
262, PlatformAddress::from_bech32m_string currently calls bech32::decode(s)
which accepts both Bech32 and Bech32m checksums; change the implementation to
explicitly validate the checksum is Bech32m (rather than any Bech32 variant)
before proceeding. Replace the bech32::decode call in from_bech32m_string with
the bech32 primitives API that enforces Bech32m (e.g., the
CheckedHrpstring/Bech32m-specific decode path) and map any non-Bech32m or decode
failures to ProtocolError::DecodingError so plain Bech32-checked strings are
rejected.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "7ee5be1d943795694d3ecc9080a3288fd182f7953e0785150b8d2ef22abb95fa"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants