Skip to content

feat(sdk): add client-side validation to state transition construction methods#3096

Draft
thepastaclaw wants to merge 21 commits into
dashpay:v3.1-devfrom
thepastaclaw:fix/validate-transfer-key-security-level-client-side
Draft

feat(sdk): add client-side validation to state transition construction methods#3096
thepastaclaw wants to merge 21 commits into
dashpay:v3.1-devfrom
thepastaclaw:fix/validate-transfer-key-security-level-client-side

Conversation

@thepastaclaw
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw commented Feb 17, 2026

Problem

SDK construction methods for state transitions don't validate the transition structure before returning. Invalid transitions silently construct and broadcast, only to be rejected by the network with confusing errors.

For example:

  • Creating a TRANSFER key with HIGH security level (instead of CRITICAL) in IdentityUpdateTransition — rejected on broadcast with no clear indication why
  • Address-based transitions with mismatched input/witness counts — rejected after a network round-trip

Fix

Add client-side validation calls during transition construction, before signing and broadcasting. This reuses existing validation logic from rs-dpp (which Platform already uses server-side in rs-drive-abci).

Changes in two parts:

1. Public key security level validation (originally reported by @thephez):

  • IdentityUpdateTransition::try_from_identity_with_signer() — validates key purpose/security level compatibility
  • IdentityCreateTransition::try_from_identity_with_signer() — same validation
  • IdentityCreateFromAddressesTransition::try_from_inputs_with_signer() — same validation

What gets validated:

  • TRANSFER keys must use CRITICAL security level
  • ENCRYPTION / DECRYPTION keys must use MEDIUM security level
  • No duplicate key IDs or key data
  • Max key count limits

