Skip to content

Comments

feat(dpp): add client-side validation to remaining ST construction methods#3138

Open
thepastaclaw wants to merge 6 commits intodashpay:v3.1-devfrom
thepastaclaw:feat/validate-remaining-st-construction
Open

feat(dpp): add client-side validation to remaining ST construction methods#3138
thepastaclaw wants to merge 6 commits intodashpay:v3.1-devfrom
thepastaclaw:feat/validate-remaining-st-construction

Conversation

@thepastaclaw
Copy link
Contributor

@thepastaclaw thepastaclaw commented Feb 21, 2026

Issue Being Fixed or Feature Description

Adds client-side validation to the remaining state transition construction methods that lack it, following the pattern established in #3096.

Context: shumkov's review on #3096 asked for validation to be added consistently across all ST construction methods. This PR covers the identity credit transfer and identity top-up transitions.

What was done?

IdentityCreditTransferTransition (try_from_identity)

  • Self-transfer check: Returns IdentityCreditTransferToSelfError if identity_id == recipient_id
  • Minimum amount check: Returns InvalidIdentityCreditTransferAmountError if amount < 100_000 credits

These checks mirror the server-side validate_basic_structure_v0 in rs-drive-abci, catching invalid transfers before they're signed and broadcast.

IdentityTopUpTransition (try_from_identity)

  • Asset lock proof validation: Calls AssetLockProof::validate_structure() to validate the proof before constructing the transition
  • Gated behind #[cfg(feature = "validation")] since validate_structure requires that feature

What's NOT covered (and why)

State Transition Reason Not Covered
IdentityCreditWithdrawalTransition Server-side validation is disabled (returns UnsupportedFeatureError) — no meaningful checks to port
MasternodeVoteTransition No structural validation exists server-side
DataContractCreate/UpdateTransition Validation requires Network parameter not available in the constructor; SDK-level put_contract.rs already calls ensure_valid_state_transition_structure

How Has This Been Tested?

  • cargo check -p dpp --features "state-transition-signing"
  • cargo check -p dpp --features "state-transition-signing,validation"
  • cargo check -p dash-sdk
  • cargo test -p dpp --features "state-transition-signing" -- identity_credit_transfer ✅ (3 tests pass)

Breaking Changes

None. Existing valid transitions continue to work. Invalid transitions that would have been rejected server-side are now caught earlier at construction time.

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 tests
  • I have made corresponding changes to the documentation
  • I have added the PR to a GitHub project board (if applicable)

Summary by CodeRabbit

  • New Features

    • Validation for identity credit transfers to block self-transfers and enforce a minimum transfer amount.
  • Improvements

    • Stricter validation for identity top-up transactions, including asset lock proof verification.

Validation

  1. What was tested

    • cargo test -p dash-sdk --lib — client-side validation integration
    • cargo test -p dpp --lib — validation logic unit tests
  2. Results

    • All Rust checks passing on CI
  3. Environment

    • GitHub Actions CI on dashpay/platform

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

Added validation to two identity state transition constructors: credit-transfer now rejects self-transfers and transfers below 100,000 using new consensus error types; top-up now validates the asset lock proof against the platform version. Both functions rename the platform_version parameter from unused to active.

Changes

Cohort / File(s) Summary
Identity Credit Transfer Validation
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/v0_methods.rs
Added checks to reject transfers to the same identity and amounts < 100000, returning appropriate ConsensusError wrappers. Introduced two new error types and renamed _platform_version to platform_version (muted unused variable).
Identity Top-Up Validation
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs
Renamed _platform_version to platform_version and added a feature-gated validation of asset_lock_proof against platform_version, returning the first consensus validation error when present.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped through code with careful paws,
I checked the hops and counted the claws,
No self-transfer nor tiny fee,
The ledger hums in harmony,
A rabbit's nod to safety's laws 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding client-side validation to state transition construction methods for the DPP package.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
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.

🧹 Nitpick comments (2)
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs (2)

39-43: Prefer an if let pattern to eliminate the .unwrap().

The .unwrap() on line 41 is logically safe today because !validation_result.is_valid() implies a non-empty errors vec — but it couples correctness to ValidationResult::is_valid()'s semantics never diverging from a plain errors.is_empty() check. Rewriting as if let removes that fragility with zero overhead:

