Repository navigation
feat(platform-wallet)!: expose protocol-14-aware signing-key selection - #5588
Claudius-Maginificent wants to merge 58 commits into
Conversation
…s with a disabled key `credit_signing_key` and the identity-update MASTER picker accepted disabled keys. Platform refuses a disabled signing key unconditionally (dpp's `verify_public_key_is_enabled` before signing, Drive's identity-signed validation after), so an identity whose lowest-id TRANSFER or MASTER key was disabled could never transfer, withdraw or update while an enabled replacement sat right next to it. Both now skip disabled keys; the MASTER picker moves into `signing_key.rs` as `master_signing_key` so it is testable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
A client choosing a signing key before it builds a batch needs the levels consensus accepts for a document type's requirement, but that rule only existed inside `combined_security_level_requirement`, which needs a built batch. `accepted_for_requirement` states it once for callers that have no transition yet; tests pin it to what consensus returns for document and token batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…sition is built `ContractBounds::check_scope` answers what `check_batched_transition` answers, for a `ContractBoundsScope` (whole contract, document type, or token) instead of a built batch member, and `check_batched_transition` now delegates to it (the existing per-transition tests pin the unchanged verdicts). `IdentityPublicKey::can_sign_non_batch` states the rule that a contract-bound AUTHENTICATION key signs only batches; a test pins it to the pre-sign check. Clients can now choose a key that Platform will accept without building the transition first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
Clients that choose a key before they hand it to a wallet operation (Dash Evo Tool) kept private copies of the wallet's key policy because the selector was crate-private and covered only AUTHENTICATION keys for one document type. `usable_signing_key` generalizes it over purpose and a `SigningScope` (non-batch, document type, whole contract), using dpp's `check_scope` and non-batch rule for contract bounds, and keeps the split between "no eligible key" (`Ok(None)`) and "eligible key the signer cannot use" (`Err`). `check_signing_key` validates a key the caller chose with the same rules and answers with the typed error Platform returns for each refusal. `usable_authentication_key` becomes a thin wrapper over it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…-request and document writes DPNS registration and marketplace writes, contact-request send and generic document create picked the first key at an accepted level, ignoring contract bounds, expiry and limits: an identity holding a key bound to another contract or document type (or a spent application key) got that key and a refused transition. They now go through `usable_signing_key` via a shared `document_signing_key` (levels from `SecurityLevel::accepted_for_requirement`, which replaces the wallet's own copy of that rule). DPNS registration asks for a key covering the whole DPNS contract, since it writes `preorder` then `domain`. The explicit-key document flows (replace, delete, transfer, set price, purchase) now check the chosen key with `check_signing_key` against the document type's requirement and bounds, so a key Platform would refuse is refused before anything is built, with Platform's own typed error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…ct, identity-update and credit transitions Token operations, contract create/update, identity update and credit transfer/withdrawal now pick their key with `usable_signing_key`: tokens ask for a key covering the token's whole contract (a document-type bound key cannot sign them), the non-batch transitions skip contract-bound AUTHENTICATION keys that Platform refuses outside a batch, and unlimited keys are taken before budgeted ones. The signer-aware `AvailableSigningKey` trait has no callers left and is removed; its tests now exercise `usable_signing_key`. Contract create keeps its CRITICAL-only level set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
Clients that build DashPay auto-accept proofs or contact requests need the same ECDH root key the wallet uses (the first enabled ECDSA_SECP256K1 ENCRYPTION key); exposing the selector lets them call it instead of keeping a copy that drifts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
A key chooser that warns per key (outside its contract bounds, or bound to a contract group only Platform can answer for) needs the three-way bounds verdict the selector uses, not just the chosen key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
Follow the coding conventions for the new DPNS and contact-request key pickers (no inline module paths at call sites) and move a getter import that only the document tests use into their module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
…selection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujJpB9DBhvtbjhYB3aG9Q
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Scope and security rules packages/rs-dpp/src/identity/identity_public_key/..., packages/rs-dpp/src/state_transition/mod.rs, packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/... |
Contract bounds now support whole-contract, document-type, and token scopes. New helpers define non-batch eligibility, accepted security levels, and batch signing purposes. |
Shared selection and typed errors packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs, packages/rs-platform-wallet/src/wallet/identity/network/signing_key.rs, packages/rs-platform-wallet/src/error.rs, packages/rs-platform-wallet/src/lib.rs, packages/rs-platform-wallet/src/wallet/identity/network/mod.rs, packages/rs-platform-wallet-ffi/src/error.rs |
Shared helpers filter and validate keys by purpose, level, type, status, expiry, scope, and signer availability. The wallet adds NoUsableSigningKey and exports the helpers. The FFI conversion maps that error to ErrorUnknown and retains its rendered context. |
Prepared registrations and contract snapshots packages/rs-sdk/src/platform/dashpay/contact_request.rs, packages/rs-sdk/src/platform/dashpay/mod.rs, packages/rs-sdk/src/platform/dpns_usernames/mod.rs, packages/rs-sdk/src/platform/system_data_contract.rs, packages/rs-sdk/src/platform.rs, packages/rs-platform-wallet/src/wallet/identity/network/sdk_writer.rs |
The SDK exposes prepared contact-request and DPNS-registration data with contract IDs and signing requirements. Submission uses the prepared data. System-contract fetching checks protocol-version consistency. |
Document, contract, identity, and credit operations packages/rs-platform-wallet/src/wallet/identity/network/document.rs, packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rs, packages/rs-platform-wallet/src/wallet/identity/network/dpns.rs, packages/rs-platform-wallet/src/wallet/identity/network/dpns_marketplace.rs, packages/rs-platform-wallet/src/wallet/identity/network/contract.rs, packages/rs-platform-wallet/src/wallet/identity/network/update.rs, packages/rs-platform-wallet/src/wallet/identity/network/withdrawal.rs, packages/rs-platform-wallet/src/wallet/masternode_withdrawal.rs |
Document and DPNS operations check keys against document or contract scope. Contract, identity, and credit operations use shared selection or explicit-key validation. Withdrawal selection depends on whether the destination is a chosen address. |
Token operations packages/rs-platform-wallet/src/wallet/identity/network/tokens/* |
Token key selection and explicit-key validation include contract, token-position, and action context. Token operations check supplied keys before transition construction. |
Priority:
Estimated code review effort: 4 (Complex) | ~60 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant WalletOperation
participant KeySelection
participant ContractBounds
participant Signer
WalletOperation->>KeySelection: request or validate key for operation scope
KeySelection->>ContractBounds: check contract, document, or token bounds
KeySelection->>Signer: check key availability
KeySelection-->>WalletOperation: return eligible key or validation error
Possibly related PRs
- dashpay/platform#5546: Both changes address key types accepted for contact-request signing; this pull request routes selection through shared scope-aware checks and includes ECDSA_HASH160 in wallet signing types.
Merge Risk: 
Minimal · previous review 129e5
-
No confirmed issue blocks merging after normal checks.
Pre-merge checks: 
4
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
|
⛔ Final review complete — 1 blocking finding(s) (commit cdb38c5) · triage: critical |
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
Claudius-Maginificent
left a comment
There was a problem hiding this comment.
Review from a consumer's side. Dash Evo Tool would replace its own copy of the protocol-14 key rules with this API, so this checks the PR against two needs: the wallet's own pickers have to be fixed, and a client has to be able to choose and check a key before it builds a transition.
What a client uses
usable_signing_key+SigningScope: picks a key before anything is built. The client needs that key to know which wallet to unlock, and to show "no usable key" before the user submits.check_signing_key: checks a key the user picked by hand, and returns Platform's own typed refusal.SigningScope::bounds_check: lets the client warn about a contract-bound or group-bound key in a key picker.SecurityLevel::accepted_for_requirement: the accepted-levels rule, so clients stop restating it.
The protocol-14 fixes to the wallet's own pickers (bounds, expiry, unlimited-first order, skipping disabled keys) are needed in their own right. The move from allowed_signing_security_levels to accepted_for_requirement, and check_batched_transition now calling check_scope, also remove two copies of the same rule. Both are good.
What has no consumer
IdentityPublicKey::can_sign_non_batch: only a test calls it.SigningScope::bounds_checkandverify_identity_key_boundseach implement the rule themselves.select_own_encryption_keybecoming public: no caller outside the crate, and unrelated to signing-key selection.- The second re-export path
platform_wallet::wallet::identity::{check_signing_key, usable_signing_key, SigningScope, select_own_encryption_key}: the crate-root re-export already covers it.
Dropping these would make the PR smaller without losing anything a client or the wallet uses. Details inline.
Follow-ups, not for this PR
- The automatic operations still report "no key" as
InvalidIdentityData(String), so a caller cannot tell it apart from other failures without matching on text. - The explicit-key paths (
register_name_with_signer, the token*_with_signervariants, an explicit key passed for a credit transfer or withdrawal) still pass the key on without the protocol-14 check that the explicit document operations now get.
Neither blocks this PR; both are inline as notes.
🤖 Co-authored by Claudius the Magnificent AI Agent
This reverts commit 68d0ed0.
…e root only check_signing_key, usable_signing_key and SigningScope were re-exported twice: from the crate root and again from wallet::identity. One public path is enough; the crate root keeps them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SigningScope::bounds_check restated dpp's rule that a contract-bound AUTHENTICATION key cannot sign a non-batch transition, leaving IdentityPublicKey::can_sign_non_batch without a non-test caller. The NonBatch arm now asks can_sign_non_batch, and a test pins the verdict to it for every purpose and bounds kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gKey The operations that pick their own signing key turned the selector's "no eligible key" answer into InvalidIdentityData(String), so a caller could only tell it apart by its text. They now return PlatformWalletError::NoUsableSigningKey with the identity, purpose, accepted security levels and the transition it was to sign: contract create/update, identity update, token operations, document create, DPNS registration and marketplace, and contact-request send. The FFI keeps it on the code InvalidIdentityData had (ErrorUnknown), so the Swift and Kotlin result enums are unchanged; the message carries the context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…registering register_name_with_signer passed the caller's key straight to the SDK, so a key Platform refuses (MASTER, disabled, expired, or bound to only one DPNS document type) was only rejected after network round-trips. It now runs check_signing_key with the automatic path's rules (a HIGH or CRITICAL ECDSA AUTHENTICATION key covering the whole DPNS contract) and returns Platform's typed refusal before any request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… front credit_signing_key only checked that the signer held an explicit key, and the token *_with_signer variants passed theirs on unchecked, so a disabled, expired, wrong-purpose or out-of-bounds key was signed and then refused by Platform. An explicit credit key must now be TRANSFER (or OWNER for a withdrawal) and pass check_signing_key for a non-batch transition. Every token *_with_signer variant checks its key with the automatic token path's rules: a CRITICAL ECDSA AUTHENTICATION key whose bounds cover the token's whole contract. Each refusal is Platform's typed error, returned before any request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…purpose Some transitions accept more than one key purpose (a withdrawal takes TRANSFER or OWNER), so credit_signing_key checked the purpose itself and then called check_signing_key with the key's own purpose, a check that could not fail. check_signing_key now takes the accepted purposes and refuses any other with WrongPublicKeyPurposeError naming all of them, so the rule lives in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The protocol-14 key-selection refactor is broadly correct and the changed wallet paths now reject disabled, expired, limited, and out-of-scope keys during automatic selection. Several non-blocking consistency and ownership issues remain, including incomplete validation for explicitly supplied credit keys and loss of token-position information for group-bound token scopes. Rust wallet and workspace checks are skipped in the supplied CI snapshot, while no relevant code check is reported as failed.
🟡 6 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🟡 Suggestion: Validate explicit credit keys with the same eligibility policy
packages/rs-platform-wallet/src/wallet/identity/network/signing_key.rs:78-87
The automatic credit path now uses usable_signing_key, so it filters disabled, expired, contract-bound, and otherwise ineligible keys. An explicitly supplied credit key still goes directly through require_available, which checks only whether the signer can access it. Consequently, callers of transfer_credits_with_signer and the explicit withdrawal path can select a disabled, expired, wrong-purpose, wrong-level, wrong-type, or contract-bound key and only discover the refusal later during transition signing or Platform validation. Route explicit credit keys through the same purpose, level, type, disabled, expiry, and NonBatch bounds checks used by automatic selection.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large, cross-cutting change to signing-key selection and validation that directly affects cryptographic key handling, signatures, identity updates, and funds movement across platform-wallet and DPP. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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-platform-wallet/src/wallet/identity/network/signing_key.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/signing_key.rs:78-87: Validate explicit credit keys with the same eligibility policy
The automatic credit path now uses `usable_signing_key`, so it filters disabled, expired, contract-bound, and otherwise ineligible keys. An explicitly supplied credit key still goes directly through `require_available`, which checks only whether the signer can access it. Consequently, callers of `transfer_credits_with_signer` and the explicit withdrawal path can select a disabled, expired, wrong-purpose, wrong-level, wrong-type, or contract-bound key and only discover the refusal later during transition signing or Platform validation. Route explicit credit keys through the same purpose, level, type, disabled, expiry, and `NonBatch` bounds checks used by automatic selection.
In `packages/rs-platform-wallet/src/wallet/identity/network/tokens/helpers.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/tokens/helpers.rs:29-43: Preserve the token position in token signing scopes
Token operations pass `SigningScope::Contract`, so a `ContractGroup`-bound key is evaluated with `ContractBoundsScope::Contract` and the returned membership requirement contains `ContractGroupMember::Contract`. Consensus evaluates each token transition with its specific `TokenContractPosition` and returns `ContractGroupMember::Token(position)`. Automatic selection currently skips group-bound keys, so this is conservative for existing wallet flows, but the public `bounds_check` API exposes an inaccurate preflight result for token callers. Add a token-specific signing scope carrying the token position and use it for token operations, while retaining `Contract` for operations that truly cover the whole contract.
In `packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:48-64: Delegate non-batch bounds checks to the DPP helper
The `NonBatch` branch implements the contract-bound rule locally by returning `Denied` for every contract-bound AUTHENTICATION key. The same PR adds `IdentityPublicKey::can_sign_non_batch()` in DPP as the protocol-owned predicate used to express this consensus rule. Keeping a second implementation in the wallet currently produces the same result, but future protocol changes can make wallet preselection and DPP signing disagree. Use the DPP helper for this branch and keep the wallet layer responsible for signer availability and ordering.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:91-98: Do not treat a pre-epoch clock as Unix epoch
The new selector uses `now_ms()` as an eligibility check for key expiry, but `now_ms()` returns `0` when `SystemTime` is before the Unix epoch. On a device with an invalid or unset clock, a key with any positive expiry is therefore treated as unexpired and may be selected, despite the selector promising that returned keys are currently eligible. The resulting transition is then rejected when Platform evaluates it against block time. Make clock failure explicit or fail closed for expiry-bearing keys instead of silently using epoch zero.
In `packages/rs-dpp/src/identity/identity_public_key/security_level.rs`:
- [SUGGESTION] packages/rs-dpp/src/identity/identity_public_key/security_level.rs:109-120: Make accepted security levels the single DPP-owned rule
`accepted_for_requirement` documents itself as the consensus rule, but `combined_security_level_requirement` still independently constructs the CRITICAL/HIGH/MEDIUM range. The agreement tests show that both implementations currently match, but they do not prevent a future protocol change from updating one and not the other. Keep the batch method responsible for aggregating the strongest requirement, then delegate construction of the accepted level list to this helper.
In `packages/rs-platform-wallet/src/wallet/identity/network/dpns.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/dpns.rs:48-59: Derive the DPNS registration requirement from both document types
DPNS registration signs both `preorder` and `domain`, but the shared selector is called with a hardcoded `SecurityLevel::HIGH`. The marketplace selector already obtains its requirement from the fetched contract. If either DPNS document type's requirement changes, registration can select a key that does not satisfy the combined batch requirement and fail later, while marketplace operations continue to follow the contract. Resolve both document types from the fetched contract and pass the strongest requirement to the shared selector.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- DPP automatic identity transition pickers still allow disabled keys — The DPP constructors for identity credit transfer, withdrawal, transfer-to-addresses, and shield-from-identity still pass
allow_disabled = truetoget_first_public_key_matching, so they can choose a disabled key beforesign_externalrejects it. These are pre-existing DPP call sites that the PR explicitly leaves unchanged while fixing platform-wallet selection; they should be tracked as a separate DPP hardening change.- Follow-up: Create or link a separate rs-dpp issue covering enabled-only automatic key selection and fallback tests for all four constructors.
|
Ready for review — |
…stration's Debug output DpnsNameRegistration derived Debug through its documents, so formatting a prepared registration printed the domain document: the clear label and, before DPNS v3, the preorderSalt its preorder commits to. A log line carrying them before the preorder lands lets anyone race for the name. DpnsNameRegistration now implements Debug by hand and shows only the identity id, the DPNS contract id and the signing-key security level. The private DpnsRegistrationDocuments no longer derives Debug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AUTHENTICATION key qualifies Automatic lone-transfer/claim selection also took a TRANSFER key when the identity held a qualifying CRITICAL AUTHENTICATION key the signer could not reach, which turned the signer-unavailable error such an identity always got into a signature by another key. A purpose is now tried only when no key of the earlier ones qualifies, so identities with a qualifying AUTHENTICATION key keep their exact previous outcome and TRANSFER is a pure fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…batch key-purpose rule A client choosing the key for a token transition has no built batch to ask purpose_requirement, so the wallet restated the rule by hand. The rule now lives in one pure function: the purposes a key may have to sign a version 1 batch, given the action of its only transition when that is a token transition. purpose_requirement delegates to it, so Drive and clients run the same code. In-place, output-identical refactor of the shipped BatchTransitionV1 (protocol versions 9 to 15): action_type() is Transfer exactly for TokenTransition::Transfer and Claim exactly for TokenTransition::Claim, the two transitions the previous as_transition_token_transfer / as_transition_token_claim checks matched; every other batch still gets AUTHENTICATION alone. Tests pin the outcome for every token action, a lone document, several transitions and an empty batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… operation Drop the hand-kept TOKEN_SIGNING_PURPOSES and LONE_TOKEN_TRANSFER_SIGNING_PURPOSES copies of consensus's batch purpose rule. check_token_signing_key and token_resolve_signing_key take the operation's TokenTransitionActionType and read the allowed purposes from BatchTransitionV1::signing_key_purposes, since every token operation the wallet sends is a batch holding that one transition. Selection behaviour is unchanged: AUTHENTICATION first, TRANSFER only for a lone transfer or claim of an identity with no qualifying AUTHENTICATION key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The reviewed head fixes the previously identified signing-key selection, token-purpose, contract-snapshot, bounds-ordering, and DPNS redaction issues. Five non-blocking issues remain: local-clock expiry decisions, duplicated document-create and security-level protocol rules, the missing Unreleased changelog entry, and acceptance of a stale system contract after a failed protocol refresh. No blocking correctness issue was found; several CI and policy checks were still pending in the supplied snapshot.
🟡 5 suggestion(s)
4 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large, intricate change to identity signing-key selection, contract-bound authorization, signature construction, and funds-related wallet operations across DPP and platform-wallet, directly affecting cryptographic key handling and signing correctness in functions such as key selection and signing-key generation. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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-sdk/src/platform/system_data_contract.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/system_data_contract.rs:66-83: Do not accept a stale system contract after a failed protocol refresh
`fetch_current_system_data_contract()` calls `refresh_protocol_version()`, but that method deliberately swallows refresh failures and returns the SDK's previously learned version. If the SDK starts at an older version, the refresh fails, and a node still returns a proof at that old version, `contract_proved_at_parsed_version()` can see matching metadata and accept the stale contract even though the network has upgraded. Require a successful current-version observation before accepting the contract, or propagate the inability to establish the current protocol version instead of treating the cached version as authoritative.
In `packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:146-149: Use an authoritative or conservative time source for expiry eligibility
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4393955798)
Automatic and explicit key checks compare expiry with the device's local wall clock, while Platform evaluates expiry against consensus block time. If the device clock lags the network, the wallet can select or approve a key that is already expired when the transition is processed, causing rejection after signing and submission. Use trusted SDK response metadata or a last verified protocol timestamp where available; otherwise apply a conservative skew margin or fail closed for expiry-bearing keys.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:117-125: Centralize document-create consumed-type bounds semantics in DPP
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4393955780)
`SigningScope::DocumentCreate` independently walks `consumable_document_type_names()` and applies `check_scope`, while Drive's batch advanced-structure validation performs the same aggregation. The implementations currently agree, but future changes to consume semantics or contract bounds can make wallet preflight select a key that consensus rejects, or reject one consensus accepts. Add a DPP-owned document-create scope verdict and have both the wallet and Drive consume it, leaving stateful contract-group membership resolution in Drive. This remains untracked, so it cannot yet be reasonably deferred.
In `packages/rs-dpp/src/identity/identity_public_key/security_level.rs`:
- [SUGGESTION] packages/rs-dpp/src/identity/identity_public_key/security_level.rs:109-120: Make accepted security levels the single DPP-owned rule
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4388917936)
`accepted_for_requirement()` independently constructs the accepted CRITICAL/HIGH/MEDIUM range, while `combined_security_level_requirement()` retains a second construction of the same rule. The agreement tests protect the current output but do not prevent a future protocol change from updating one implementation without the other. Keep batch processing responsible for finding the strongest requirement, then delegate accepted-level construction to `SecurityLevel::accepted_for_requirement()`.
In `packages/rs-platform-wallet/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/error.rs:89-104: Record the breaking wallet error API in the Unreleased changelog
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4390783260)
`PlatformWalletError::NoUsableSigningKey` is a new public enum variant and breaks exhaustive Rust matches. The PR description documents the change, but the active `CHANGELOG.md` Unreleased section does not. The repository history includes breaking feature commits such as `60cc5f4508` that update the Unreleased section in the PR, so add an entry describing the variant and noting that FFI result codes remain unchanged.
…lished way Drop Sdk::fetch_current_system_data_contract, contract_proved_at_parsed_version and SYSTEM_CONTRACT_FETCH_ATTEMPTS. prepare_contact_request goes back to fetch_dashpay_contract (the context provider's contract at the SDK's protocol version, fetched and proved when it holds none), as before this branch. The key is still chosen from the same contactRequest type the document is built as. DPNS registration keeps the DPNS-local proved-at-parsed-version fetch that #5337 introduced, restored verbatim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WunYYjpfLux1ByK7fGF7SG
|
Correction to my earlier reply on the 🤖 Co-authored by Claudius the Magnificent AI Agent |
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The PR fixes most of the protocol-14 signing-key selection issues, but several correctness and ownership concerns remain. The Rust wallet still relies on potentially stale cached system contracts, uses local wall-clock time for consensus expiry decisions, and duplicates document-create and security-level rules; the Swift SDK also continues to bypass the new Rust-owned selection policy. Changed-package CI checks were still queued at the captured time.
🔴 1 blocking | 🟡 4 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🔴 Blocking: Route Swift document operations through the Rust signing-key owner
packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift:29-55
The first-party Swift path still ranks signing candidates in Swift through KeyManager.rankSigningCandidates and hardcodes CRITICAL for both contract creation and update. Its document create/replace/delete/transfer/price/purchase operations therefore bypass the Rust policy added by this PR, including contract bounds, expiry, disabled-key handling, weakest-sufficient levels, and supported HASH160 keys. This leaves a second signing-key policy in a mobile SDK and can cause Swift to select a key that Platform rejects or require unnecessarily strong authentication. Marshal the operation and contract/document context through the Rust platform-wallet selector rather than maintaining policy in Swift.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)
4 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large cross-cutting change that modifies consensus-relevant signing-key selection, contract-bound authorization, cryptographic signing eligibility, and funds-moving wallet operations, notably inkey_selection.rs,signing_key.rs, and the DPP identity-key validation code. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift:29-55: Route Swift document operations through the Rust signing-key owner
The first-party Swift path still ranks signing candidates in Swift through `KeyManager.rankSigningCandidates` and hardcodes CRITICAL for both contract creation and update. Its document create/replace/delete/transfer/price/purchase operations therefore bypass the Rust policy added by this PR, including contract bounds, expiry, disabled-key handling, weakest-sufficient levels, and supported HASH160 keys. This leaves a second signing-key policy in a mobile SDK and can cause Swift to select a key that Platform rejects or require unnecessarily strong authentication. Marshal the operation and contract/document context through the Rust platform-wallet selector rather than maintaining policy in Swift.
In `packages/rs-sdk/src/platform/system_data_contract.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/system_data_contract.rs:17-22: Do not accept a stale system contract after a failed protocol refresh
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4405158169)
Although the former refresh helper was removed, the replacement still returns any contract supplied by the context provider before making a proved network fetch. The trusted provider cache does not verify the requested protocol version when it finds a known contract, so an in-place-updated DashPay contract can remain stale after a protocol or system-contract upgrade. Contact-request preparation then builds and selects a key against that stale schema, and Platform rejects the transition only after signing. Require current-version provenance for cached contracts or invalidate/refetch them when freshness cannot be established.
In `packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:146-149: Use an authoritative or conservative time source for expiry eligibility
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4393955798)
Automatic selection compares key expiry with the device's local wall clock, while Platform evaluates expiry against consensus block time. The new pre-epoch fail-closed behavior only handles an unreadable clock; a clock that lags the network can still select a key that Platform already considers expired. The wallet can therefore sign and submit a transition that is rejected after the signing operation, potentially consuming fees or nonces. Use trusted block-time metadata when available or apply a conservative margin to expiry-bearing keys.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:117-130: Centralize document-create consumed-type bounds semantics in DPP
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4393955780)
`SigningScope::DocumentCreate` now manually walks `consumable_document_type_names()` and applies `check_scope`, while Drive's batch advanced-structure validation independently performs the same consumed-type aggregation. This PR introduced the wallet-side copy as part of its new public preflight policy. The current implementations agree, but a future change to consume semantics or bounds handling can make wallet selection accept a key that consensus rejects. Add a DPP-owned document-create scope verdict and have both wallet preflight and Drive consume it, leaving only state-dependent contract-group membership resolution in Drive.
In `packages/rs-dpp/src/identity/identity_public_key/security_level.rs`:
- [SUGGESTION] packages/rs-dpp/src/identity/identity_public_key/security_level.rs:109-120: Make accepted security levels the single DPP-owned rule
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4388917936)
`SecurityLevel::accepted_for_requirement` presents the accepted-level set as the protocol rule, but the batch requirement implementation still constructs the CRITICAL/HIGH/MEDIUM range independently, and Drive has another equivalent construction. The agreement tests pin today's values but do not prevent a future protocol change from updating only one implementation. Keep batch code responsible for finding the strongest requirement, then delegate accepted-level construction to the DPP helper and update the consensus callers together.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Add a dedicated FFI result code for NoUsableSigningKey — The new Rust error is intentionally mapped to the existing generic FFI error code, so Swift and Kotlin ABI enums remain unchanged. Hosts cannot distinguish NoUsableSigningKey without inspecting the message, but assigning a new stable code requires a coordinated platform-wallet-ffi, Swift, and Kotlin API change outside this PR.
- Follow-up: Track a coordinated FFI and mobile API update that assigns and propagates a dedicated NoUsableSigningKey result code.
…let-public-signing-key-selection Conflicts: - rs-platform-wallet/src/lib.rs: export both the public signing-key selection API and PreparedDashPayPayment. - identity/network/signing_key.rs: keep the withdrawal test and both families of test wallet helpers, sharing wallet_with_identity_in so a test can choose the SDK, the store and the identity. update_identity_tests imports KeyType and SecurityLevel itself: this branch removed them from update.rs's top-level imports, and the tests added on the base reached them through `use super::*`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WunYYjpfLux1ByK7fGF7SG
…let-public-signing-key-selection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WunYYjpfLux1ByK7fGF7SG
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The Rust-side signing policy is substantially centralized and the current snapshot/bounds changes are mostly correct. One blocking layer-ownership issue remains: Swift still independently selects keys and bypasses the new Rust policy. Additional non-blocking issues remain around stale cached system contracts, local-clock expiry checks, omitted BLS keys from automatic selection, and duplicated security-level policy; Rust workspace tests and several package checks were still pending at the captured CI state.
🔴 1 blocking | 🟡 4 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🔴 Blocking: Route Swift document operations through the Rust signing-key owner
packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift:29-55
Swift selectSigningKey still owns signing policy: it hardcodes CRITICAL for contract operations and prefers CRITICAL for document operations, while only delegating signer availability to Rust. The helper does not apply the Rust selector's protocol-14 bounds, expiry, disabled-key, key-type, or weakest-sufficient-level rules. All Swift document operations continue to call it, so supported Swift clients can select a different key or reject a valid HIGH, BLS, HASH160, or bounded-key path than rs-platform-wallet. Move selection through the Rust wallet/FFI API and leave Swift responsible only for marshalling and signer callbacks.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, security-auditor)
3 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large cross-cutting change that directly alters signing-key selection, authentication requirements, contract-bound validation, and funds-related wallet operations across DPP and platform-wallet code, including the critical key-handling surface inkey_selection.rsandsigning_key.rs. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity quota lookup failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 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/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/FFI/StateTransitionExtensions.swift:29-55: Route Swift document operations through the Rust signing-key owner
Swift `selectSigningKey` still owns signing policy: it hardcodes CRITICAL for contract operations and prefers CRITICAL for document operations, while only delegating signer availability to Rust. The helper does not apply the Rust selector's protocol-14 bounds, expiry, disabled-key, key-type, or weakest-sufficient-level rules. All Swift document operations continue to call it, so supported Swift clients can select a different key or reject a valid HIGH, BLS, HASH160, or bounded-key path than `rs-platform-wallet`. Move selection through the Rust wallet/FFI API and leave Swift responsible only for marshalling and signer callbacks.
In `packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:40-50: Include BLS keys in automatic document and token selection
`WALLET_SIGNING_KEY_TYPES` contains only `ECDSA_SECP256K1` and `ECDSA_HASH160`, while `PLATFORM_SIGNING_KEY_TYPES` also includes `BLS12_381`. The generic selector documents and implements DIP-11 ordering with BLS before HASH160, but document, contract, identity-update, and token automatic selectors pass the narrower array. Consequently, an identity with only a valid BLS AUTHENTICATION key can fail automatic selection with `NoUsableSigningKey`, even though Platform verifies BLS identity signatures and the explicit-key paths accept it. Include BLS in the automatic set or document and enforce a deliberate signer-level reason why BLS is credit-only.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/key_selection.rs:146-149: Use an authoritative or conservative time source for expiry eligibility
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4393955798)
Automatic selection compares key expiry with the device's local `SystemTime`, while Platform evaluates expiry against consensus block time. A device clock that lags the network can select a key already expired according to Platform, causing the subsequent transition to fail after signing preparation. The typed Platform refusal and no-fee behavior limit the impact, but the selector still reports an unusable key as eligible and creates an avoidable availability failure. Use trusted protocol time metadata when available or apply a conservative expiry margin.
In `packages/rs-sdk/src/platform/system_data_contract.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/system_data_contract.rs:10-23: Do not accept a stale system contract after a failed protocol refresh
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4405158169)
The earlier `fetch_current_system_data_contract` refresh implementation is gone, but the replacement `fetch_system_data_contract` still returns any contract held by the context provider for `self.version()` without establishing a fresh network observation or proving that the cached contract is current. `prepare_contact_request` relies on this helper. During a system-contract or protocol upgrade, contact-request preparation can therefore use an obsolete DashPay schema and build a transition that Platform rejects. Require a fresh proof-backed version observation for the current-contract path, or expose that freshness is unavailable instead of treating the cached contract as authoritative.
In `packages/rs-dpp/src/identity/identity_public_key/security_level.rs`:
- [SUGGESTION] packages/rs-dpp/src/identity/identity_public_key/security_level.rs:109-120: Make accepted security levels the single DPP-owned rule
(existing thread: https://github.com/dashpay/platform/pull/5588#discussion_r4388917936)
`SecurityLevel::accepted_for_requirement` is presented as the consensus rule, but `combined_security_level_requirement` still independently constructs the CRITICAL/HIGH/MEDIUM range in the batch-transition implementation. The current agreement tests prove only that the two implementations match today; they do not prevent a future protocol change from updating one without updating the other. Keep batch logic responsible for aggregating the strongest requirement, then delegate accepted-level construction to this DPP helper when the consensus-side follow-up is made.
| /// The key types the wallet picks its own signing keys from, except for a credit transfer or | ||
| /// withdrawal key, which may be any of [`PLATFORM_SIGNING_KEY_TYPES`]. Pass these as | ||
| /// `key_types` to [`usable_signing_key`] to choose the key the wallet itself would; a key the | ||
| /// caller chose is checked against [`PLATFORM_SIGNING_KEY_TYPES`] instead. | ||
| /// | ||
| /// `ECDSA_HASH160` is here so identities whose keys hold only the hash of the public key, the | ||
| /// default layout older Dash Evo Tool versions created, keep signing wherever Platform | ||
| /// accepts the key; [`usable_signing_key`] still prefers an `ECDSA_SECP256K1` key, the type | ||
| /// DIP-11 defines. | ||
| pub const WALLET_SIGNING_KEY_TYPES: [KeyType; 2] = | ||
| [KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160]; |
There was a problem hiding this comment.
🟡 Suggestion: Include BLS keys in automatic document and token selection
WALLET_SIGNING_KEY_TYPES contains only ECDSA_SECP256K1 and ECDSA_HASH160, while PLATFORM_SIGNING_KEY_TYPES also includes BLS12_381. The generic selector documents and implements DIP-11 ordering with BLS before HASH160, but document, contract, identity-update, and token automatic selectors pass the narrower array. Consequently, an identity with only a valid BLS AUTHENTICATION key can fail automatic selection with NoUsableSigningKey, even though Platform verifies BLS identity signatures and the explicit-key paths accept it. Include BLS in the automatic set or document and enforce a deliberate signer-level reason why BLS is credit-only.
| /// The key types the wallet picks its own signing keys from, except for a credit transfer or | |
| /// withdrawal key, which may be any of [`PLATFORM_SIGNING_KEY_TYPES`]. Pass these as | |
| /// `key_types` to [`usable_signing_key`] to choose the key the wallet itself would; a key the | |
| /// caller chose is checked against [`PLATFORM_SIGNING_KEY_TYPES`] instead. | |
| /// | |
| /// `ECDSA_HASH160` is here so identities whose keys hold only the hash of the public key, the | |
| /// default layout older Dash Evo Tool versions created, keep signing wherever Platform | |
| /// accepts the key; [`usable_signing_key`] still prefers an `ECDSA_SECP256K1` key, the type | |
| /// DIP-11 defines. | |
| pub const WALLET_SIGNING_KEY_TYPES: [KeyType; 2] = | |
| [KeyType::ECDSA_SECP256K1, KeyType::ECDSA_HASH160]; | |
| pub const WALLET_SIGNING_KEY_TYPES: [KeyType; 3] = [ | |
| KeyType::ECDSA_SECP256K1, | |
| KeyType::BLS12_381, | |
| KeyType::ECDSA_HASH160, | |
| ]; |
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality)
TL;DR
Since protocol version 14 an identity key can carry a budget, an expiry or contract bounds, and Platform refuses a key that doesn't fit the transition. Only two of the wallet's own writes (DashPay profile and contactInfo) knew that. Clients such as Dash Evo Tool kept their own copies of the rules. This PR:
ECDSA_HASH160) and acceptsECDSA_HASH160keys, which Dash Evo Tool identities hold,Issue being fixed or feature implemented
usable_authentication_keywaspub(crate)and only covered AUTHENTICATION keys for one document type. Dash Evo Tool re-implemented it, together with dpp's contract-bounds rule and the "levels accepted for a requirement" rule, and its copy has already drifted from the wallet's ordering.credit_signing_keyand the identity-update MASTER picker passedallow_disabled = true. Platform refuses a disabled signing key unconditionally: dpp'sverify_public_key_is_enabledrefuses it before signing, and Drive's identity-signed validation refuses it after.ECDSA_HASH160identities could not sign. Identities created by older Dash Evo Tool versions holdECDSA_HASH160authentication keys. Platform accepts them on every identity-signed transition, but automatic selection read onlyECDSA_SECP256K1keys.What was done?
dpp (no consensus behaviour change)
SecurityLevel::accepted_for_requirement(requirement): the levels a key may carry to meet a document type's requirement. Tests pin it tocombined_security_level_requirementfor document and token batches.ContractBoundsScope(Contract/DocumentType(&str)/Token(position)) andContractBounds::check_scope(contract_id, scope): the bounds verdict before a transition is built.check_batched_transitiondelegates to it. The existing per-transition tests stay green and pin the verdicts as unchanged.IdentityPublicKey::can_sign_non_batch(): a contract-bound AUTHENTICATION key signs only batches. A test pins it to the pre-sign check for every purpose and every bounds kind. The wallet'sSigningScope::bounds_checkuses it, so the rule is written once.rs-sdk (additive)
Sdk::prepare_dpns_name_registration/Sdk::submit_dpns_name_registration: prepare takes the network's current, proved DPNS contract, builds thepreorderanddomaindocuments, and returns aDpnsNameRegistrationstating the signing-level requirement and contract id of that snapshot. Submit signs and submits exactly those documents.register_dpns_name*are now prepare + submit.Sdk::prepare_contact_request/Sdk::submit_contact_request: prepare refreshes the protocol version, takes the network's current, proved DashPay contract and returns aPreparedContactRequestholding thecontactRequestdocument type of that snapshot; submit builds and submits as exactly that type.create_contact_requestandsend_contact_requestkeep their signatures and use one snapshot each.Sdk::fetch_current_system_data_contract(inplatform::system_data_contract) is the shared "proved at the parsed protocol version" fetch behind both.platform-wallet: new public API, re-exported from the
platform_walletcrate rootSigningScope<'a>:NonBatch/DocumentType { contract_id, document_type_name }/Contract { contract_id }/Token { contract_id, position }.SigningScope::bounds_check(&key)returns dpp's three-wayBatchedTransitionBoundsCheck, using the sameContractBoundsScopeconsensus uses. ForNonBatchit asksIdentityPublicKey::can_sign_non_batch.usable_signing_key(identity, signer, purpose, security_levels, key_types, scope) -> Result<Option<&IdentityPublicKey>, dash_sdk::Error>:ECDSA_SECP256K1,BLS12_381) beforeECDSA_HASH160, then the weakest sufficient security level, then the lowest key id. Security levels are minimums: a stronger key signs only when the signer cannot reach a weaker one. Consensus accepts either, so no on-chain behaviour changes.Ok(None)means no key is eligible.Errmeans an eligible key exists but the signer cannot use it.check_signing_key(key, signer, purposes, security_levels, key_types, scope) -> Result<(), dash_sdk::Error>checks a key the caller chose explicitly, using the same rules.purposeslists every purpose the transition accepts; a withdrawal, for example, takes TRANSFER or OWNER. Each refusal is the consensusSignatureErrorPlatform itself returns, checked in Drive's order:InvalidIdentityPublicKeyTypeError,InvalidSignaturePublicKeyPurposeError,InvalidSignaturePublicKeySecurityLevelError,PublicKeyIsDisabledError; thenPublicKeyExpiredError,ContractBoundedKeyOutOfBoundsErrororContractBoundedKeyNonBatchError. A group-bound key is accepted and left to Platform. A key the signer cannot use gets the existing signer-unavailable error. With the clock before the Unix epoch, a key with an expiry is refused withdash_sdk::Error::Config.PlatformWalletError::NoUsableSigningKey { identity_id, purpose, security_levels, key_types, transition }: no key of the identity can signtransition. It is distinct from the signer-unavailable error. In the FFI it keeps the codeInvalidIdentityDatahad (ErrorUnknown), so the Swift and Kotlin result enums are unchanged.select_own_encryption_keyis now public.platform-wallet: own selectors moved onto it
WALLET_SIGNING_KEY_TYPESis now[ECDSA_SECP256K1, ECDSA_HASH160](was[ECDSA_SECP256K1]). This is a public API change: the constant is re-exported from the crate root and its type changes from[KeyType; 1]to[KeyType; 2]. The wallet's own document, token, contract and identity-update selectors pick from it, soECDSA_HASH160keys (Dash Evo Tool identities) are accepted by automatic selection wherever consensus accepts them, and a DIP-11 key type is still preferred when the identity holds both. Credit transfer/withdrawal selection and every explicit-key check usePLATFORM_SIGNING_KEY_TYPES(ECDSA_SECP256K1,BLS12_381,ECDSA_HASH160), the set Drive verifies.preorderand thendomain. One key signs both documents, so its level must meet the stronger of their two requirements.register_name_with_signerandregister_name_with_external_signercheck or select the key from theDpnsNameRegistrationreturned byprepare_dpns_name_registrationand submit that same value. Public signatures are unchanged, and the extra DPNS contract round-trip is gone.PreparedContactRequest(thecontactRequesttype of the network's current DashPay contract) and hands the same value tosubmit_contact_request. Public signatures are unchanged.SigningScope::Token: the key's bounds must cover that token.NonBatch.NoUsableSigningKeyinstead of anInvalidIdentityDatastring: contract create/update, identity update, token operations, document create, DPNS registration and marketplace, contact-request send.InvalidIdentityDataerrors (see Follow-ups). They do not useWALLET_SIGNING_KEY_TYPES.register_name_with_signer: an AUTHENTICATION key at the level the DPNSpreorderanddomaindocuments require, whose bounds cover the whole DPNS contract.to_addressis given (Platform refuses an OWNER key for a withdrawal to a chosen address,WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError). Automatic selection follows the same rules. The key must be enabled and unexpired.token_*_with_signer: a CRITICAL AUTHENTICATION key whose bounds cover the token.token_transfer_with_signerandtoken_claim_with_signeralso take a CRITICAL TRANSFER key, which consensus allows for a batch of exactly one token transfer or claim.AvailableSigningKeytrait is removed, since nothing calls it any more.usable_authentication_keybecomes a thin wrapper, so the profile and contactInfo call sites are untouched.How Has This Been Tested?
ECDSA_HASH160of the same group; aECDSA_HASH160-only identity still signs. Credit keys: CRITICAL TRANSFER keys {3 HASH160, 4 SECP256K1} pick 4 (pinned incredit_signing_keytests).prepare_*tests share one fixture that raises a document type'ssignatureSecurityLevelRequirement).should_skip_disabled_transfer_keys_before_owner_fallbackshould_skip_a_disabled_master_keyshould_choose_a_document_key_whose_bounds_cover_the_document_typeshould_choose_an_unlimited_document_key_before_a_limited_oneshould_pick_only_a_credit_key_of_a_type_platform_verifiesshould_sign_credit_operations_only_with_a_critical_keyshould_require_the_level_the_contact_request_document_type_requiresshould_refuse_an_explicit_document_key_platform_would_refuse: extended so it also acceptsECDSA_HASH160andBLS12_381keys and refusesEDDSA_25519_HASH160.should_register_with_a_key_that_covers_both_dpns_document_typesshould_sign_a_contact_request_with_a_key_bound_to_contact_requests_or_unboundshould_sign_a_token_operation_with_a_key_that_covers_the_whole_contractshould_sign_a_contract_transition_only_with_a_key_free_of_contract_boundsshould_report_no_usable_contract_key_as_a_typed_errorshould_report_no_usable_master_key_as_a_typed_errorshould_report_no_usable_token_key_as_a_typed_errorshould_refuse_an_explicit_registration_key_before_any_network_callshould_refuse_an_explicit_credit_key_platform_would_refuseshould_refuse_an_explicit_token_key_before_any_network_callshould_accept_an_explicit_transfer_key_for_a_lone_token_transfer_or_claimshould_accept_an_explicit_token_key_of_any_type_platform_verifiesshould_report_a_token_scope_as_the_token_consensus_checksshould_not_trust_an_expiry_without_a_valid_clockshould_require_the_strongest_level_of_both_registration_documentsshould_not_sign_a_withdrawal_to_an_address_with_an_owner_keyusable_signing_key,check_signing_keyandbounds_check, plus dpp agreement tests that tie the new helpers to the existing consensus logic.should_deny_a_non_batch_signature_exactly_when_dpp_says_the_key_cannot_sign_one: covers every purpose dpp decodes and every bounds kind.should_keep_no_usable_signing_key_on_its_former_code_with_the_context_in_the_message(platform-wallet-ffi).cargo test -p platform-wallet -p dash-sdkpasses on155f8f1667;cargo clippy -p platform-wallet -p dash-sdk --all-features --tests -- -D warningsis clean.cargo test -p platform-wallet-ffi --lib error::passed.cargo test -p dpp --lib --features state-transition-signing,bls-signaturespassed, filtered to the touched modules.cargo clippy -p dpp -p platform-wallet -p platform-wallet-ffi --all-targets --all-features -- -D warningsis clean.Breaking Changes
WALLET_SIGNING_KEY_TYPESchanges type from[KeyType; 1]to[KeyType; 2](ECDSA_SECP256K1,ECDSA_HASH160). It is re-exported from theplatform_walletcrate root. Code that destructures or indexes it as a one-element array no longer compiles; passing it as&WALLET_SIGNING_KEY_TYPEStousable_signing_keystill works. No in-repo consumer outside the crate uses it.PlatformWalletErrorgains the variantNoUsableSigningKey. That breaks an exhaustivematchonPlatformWalletErrorthat has no wildcard arm. The FFI result codes, Swift and Kotlin are unchanged.prepare_*/submit_*methods. platform-wallet function signatures are unchanged.Behaviour changes that clients will see:
ECDSA_HASH160keys now sign (automatic selection accepts them), after any DIP-11-type key of the same group.NoUsableSigningKeyinstead ofInvalidIdentityData(String). FFI hosts still seeErrorUnknown, but the message text changes.InvalidIdentityDatastring.BIP13_SCRIPT_HASH, is refused up front.SignatureErrors rather thanProtocolErrorvariants such asWrongPublicKeyPurposeError.In-place changes to shipped generations
None. Drive and drive-abci are unchanged. The dpp edits are on the client signing side.
verify_identity_key_bounds(signing path,state-transition-signing) is refactored to usecan_sign_non_batchwithout changing behaviour, which an exhaustive purpose × bounds test pins. The new helperscheck_scope,accepted_for_requirementandcan_sign_non_batchmirror Drive's rules, but Drive does not call them.BatchTransitionV1::purpose_requirement(rs-dpp/.../batch_transition/v1/identity_signed.rs). Protocol versions 9 to 15 select batch structure version 1 (batch_state_transitiondefault 1 fromSTATE_TRANSITION_SERIALIZATION_VERSIONS_V2). The method now delegates to the new pureBatchTransitionV1::signing_key_purposes. Consensus cannot change, because the refactor is output-identical:TokenTransitionActionTypeGetter::action_type()returnsTransferexactly forTokenTransition::TransferandClaimexactly forTokenTransition::Claim. Those are the only transitions the oldas_transition_token_transfer()/as_transition_token_claim()checks matched.[AUTHENTICATION].PlatformVersion.v1_signing_key_purposes_pin_every_lone_token_action_and_any_other_batch,v1_purpose_requirement_answers_through_signing_key_purposesand the existingv1_purpose_requirement_*tests.Follow-ups (not in this PR)
combined_security_level_requirement(dpp) and Drivestate_transition_action/batch/mod.rscould useSecurityLevel::accepted_for_requirement.StateTransition::verify_identity_key_bounds(dpp) and drive-abcivalidate_state_transition_identity_signed/v1could useIdentityPublicKey::can_sign_non_batchfor the non-batch rule.IdentityCreditTransferTransition/IdentityCreditWithdrawalTransition/IdentityCreditTransferToAddressesTransition/ShieldFromIdentityTransitiontry_from_identitypassesallow_disabled = truetoget_first_public_key_matching, the same disabled-key problem as the wallet bug fixed here.ECDSA_HASH160. It conflicts textually with this PR incontact_requests.rs(the contact-request selector moves ontodocument_signing_keyand the prepared request here). Whichever PR merges second resolves the conflict.profile.rsandcontact_info.rsare untouched here, and fix(platform-wallet): support HASH160 DashPay contact signing keys #5546 owns contactInfo.InvalidIdentityDatawhen no key qualifies. After both PRs merge, they can move ontoWALLET_SIGNING_KEY_TYPESandNoUsableSigningKey.selectSigningKeyin swift-sdkStateTransitionExtensions.swiftstill picks document keys withpreferCritical: trueand asks for CRITICAL on contract create, contrary to the policy here. It belongs inrs-platform-walletkey_selection.NoUsableSigningKeyFFI result code would need the Swift and Kotlin result enums mirrored in a coordinated change.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Co-authored by Claudius the Magnificent AI Agent
🤖 Generated with Claude Code
PR Hygiene ·
cdb38c5dpp(packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs,packages/rs-dpp/src/identity/identity_public_key/mod.rs,packages/rs-dpp/src/identity/identity_public_key/security_level.rsand 3 more) — QuantumExplorer or shumkovrs-platform-wallet-ffi(packages/rs-platform-wallet-ffi/src/error.rs) — HashEngineering or ZocoLini or llbartekll or romchornyirs-platform-wallet(packages/rs-platform-wallet/src/error.rs,packages/rs-platform-wallet/src/lib.rs,packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rsand 24 more) — HashEngineering or ZocoLini or llbartekll or romchornyirust-sdk(packages/rs-sdk/src/platform/dashpay/contact_request.rs,packages/rs-sdk/src/platform/dashpay/mod.rs,packages/rs-sdk/src/platform/dpns_usernames/mod.rsand 1 more) — lklimek or shumkovWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.Summary by CodeRabbit