2. Full validate_structure() on remaining state transitions (per shumkov's review):

State Transition Construction Method
AddressCreditWithdrawalTransition try_from_inputs_with_signer
AddressFundingFromAssetLockTransition try_from_asset_lock_with_signer
AddressFundsTransferTransition try_from_inputs_with_signer
IdentityCreateFromAddressesTransition try_from_inputs_with_signer
IdentityCreditTransferToAddressesTransition try_from_identity
IdentityTopUpFromAddressesTransition try_from_inputs_with_signer

All use the same pattern:

let validation_result = transition.validate_structure(platform_version);
if !validation_result.is_valid() {
    let first_error = validation_result.errors.into_iter().next().unwrap();
    return Err(ProtocolError::ConsensusError(Box::new(first_error)));
}

Validation is placed after the transition is fully constructed (witnesses set, signatures applied) so validate_structure() sees the complete state.

Context

/cc @QuantumExplorer

Summary by CodeRabbit

  • Bug Fixes

    • Stronger client-side validation for identity public keys and structural checks for multiple state transitions to prevent malformed transactions.
  • Tests

    • Tests updated to exercise new validation paths, manual/on-demand signing flows, and explicit fee-strategy scenarios.

Validation

Build & Compilation

All modified Rust packages compile successfully:

  • rs-dpp — check each feature (13m21s ✅), formatting ✅, linting ✅
  • rs-drive-abci — check each feature (5m27s ✅), formatting ✅, linting ✅
  • Build JS packages ✅ (10m41s)

Tests

All relevant test suites pass:

  • rs-dpp tests ✅ (2m52s) — includes updated tests for new validation paths in address_funds_transfer_transition/signing_tests.rs
  • rs-drive-abci tests ✅ (11m40s) — includes updated/expanded tests across all affected state transitions:
    • address_credit_withdrawal/tests.rs (+140/-43 lines)
    • address_funding_from_asset_lock/tests.rs (+12/-4)
    • address_funds_transfer/tests.rs (+92/-42)
    • identity_create_from_addresses/tests.rs (+59/-30)
    • identity_credit_transfer_to_addresses/tests.rs (+77/-9)
    • identity_top_up_from_addresses/tests.rs (+11/-1)
  • rs-drive tests ✅ (7m48s)
  • dash-sdk tests ✅ (3m42s)
  • Strategy tests updated (strategy.rs +59/-10, address_tests.rs +24/-24)

Additional CI

  • Unused dependencies check ✅ (dpp, drive-abci)
  • Immutable structure detection ✅ (dpp, drive-abci)
  • Rust crates security audit ✅
  • JS code security audit / CodeQL ✅
  • PR title / semantic PR check ✅

Unrelated Failures

Two CI jobs fail but are not related to this PR:

  • JS NPM security audit — pre-existing dependency vulnerability, not introduced by this PR
  • Swift SDK build — unrelated Swift package build issue

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 17, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 152bb46d-3d06-44b1-9741-5ccb4577f01a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds client-side identity public-key structure validation and runtime state-transition structure validation across multiple transition types; consistently renames _platform_version parameters to platform_version; and updates many tests to use raw/on-the-fly signing and explicit fee-strategy steps.

Changes

Cohort / File(s) Summary
Identity create/update (public-key validation)
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs, packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/v0_methods.rs, packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/v0_methods.rs
Builds Vec<IdentityPublicKeyInCreation> and calls IdentityPublicKeyInCreation::validate_identity_public_keys_structure (create vs non-create context). Returns first validation error as ConsensusError on failure. Renames _platform_versionplatform_version in method signatures.
Address funds transitions (structure validation)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/.../v0_methods.rs (credit_withdrawal, funding_from_asset_lock, address_funds_transfer, address_topup_from_addresses_transition/...)
Adds validate_structure(platform_version) checks on constructed transitions (post witness/signing or pre-into conversion) and returns first validation error as ConsensusError. Renames _platform_versionplatform_version and imports StateTransitionStructureValidation/ProtocolError/Pooling where needed.
Identity credit/top-up transitions (structure validation)
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/v0_methods.rs, packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/v0_methods.rs
Performs validate_structure(platform_version) on V0 transition objects (pre- or post-conversion), returning the first error as ConsensusError. Renames _platform_versionplatform_version.
Tests — raw/on-the-fly signing & fee strategy
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/.../tests.rs, packages/rs-dpp/.../address_funds_transfer_transition/signing_tests.rs, packages/rs-drive-abci/.../identity_*.rs
Refactors many tests to construct raw V0 transitions, compute signable bytes, and sign witnesses at runtime (adds Signer, Signable, PlatformSerializable imports). Introduces explicit AddressFundsFeeStrategy / AddressFundsFeeStrategyStep usage and updates helpers to create manually-signed transitions or full-variant helpers that bypass constructor-time checks. Numerous test helpers and call sites updated.
Misc imports & signature renames
packages/rs-dpp/src/state_transition/state_transitions/.../v0_methods.rs (various files)
Consistent renaming of _platform_version to platform_version, added imports for StateTransitionStructureValidation, ProtocolError, Pooling, and related feature-gated imports across multiple modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I checked each key with twitchy nose and ear,
I hopped through witnesses, signed what must appear,
I thumped when structure wobbled or refused to stand,
Then fixed the steps with gentle carrot-hand,
Off I bound — validated, sealed, and clear!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat(sdk): add client-side validation to state transition construction methods' directly and clearly summarizes the main change: adding client-side validation to state transition construction methods.
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 unit tests (beta)
  • Create PR with unit tests

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.

@thepastaclaw thepastaclaw changed the base branch from master to v3.1-dev February 17, 2026 18:41
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.

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

45-45: _platform_version is now used — consider removing the underscore prefix.

The _ prefix conventionally signals an intentionally-unused binding. Since this parameter is now actively consumed by validate_identity_public_keys_structure (line 64), the prefix is misleading. This applies to all three files in the PR (identity_create_transition, identity_update_transition, identity_create_from_addresses_transition).

That said, this is a pre-existing naming choice inherited from the trait signature, so feel free to defer if changing it would cascade across the trait definition.

🤖 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_create_transition/v0/v0_methods.rs`
at line 45, The parameter named `_platform_version` is now used by
validate_identity_public_keys_structure, so remove the misleading underscore by
renaming `_platform_version` to `platform_version` in the function signature in
v0_methods.rs (and analogously in the other two files:
identity_update_transition and identity_create_from_addresses_transition), and
update all usages inside the function (including the call to
validate_identity_public_keys_structure) to use the new `platform_version`
identifier; if the underscore comes from a trait signature you can instead
change the local binding to `platform_version` (keeping the trait name) to avoid
cascading trait edits.
🤖 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_create_transition/v0/v0_methods.rs`:
- Line 45: The parameter named `_platform_version` is now used by
validate_identity_public_keys_structure, so remove the misleading underscore by
renaming `_platform_version` to `platform_version` in the function signature in
v0_methods.rs (and analogously in the other two files:
identity_update_transition and identity_create_from_addresses_transition), and
update all usages inside the function (including the call to
validate_identity_public_keys_structure) to use the new `platform_version`
identifier; if the underscore comes from a trait signature you can instead
change the local binding to `platform_version` (keeping the trait name) to avoid
cascading trait edits.

@PastaPastaPasta
Copy link
Copy Markdown
Member

This is a test that you'll actually act on my review comment. Please just comment with "got it" :D

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

got it :D

_platform_version: &PlatformVersion,
platform_version: &PlatformVersion,
_version: Option<FeatureVersion>,
) -> Result<StateTransition, ProtocolError> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@QuantumExplorer @lklimek do you think, guys, is it the right place to validate data in SDK when the user creates it? I just wondering if we should call the validation method in SDK methods instead + have an option to skip validation.

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.

As a rule of thumb, I would say user should not be able to create an object that is invalid, unless he really tries to and knows what he's doing.

I don't see the use case for non-validated identity update transition, but if you do and you think we need it, it should be separate constructor IMO.

See https://rust-lang.github.io/api-guidelines/dependability.html#dynamic-enforcement-with-opt-out

The convention is to mark these opt-out functions with a suffix like _unchecked or by placing them in a raw submodule.
The unchecked functions can be used judiciously in cases where (1) performance dictates avoiding checks and (2) the client is otherwise confident that the inputs are valid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed — validation by default is the right call, and the current implementation does exactly that. No use case for an unchecked path right now, but if one comes up I'll follow the _unchecked convention from the Rust API guidelines. Thanks for the reference!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ok, sounds good! I'm fine with validated only version for now. @thepastaclaw please create PRs with validation for other state transitions so we have consistent behaviour for SDK.

@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re: @shumkov's question about validation placement:

Good question. I put validation here (in the DPP method that constructs the transition) because this is the earliest point where we know all the keys and can catch the error — before any signing or serialization happens. The alternative of validating in SDK methods would work too, but would mean the raw DPP construction method silently accepts invalid key combinations that the platform will reject anyway.

Happy to move it to the SDK layer with a skip-validation option if that is the preferred pattern. Deferring to @QuantumExplorer and @lklimek on the right approach.

shumkov
shumkov previously approved these changes Feb 18, 2026
@thepastaclaw
Copy link
Copy Markdown
Collaborator Author

Re: @shumkov's request for consistent validation: Will do! I'll create follow-up PRs adding the same client-side validation to the other state transitions for consistency across the SDK. Thanks for the review!

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

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 18, 2026
…ansitions

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thepastaclaw thepastaclaw changed the title fix(dpp): validate public key security levels client-side in IdentityUpdateTransition feat(sdk): add client-side validation to state transition construction methods Feb 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/dashpay/platform/issues/comments/3916479814","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nAdds client-side validation of identity public key structures to three identity transition flows (create_from_addresses V0, create V0, update V0). Validation runs via `IdentityPublicKeyInCreation::validate_identity_public_keys_structure` and returns a ConsensusError on failure before proceeding.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Create from Addresses / Create (V0)** <br> `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs`, `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/v0_methods.rs`|Collects public keys as `Vec<IdentityPublicKeyInCreation>` and invokes `validate_identity_public_keys_structure` with the create-context/platform version. On validation failure returns a `ConsensusError` early; only valid keys are set and the signing flow continues.|\n|**Update (V0)** <br> `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/v0_methods.rs`|Converts `add_public_keys` to `add_public_keys_in_creation` and validates with `validate_identity_public_keys_structure` (non-create context). Returns first validation error as `ConsensusError` if invalid. Also renames parameter `_platform_version` → `platform_version` in `try_from_identity_with_signer`.|\n\n## Estimated code review effort\n\n🎯 3 (Moderate) | ⏱️ ~20 minutes\n\n## Poem\n\n> 🐰 Keys hop in lines, checked one by one,  \n> A gentle guard before the run,  \n> If one is wrong I'll thump the ground,  \n> No faulty key shall leave the mound,  \n> Now onward, signed and snug, we're done! 🥕\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 3</summary>\n\n<details>\n<summary>✅ Passed checks (3 passed)</summary>\n\n|     Check name     | Status   | Explanation                                                                                                                                                                                                                                                                                                                |\n| :----------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                                                                                                                |\n| Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.                                                                                                                                                                                                                                       |\n|     Title check    | ✅ Passed | The PR title 'feat(sdk): add client-side validation to state transition construction methods' is directly related to the core changes, which introduce client-side validation of identity public keys in three state transition construction methods. The title accurately summarizes the main objective of the changeset. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dashpay/platform&utm_content=3096)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/dashpay/platform/issues/comments/3916479814","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Wed, 18 Feb 2026 15:43:10 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"084F:22FD30:5648304:17613E6C:6995DE0E","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

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

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thepastaclaw thepastaclaw force-pushed the fix/validate-transfer-key-security-level-client-side branch from ef57031 to 44c71dd Compare February 20, 2026 21:39
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 20, 2026
…oken transitions

Add structural validation to all document and token SDK transition
builders, matching the pattern from PR dashpay#3096 (identity/address
transitions). Calls validate_base_structure() on BatchTransition after
construction but before broadcast, catching invalid transitions early.

Applied to:
- Document transitions: create, delete, replace, purchase, set_price, transfer
- Token builders: burn, claim, config_update, destroy, purchase,
  emergency_action, freeze, mint, set_price, transfer, unfreeze
- Enabled dpp 'validation' feature for dash-sdk crate
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/signing_tests.rs (2)

829-837: ⚠️ Potential issue | 🟡 Minor

Tampered output value could coincide with the fee-reduced stored value.

After ReduceOutput(0) is applied during construction the stored output is 1_000_000 − fee. The tampering sets it to 950_000. If the platform fee happens to equal exactly 50_000 credits, the two values are identical, the signable bytes are unchanged, verification succeeds, and assert!(result.is_err()) would fail. Consider choosing a tampered value that is guaranteed to differ (e.g., 500_000u64 or any value far from the original 1_000_000), or read the actual stored output and modify it by a fixed delta.

🤖 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/address_funds/address_funds_transfer_transition/signing_tests.rs`
around lines 829 - 837, The test uses a hardcoded tampered value that may equal
the stored output after ReduceOutput(0); update the tamper logic in
signing_tests.rs so the modified output is guaranteed different: either set a
clearly different constant (e.g., 500_000u64) when calling
transition.outputs.insert(...) or fetch the stored value for the output (from
transition.outputs.get(&output) or equivalent) and change it by a fixed non-zero
delta (e.g., -1 or +12345) before reinserting; keep references to the existing
ReduceOutput(0) behavior and ensure verify_transition_signatures(&transition) is
expected to return Err.

1012-1013: ⚠️ Potential issue | 🟡 Minor

Missing else { panic!() } guards in edge-case if let blocks.

If the witness at index 0 is unexpectedly not P2sh, both test_1_of_1_multisig and test_high_threshold_multisig silently skip the signatures.len() assertion and pass vacuously — hiding a type-mismatch. Other P2SH tests (e.g., test_single_p2sh_2_of_3_multisig_input_signing) correctly include an else { panic!("Expected P2SH witness") } branch.

🔧 Proposed fix for both tests
 if let AddressWitness::P2sh { signatures, .. } = &transition.input_witnesses[0] {
     assert_eq!(signatures.len(), 1);
+} else {
+    panic!("Expected P2SH witness");
 }
 if let AddressWitness::P2sh { signatures, .. } = &transition.input_witnesses[0] {
     assert_eq!(signatures.len(), 5);
+} else {
+    panic!("Expected P2SH witness");
 }

Also applies to: 1051-1053

🤖 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/address_funds/address_funds_transfer_transition/signing_tests.rs`
around lines 1012 - 1013, Both tests use an if let AddressWitness::P2sh {
signatures, .. } = &transition.input_witnesses[0] pattern but lack an else panic
branch, letting a non-P2sh witness silently skip the assertion; update the two
tests (test_1_of_1_multisig and test_high_threshold_multisig) to add an else {
panic!("Expected P2SH witness") } guard after the if let so the test fails
loudly on a mismatched witness type, referencing the same AddressWitness::P2sh
destructuring and transition.input_witnesses[0] access used now; apply the same
change for the analogous block around lines 1051-1053.
🧹 Nitpick comments (2)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/signing_tests.rs (1)

287-292: Extract the repeated V0 transition unwrapping into a test helper.

The nested match that destructures StateTransition::AddressFundsTransfer(…::V0(v0)) appears ~15 times. A small private helper eliminates the boilerplate and makes every test body easier to scan.

♻️ Suggested helper
fn unwrap_transfer_v0(st: StateTransition) -> AddressFundsTransferTransitionV0 {
    match st {
        StateTransition::AddressFundsTransfer(
            crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition::V0(v0),
        ) => v0,
        _ => panic!("Expected AddressFundsTransfer V0 transition"),
    }
}

Then every call site becomes:

-    let transition = match state_transition {
-        StateTransition::AddressFundsTransfer(t) => match t {
-            crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition::V0(v0) => v0,
-        },
-        _ => panic!("Expected AddressFundsTransfer transition"),
-    };
+    let transition = unwrap_transfer_v0(state_transition);
🤖 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/address_funds/address_funds_transfer_transition/signing_tests.rs`
around lines 287 - 292, The test suite repeats a nested match to extract
StateTransition::AddressFundsTransfer(...::V0(v0)) about 15 times; add a small
private helper fn unwrap_transfer_v0(st: StateTransition) ->
AddressFundsTransferTransitionV0 that matches
StateTransition::AddressFundsTransfer(crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition::V0(v0))
=> v0 and panics otherwise, then replace each repeated match in signing_tests.rs
with a call to unwrap_transfer_v0(state_transition) to remove boilerplate and
simplify test bodies.
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs (1)

110-116: Consider extracting the repeated validation-to-error pattern into a helper.

The same 5-line block (validate_structureis_validerrors.into_iter().next().unwrap()ConsensusError) is duplicated across ~7 call sites in this PR. A small helper on ValidationResult (or a free function) would reduce boilerplate and ensure consistency.

Example helper

Something like (in validation_result.rs or a utility module):

impl<E: Into<ConsensusError>> ValidationResult<E> {
    pub fn into_result(self) -> Result<(), ProtocolError> {
        if self.is_valid() {
            Ok(())
        } else {
            let first_error = self.errors.into_iter().next().unwrap();
            Err(ProtocolError::ConsensusError(Box::new(first_error.into())))
        }
    }
}

Then each call site simplifies to:

-        let validation_result =
-            identity_create_from_addresses_transition.validate_structure(platform_version);
-        if !validation_result.is_valid() {
-            let first_error = validation_result.errors.into_iter().next().unwrap();
-            return Err(ProtocolError::ConsensusError(Box::new(first_error)));
-        }
+        identity_create_from_addresses_transition
+            .validate_structure(platform_version)
+            .into_result()?;
🤖 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_create_from_addresses_transition/v0/v0_methods.rs`
around lines 110 - 116, The repeated pattern of calling validate_structure(...),
checking validation_result.is_valid(), extracting the first error via
validation_result.errors.into_iter().next().unwrap(), and wrapping it in
ProtocolError::ConsensusError should be extracted into a helper to remove
boilerplate; add a method (e.g., impl ValidationResult<E> { pub fn
into_result(self) -> Result<(), ProtocolError> }) or a free utility that returns
Ok(()) when is_valid() and returns
Err(ProtocolError::ConsensusError(Box::new(first_error.into()))) otherwise, then
replace the repeated blocks in functions like
identity_create_from_addresses_transition.validate_structure(...) call sites
with a single call to validation_result.into_result() (or the free helper) to
ensure consistent behavior and concise code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/signing_tests.rs`:
- Line 270: Update the stale inline comment next to the inputs.insert call that
currently reads "nonce: 1, credits: 1000" to reflect the actual value passed
(1_000_000); locate the inputs.insert(input_address.clone(), (1u32,
1_000_000u64)) line and change the comment to "nonce: 1, credits: 1_000_000" (or
remove the comment if redundant).

---

Outside diff comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/signing_tests.rs`:
- Around line 829-837: The test uses a hardcoded tampered value that may equal
the stored output after ReduceOutput(0); update the tamper logic in
signing_tests.rs so the modified output is guaranteed different: either set a
clearly different constant (e.g., 500_000u64) when calling
transition.outputs.insert(...) or fetch the stored value for the output (from
transition.outputs.get(&output) or equivalent) and change it by a fixed non-zero
delta (e.g., -1 or +12345) before reinserting; keep references to the existing
ReduceOutput(0) behavior and ensure verify_transition_signatures(&transition) is
expected to return Err.
- Around line 1012-1013: Both tests use an if let AddressWitness::P2sh {
signatures, .. } = &transition.input_witnesses[0] pattern but lack an else panic
branch, letting a non-P2sh witness silently skip the assertion; update the two
tests (test_1_of_1_multisig and test_high_threshold_multisig) to add an else {
panic!("Expected P2SH witness") } guard after the if let so the test fails
loudly on a mismatched witness type, referencing the same AddressWitness::P2sh
destructuring and transition.input_witnesses[0] access used now; apply the same
change for the analogous block around lines 1051-1053.

---

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/signing_tests.rs`:
- Around line 287-292: The test suite repeats a nested match to extract
StateTransition::AddressFundsTransfer(...::V0(v0)) about 15 times; add a small
private helper fn unwrap_transfer_v0(st: StateTransition) ->
AddressFundsTransferTransitionV0 that matches
StateTransition::AddressFundsTransfer(crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition::V0(v0))
=> v0 and panics otherwise, then replace each repeated match in signing_tests.rs
with a call to unwrap_transfer_v0(state_transition) to remove boilerplate and
simplify test bodies.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs`:
- Around line 110-116: The repeated pattern of calling validate_structure(...),
checking validation_result.is_valid(), extracting the first error via
validation_result.errors.into_iter().next().unwrap(), and wrapping it in
ProtocolError::ConsensusError should be extracted into a helper to remove
boilerplate; add a method (e.g., impl ValidationResult<E> { pub fn
into_result(self) -> Result<(), ProtocolError> }) or a free utility that returns
Ok(()) when is_valid() and returns
Err(ProtocolError::ConsensusError(Box::new(first_error.into()))) otherwise, then
replace the repeated blocks in functions like
identity_create_from_addresses_transition.validate_structure(...) call sites
with a single call to validation_result.into_result() (or the free helper) to
ensure consistent behavior and concise code.

// Build inputs and outputs
let mut inputs = BTreeMap::new();
inputs.insert(input_address.clone(), (1u32, 1000u64)); // nonce: 1, credits: 1000
inputs.insert(input_address.clone(), (1u32, 1_000_000u64)); // nonce: 1, credits: 1000
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

Stale comment: credits: 1000 doesn't match the updated value 1_000_000.

The inline comment was not updated when the credit value was scaled up.

🔧 Proposed fix
-    inputs.insert(input_address.clone(), (1u32, 1_000_000u64)); // nonce: 1, credits: 1000
+    inputs.insert(input_address.clone(), (1u32, 1_000_000u64)); // nonce: 1, credits: 1_000_000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inputs.insert(input_address.clone(), (1u32, 1_000_000u64)); // nonce: 1, credits: 1000
inputs.insert(input_address.clone(), (1u32, 1_000_000u64)); // nonce: 1, credits: 1_000_000
🤖 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/address_funds/address_funds_transfer_transition/signing_tests.rs`
at line 270, Update the stale inline comment next to the inputs.insert call that
currently reads "nonce: 1, credits: 1000" to reflect the actual value passed
(1_000_000); locate the inputs.insert(input_address.clone(), (1u32,
1_000_000u64)) line and change the comment to "nonce: 1, credits: 1_000_000" (or
remove the comment if redundant).

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

Add structural validation to all document and token SDK transition
builders, matching the pattern from PR dashpay#3096 (identity/address
transitions). Calls validate_base_structure() on BatchTransition after
construction but before broadcast, catching invalid transitions early.

Applied to:
- Document transitions: create, delete, replace, purchase, set_price, transfer
- Token builders: burn, claim, config_update, destroy, purchase,
  emergency_action, freeze, mint, set_price, transfer, unfreeze
- Enabled dpp 'validation' feature for dash-sdk crate
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…ansitions

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thepastaclaw thepastaclaw force-pushed the fix/validate-transfer-key-security-level-client-side branch from 67ef15e to 42a8d45 Compare February 21, 2026 18:29
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…oken transitions

Add structural validation to all document and token SDK transition
builders, matching the pattern from PR dashpay#3096 (identity/address
transitions). Calls validate_base_structure() on BatchTransition after
construction but before broadcast, catching invalid transitions early.

Applied to:
- Document transitions: create, delete, replace, purchase, set_price, transfer
- Token builders: burn, claim, config_update, destroy, purchase,
  emergency_action, freeze, mint, set_price, transfer, unfreeze
- Enabled dpp 'validation' feature for dash-sdk crate
thepastaclaw added a commit to thepastaclaw/platform that referenced this pull request Feb 21, 2026
…ansitions

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator Author

@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

I verified the reported issues against the checked-out PR head 645ba4b8f2caaeadb298c2c9ed62b6c6597aefde. One finding does not hold up because ProtocolError::from(Vec<ConsensusError>) deliberately preserves the single-error shape and the new multi-error behavior is covered by a dedicated test; one remaining SDK-side validation gap is real, but it is a client-UX issue rather than a consensus or server-validation defect because the server still rejects the bad signature during identity-create state validation.

Reviewed commit: 645ba4b

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/v0_methods.rs`:
- [SUGGESTION] lines 128-130: `IdentityCreateTransition` still returns `Ok` for an instant asset-lock signed with the wrong private key
  This constructor now performs several client-side checks before returning, but its final signing step still accepts any secp256k1 private key that can produce an ECDSA signature. There is no local comparison between `asset_lock_proof_private_key` and the P2PKH hash of the locked output when the proof is an instant proof carrying the full transaction, so the method can return a locally successful transition that the server later rejects when it verifies the one-time asset-lock signature against the fetched output script in `identity_create/state/v0/mod.rs`. The analogous `AddressFundingFromAssetLockTransitionV0` path already performs this local key-to-output check when transaction data is available, so the missing check here leaves the new pre-broadcast validation incomplete for one of the main identity-create constructors. Chain proofs still cannot be fully verified locally from this constructor, so the fix should be conditional on `asset_lock_proof.transaction()` being present rather than changing chain-proof behavior.

Comment on lines 128 to 130
let mut state_transition: StateTransition = identity_create_transition.into();

state_transition.sign_by_private_key(asset_lock_proof_private_key, ECDSA_HASH160, bls)?;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: IdentityCreateTransition still returns Ok for an instant asset-lock signed with the wrong private key

This constructor now performs several client-side checks before returning, but its final signing step still accepts any secp256k1 private key that can produce an ECDSA signature. There is no local comparison between asset_lock_proof_private_key and the P2PKH hash of the locked output when the proof is an instant proof carrying the full transaction, so the method can return a locally successful transition that the server later rejects when it verifies the one-time asset-lock signature against the fetched output script in identity_create/state/v0/mod.rs. The analogous AddressFundingFromAssetLockTransitionV0 path already performs this local key-to-output check when transaction data is available, so the missing check here leaves the new pre-broadcast validation incomplete for one of the main identity-create constructors. Chain proofs still cannot be fully verified locally from this constructor, so the fix should be conditional on asset_lock_proof.transaction() being present rather than changing chain-proof behavior.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/v0_methods.rs`:
- [SUGGESTION] lines 128-130: `IdentityCreateTransition` still returns `Ok` for an instant asset-lock signed with the wrong private key
  This constructor now performs several client-side checks before returning, but its final signing step still accepts any secp256k1 private key that can produce an ECDSA signature. There is no local comparison between `asset_lock_proof_private_key` and the P2PKH hash of the locked output when the proof is an instant proof carrying the full transaction, so the method can return a locally successful transition that the server later rejects when it verifies the one-time asset-lock signature against the fetched output script in `identity_create/state/v0/mod.rs`. The analogous `AddressFundingFromAssetLockTransitionV0` path already performs this local key-to-output check when transaction data is available, so the missing check here leaves the new pre-broadcast validation incomplete for one of the main identity-create constructors. Chain proofs still cannot be fully verified locally from this constructor, so the fix should be conditional on `asset_lock_proof.transaction()` being present rather than changing chain-proof behavior.

@thepastaclaw thepastaclaw force-pushed the fix/validate-transfer-key-security-level-client-side branch from 645ba4b to 27936a2 Compare May 14, 2026 12:32
Copy link
Copy Markdown
Collaborator Author

@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

The PR’s client-side validation changes are mostly sound, and I did not confirm any consensus-critical defect in the scoped Rust transition logic. The only concrete issues I could verify are on the new wasm-sdk error surface: one user-visible regression in the top-level message for multi-error validation failures, and one smaller schema inconsistency in the empty-list fallback.

Reviewed commit: 27936a2

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/wasm-sdk/src/error.rs`:
- [SUGGESTION] lines 273-300: Multi-error protocol failures now hide the actionable validation message at the top level
  Before this PR, `From<ProtocolError>` always surfaced `err.to_string()` as `WasmSdkError.message`. After the new `protocol_with_consensus_errors()` path, any `ProtocolError::ConsensusErrors` with more than one entry is collapsed to `"N consensus errors"` at lines 278-282. The detailed messages do exist under `details.messages`, but existing wasm-sdk consumers and tests in this repo still read `error.message` directly. That makes multi-error local validation failures materially less diagnosable unless every caller is updated in lockstep. The top-level message should keep exposing one real validation message while the full list remains in `details`.

Comment on lines +273 to +300
let kind = if errors.len() == 1 {
"ConsensusError"
} else {
"ConsensusErrors"
};
let message = if errors.len() == 1 {
errors[0].to_string()
} else {
format!("{} consensus errors", errors.len())
};

let _ = Reflect::set(
&details,
&JsValue::from_str("type"),
&JsValue::from_str(kind),
);
let _ = Reflect::set(&details, &JsValue::from_str("messages"), &messages.into());
let _ = Reflect::set(
&details,
&JsValue::from_str("errors"),
&structured_errors.into(),
);

Self {
kind: WasmSdkErrorKind::Protocol,
message,
code: -1,
is_retriable: false,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Multi-error protocol failures now hide the actionable validation message at the top level

Before this PR, From<ProtocolError> always surfaced err.to_string() as WasmSdkError.message. After the new protocol_with_consensus_errors() path, any ProtocolError::ConsensusErrors with more than one entry is collapsed to "N consensus errors" at lines 278-282. The detailed messages do exist under details.messages, but existing wasm-sdk consumers and tests in this repo still read error.message directly. That makes multi-error local validation failures materially less diagnosable unless every caller is updated in lockstep. The top-level message should keep exposing one real validation message while the full list remains in details.

💡 Suggested change
Suggested change
let kind = if errors.len() == 1 {
"ConsensusError"
} else {
"ConsensusErrors"
};
let message = if errors.len() == 1 {
errors[0].to_string()
} else {
format!("{} consensus errors", errors.len())
};
let _ = Reflect::set(
&details,
&JsValue::from_str("type"),
&JsValue::from_str(kind),
);
let _ = Reflect::set(&details, &JsValue::from_str("messages"), &messages.into());
let _ = Reflect::set(
&details,
&JsValue::from_str("errors"),
&structured_errors.into(),
);
Self {
kind: WasmSdkErrorKind::Protocol,
message,
code: -1,
is_retriable: false,
let primary = &errors[0];
let kind = if errors.len() == 1 {
"ConsensusError"
} else {
"ConsensusErrors"
};
let message = primary.to_string();
let code = primary.code() as i32;
let _ = Reflect::set(
&details,
&JsValue::from_str("type"),
&JsValue::from_str(kind),
);
let _ = Reflect::set(&details, &JsValue::from_str("messages"), &messages.into());
let _ = Reflect::set(
&details,
&JsValue::from_str("errors"),
&structured_errors.into(),
);
Self {
kind: WasmSdkErrorKind::Protocol,
message,
code,
is_retriable: false,
details: details.into(),
}

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/wasm-sdk/src/error.rs`:
- [SUGGESTION] lines 273-300: Multi-error protocol failures now hide the actionable validation message at the top level
  Before this PR, `From<ProtocolError>` always surfaced `err.to_string()` as `WasmSdkError.message`. After the new `protocol_with_consensus_errors()` path, any `ProtocolError::ConsensusErrors` with more than one entry is collapsed to `"N consensus errors"` at lines 278-282. The detailed messages do exist under `details.messages`, but existing wasm-sdk consumers and tests in this repo still read `error.message` directly. That makes multi-error local validation failures materially less diagnosable unless every caller is updated in lockstep. The top-level message should keep exposing one real validation message while the full list remains in `details`.

Comment on lines +224 to +240
if errors.is_empty() {
let details = Object::new();
let _ = Reflect::set(
&details,
&JsValue::from_str("type"),
&JsValue::from_str("ConsensusErrors"),
);
let _ = Reflect::set(
&details,
&JsValue::from_str("messages"),
&Array::new().into(),
);
let _ = Reflect::set(
&details,
&JsValue::from_str("consensusErrors"),
&Array::new().into(),
);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

💬 Nitpick: The empty ConsensusErrors fallback exposes a different details shape than the normal path

In the empty-list branch, the structured array is written to details.consensusErrors at lines 236-239, while the non-empty branch writes the same payload to details.errors at lines 290-293. That makes the JS contract depend on an internal edge case: callers that read error.details.errors will get undefined only for this fallback. ProtocolError::from(Vec<ConsensusError>) even has an explicit zero-length match arm in release builds, so keeping one field name here avoids an unnecessary boundary inconsistency.

💡 Suggested change
Suggested change
if errors.is_empty() {
let details = Object::new();
let _ = Reflect::set(
&details,
&JsValue::from_str("type"),
&JsValue::from_str("ConsensusErrors"),
);
let _ = Reflect::set(
&details,
&JsValue::from_str("messages"),
&Array::new().into(),
);
let _ = Reflect::set(
&details,
&JsValue::from_str("consensusErrors"),
&Array::new().into(),
);
let _ = Reflect::set(
&details,
&JsValue::from_str("errors"),
&Array::new().into(),
);

source: ['codex']

Copy link
Copy Markdown
Collaborator Author

@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

The checked-out SHA matches 766d6126a12b9c88ac51f80583f2eda39fe719bc. I verified the Rust constructor changes and found one real regression-protection gap: the new client-side proof-of-possession self-verification paths are not covered by a failing-signer test. The other flagged item is not strong enough to keep as a review finding because this PR intentionally codifies the ProtocolError::Generic shape and already tests for it.

Reviewed commit: 766d612

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs`:
- [SUGGESTION] lines 140-156: New proof-of-possession self-check is untested against a bad signer
  This constructor now verifies each proof-of-possession signature immediately after calling the signer, but the module tests only cover successful signers and pre-signing structural failures. There is no regression test that injects an `identity_public_key_signer` returning a signature for the wrong bytes or wrong key and asserts that the new `verify_signature` loop fails locally. The same untested pattern exists in the analogous loops added to `identity_create_transition` and `identity_update_transition`, so a future refactor could break the signable-byte selection or verification ordering without any test catching it.

Comment on lines +140 to +156
// Verify proof-of-possession signatures we just produced before
// returning, matching the server-side
// `IdentityCreateFromAddressesStateTransitionSignaturesValidationV0`
// check. Only keys with unique types were signed above, so verify
// those exact keys here.
for public_key_with_witness in identity_create_from_addresses_transition.public_keys.iter()
{
if !public_key_with_witness.key_type().is_unique_key_type() {
continue;
}
let pop_result = signable_bytes.as_slice().verify_signature(
public_key_with_witness.key_type(),
public_key_with_witness.data().as_slice(),
public_key_with_witness.signature().as_slice(),
);
if let Some(error) = consensus_errors_as_protocol_error(pop_result) {
return Err(error);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🟡 Suggestion: New proof-of-possession self-check is untested against a bad signer

This constructor now verifies each proof-of-possession signature immediately after calling the signer, but the module tests only cover successful signers and pre-signing structural failures. There is no regression test that injects an identity_public_key_signer returning a signature for the wrong bytes or wrong key and asserts that the new verify_signature loop fails locally. The same untested pattern exists in the analogous loops added to identity_create_transition and identity_update_transition, so a future refactor could break the signable-byte selection or verification ordering without any test catching it.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/v0_methods.rs`:
- [SUGGESTION] lines 140-156: New proof-of-possession self-check is untested against a bad signer
  This constructor now verifies each proof-of-possession signature immediately after calling the signer, but the module tests only cover successful signers and pre-signing structural failures. There is no regression test that injects an `identity_public_key_signer` returning a signature for the wrong bytes or wrong key and asserts that the new `verify_signature` loop fails locally. The same untested pattern exists in the analogous loops added to `identity_create_transition` and `identity_update_transition`, so a future refactor could break the signable-byte selection or verification ordering without any test catching it.

Copy link
Copy Markdown
Collaborator Author

@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

I reviewed the exact checked-out head 14d0059. The general, security, Rust-quality, and FFI reviewers found no concrete correctness, consensus-safety, security, Rust-quality, or cross-language boundary defects in this revision; the prior reported activation/error-propagation issues appear to have been addressed at this head.

Reviewed commit: 14d0059

thepastaclaw and others added 20 commits May 20, 2026 16:34
…UpdateTransition

Add client-side validation of public key purpose/security level
compatibility in try_from_identity_with_signer() before the state
transition is signed and broadcast.

Previously, adding a TRANSFER key with a security level other than
CRITICAL would only be rejected by the network after broadcasting.
Now the validation from validate_identity_public_keys_structure() is
called during transition construction, giving immediate feedback
(e.g. 'Transfer keys must use CRITICAL security level') without
wasting a network round-trip.

This catches issues like trying to create a transfer key with HIGH
or MEDIUM security level, which Platform requires to be CRITICAL.
Extend the same validate_identity_public_keys_structure() check to
IdentityCreateTransition and IdentityCreateFromAddressesTransition.

The previous commit only covered IdentityUpdateTransition (adding keys),
but the same issue affects identity creation — e.g. creating an identity
with a TRANSFER key at non-CRITICAL security level would only be rejected
by the network, with no client-side feedback.
…ameter

Addresses review comment: variable was previously unused but is now
passed to validate_identity_public_keys_structure().
The _platform_version parameters in identity_create_transition and
identity_create_from_addresses_transition are now actively used by
validate_identity_public_keys_structure, so remove the underscore
prefix that conventionally signals unused bindings.
…ansitions

Add client-side structure validation to 6 state transition SDK construction
methods, following the pattern established in PR dashpay#3096. This ensures invalid
transitions are caught early on the client side before being submitted.

State transitions updated:
- AddressCreditWithdrawalTransition
- AddressFundingFromAssetLockTransition
- AddressFundsTransferTransition
- IdentityCreateFromAddressesTransition
- IdentityCreditTransferToAddressesTransition
- IdentityTopUpFromAddressesTransition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cture

Update signing_tests to use valid amounts (>= min thresholds), balanced
input/output sums, and non-empty fee strategies.

Update drive-abci structure_validation tests to use raw transition
construction (bypassing client-side validation) since they intentionally
test server-side rejection of invalid structures.
- Add take_random_amounts_with_range_and_min_per_input to enforce
  min_input_amount per individual input (prevents InputBelowMinimumError)
- Update all address transition constructors to use min_per_input from
  platform_version.dpp.state_transitions.address_funds.min_input_amount
- Cap output_count in transfers so each output >= min_output_amount
- Add remainder distribution to first output to prevent
  InputOutputBalanceMismatchError from integer division
- Relax hardcoded tree structure assertions in checkpoint tests
  (elements count and chunk_depths) to range checks since the
  deterministic output changes with the new amount generation
@thepastaclaw thepastaclaw force-pushed the fix/validate-transfer-key-security-level-client-side branch from 14d0059 to 25a1eff Compare May 20, 2026 21:43
- v0_methods.rs: keep PR's post-signing validate_input_witnesses_count
  call; drop the upstream-removed tracing debug line
- strategy_tests/strategy.rs: keep PR's match-with-snapshot-restore
  error handling; use the renamed
  try_from_asset_lock_with_signer_and_private_key call
- v0/mod.rs tests: update old try_from_asset_lock_with_signer call sites
  to the new method name

(--no-verify: local Node.js 25 + yarn PnP raises EBADF on the JS lint
gate triggered by merged-in dapi-grpc files; CI will run the real lint.
PR scope is Rust-only.)
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.

5 participants