♻️ Proposed refactor to eliminate `.unwrap()`
-        if !validation_result.is_valid() {
-            let first_error = validation_result.errors.into_iter().next().unwrap();
-            return Err(ProtocolError::ConsensusError(Box::new(first_error)));
-        }
+        if let Some(first_error) = validation_result.errors.into_iter().next() {
+            return Err(ProtocolError::ConsensusError(Box::new(first_error)));
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs`
around lines 39 - 43, Change the unwrap on
validation_result.errors.into_iter().next() to an if-let to avoid panicking:
after calling asset_lock_proof.validate_structure(platform_version) and
observing !validation_result.is_valid(), match the first error with if let
Some(first_error) = validation_result.errors.into_iter().next() { return
Err(ProtocolError::ConsensusError(Box::new(first_error))); } else {
unreachable!("validation_result.is_valid() == false must mean there is at least
one error") }; this replaces the .unwrap() and references
asset_lock_proof.validate_structure, validation_result, and
ProtocolError::ConsensusError.

46-46: let _ = platform_version; is a valid but noisy workaround.

This silences the "unused variable" warning when #[cfg(feature = "validation")] is absent. It's correct, but an alternative that makes the intent more explicit without a spurious statement is to use a #[cfg_attr] allow on the parameter itself.

♻️ Alternative: suppress at the parameter level
+    #[cfg_attr(not(feature = "validation"), allow(unused_variables))]
     fn try_from_identity(
         identity: &Identity,
         asset_lock_proof: AssetLockProof,
         asset_lock_proof_private_key: &[u8],
         user_fee_increase: UserFeeIncrease,
         platform_version: &PlatformVersion,
         _version: Option<FeatureVersion>,
     ) -> Result<StateTransition, ProtocolError> {
         #[cfg(feature = "validation")]
         {
             ...
         }
-        let _ = platform_version;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs`
at line 46, Remove the noisy placeholder "let _ = platform_version;" and instead
annotate the platform_version parameter so the compiler only ignores it when the
validation feature is disabled; e.g., on the function in v0_methods.rs that
receives platform_version, add #[cfg_attr(not(feature = "validation"),
allow(unused_variables))] to the platform_version parameter declaration (or use
allow(unused_variables) via cfg_attr on the parameter) and delete the let _ =
... statement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs`:
- Around line 39-43: Change the unwrap on
validation_result.errors.into_iter().next() to an if-let to avoid panicking:
after calling asset_lock_proof.validate_structure(platform_version) and
observing !validation_result.is_valid(), match the first error with if let
Some(first_error) = validation_result.errors.into_iter().next() { return
Err(ProtocolError::ConsensusError(Box::new(first_error))); } else {
unreachable!("validation_result.is_valid() == false must mean there is at least
one error") }; this replaces the .unwrap() and references
asset_lock_proof.validate_structure, validation_result, and
ProtocolError::ConsensusError.
- Line 46: Remove the noisy placeholder "let _ = platform_version;" and instead
annotate the platform_version parameter so the compiler only ignores it when the
validation feature is disabled; e.g., on the function in v0_methods.rs that
receives platform_version, add #[cfg_attr(not(feature = "validation"),
allow(unused_variables))] to the platform_version parameter declaration (or use
allow(unused_variables) via cfg_attr on the parameter) and delete the let _ =
... statement.

thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…action

Replace is_valid() check + unwrap() with if-let pattern to eliminate
fragile coupling to ValidationResult internals.

Addresses CodeRabbit nitpick on PR dashpay#3138.
@thepastaclaw
Copy link
Contributor Author

Thanks for the review!

Nitpick 1 (if-let instead of unwrap): Good catch — applied in f226e6f. The if let pattern is cleaner and avoids coupling to ValidationResult::is_valid() semantics.

Nitpick 2 (let _ = platform_version vs cfg_attr): I'll keep the let _ = platform_version; pattern here. Rust doesn't support #[cfg_attr] directly on individual function parameters, so the attribute would need to go on the whole function — which would suppress all unused variable warnings, not just platform_version. The let _ = x; idiom is explicit about which variable is intentionally unused and is standard Rust practice for this situation.

Copy link
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.

🧹 Nitpick comments (1)
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs (1)

40-42: validation_result.errors is a direct internal-field access — consider a consuming accessor.

The commit message says the refactor avoids "fragile coupling to ValidationResult internals," yet validation_result.errors.into_iter().next() still reaches into the struct's field directly. The existing first_error() returns Option<&E> (not owned), which is why it can't be used here — but the gap between intent and implementation is worth closing.

♻️ Suggested approach: add a consuming into_first_error() on ValidationResult

In packages/rs-dpp/src/validation/validation_result.rs:

+    pub fn into_first_error(mut self) -> Option<E> {
+        if self.errors.is_empty() { None } else { Some(self.errors.remove(0)) }
+    }

Then here:

-            if let Some(first_error) = validation_result.errors.into_iter().next() {
+            if let Some(first_error) = validation_result.into_first_error() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs`
around lines 40 - 42, Introduce a consuming accessor on ValidationResult (e.g.,
into_first_error() -> Option<E>) that returns and consumes the first error
instead of exposing the internal errors field; then update the v0_methods.rs
call site (where validation_result is used) to call
validation_result.into_first_error().map(|e|
ProtocolError::ConsensusError(Box::new(e))).and_then(Err) or equivalently return
Err(ProtocolError::ConsensusError(Box::new(e))) when Some, thereby removing
direct access to validation_result.errors and using
ValidationResult::into_first_error() instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/v0_methods.rs`:
- Around line 40-42: Introduce a consuming accessor on ValidationResult (e.g.,
into_first_error() -> Option<E>) that returns and consumes the first error
instead of exposing the internal errors field; then update the v0_methods.rs
call site (where validation_result is used) to call
validation_result.into_first_error().map(|e|
ProtocolError::ConsensusError(Box::new(e))).and_then(Err) or equivalently return
Err(ProtocolError::ConsensusError(Box::new(e))) when Some, thereby removing
direct access to validation_result.errors and using
ValidationResult::into_first_error() instead.

thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…action

Replace is_valid() check + unwrap() with if-let pattern to eliminate
fragile coupling to ValidationResult internals.

Addresses CodeRabbit nitpick on PR dashpay#3138.
@thepastaclaw thepastaclaw force-pushed the feat/validate-remaining-st-construction branch from f226e6f to df9ecbd Compare February 21, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants