Skip to content

fix(platform-wallet): retry an oversized InstantSend asset-lock transition with a ChainLock proof - #5460

Open
romchornyi wants to merge 13 commits into
v5.1-devfrom
fix/asset-lock-proof-size-chain-fallback
Open

romchornyi wants to merge 13 commits into
v5.1-devfrom
fix/asset-lock-proof-size-chain-fallback

Conversation

@romchornyi

@romchornyi romchornyi commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Basic explanation

What this does: A wallet pays for Platform actions (creating an identity, topping it up, moving Dash to the shielded pool, funding a Platform address) with an "asset lock" — a Core transaction — plus a proof that the lock exists. The fast proof (InstantSend) carries the whole funding transaction, so it grows with every coin spent; the slower proof (ChainLock) is a fixed ~40 bytes but needs the transaction to be in a chainlocked block. When the fast proof makes the transition too big for Platform's 20 KB limit, the wallet now waits for the ChainLock and sends the small proof instead.

Value: Wallets holding many small coins (masternode payouts, frequent small receipts) can move their balance to the shielded pool again. Before this, a shield of ~65+ coins failed right after Confirm, the funds sat in an asset lock, and Finish now replayed the same oversized proof forever (dashwallet-ios#1042). Identity registration/top-up, invitation claims and platform-address funding share the same fallback, but in practice they stay under 20 KB: an asset lock that Core relays and Platform accepts has at most 100 inputs (max_asset_lock_transaction_inputs), and those transitions are smaller than a shield.

Risks: Low. No consensus change: Drive's decoders keep their own size check, unchanged, and a drive-abci test pins that they agree with the shared one at every protocol version; DAPI keeps its wire behaviour. The SDK now refuses an oversized transition locally with the typed StateTransitionMaxSizeExceededError instead of sending it for DAPI to refuse with an untyped InvalidArgument. A transfer that needs the fallback completes a few minutes after Confirm instead of instantly.

Issue being fixed or feature implemented

Fixes dashpay/dashwallet-ios#1042, mirrored as #4460.

An 83-input asset lock produced a 23 KB ShieldFromAssetLock transition. DAPI refused it in validate_state_transition_bytes with an untyped InvalidArgument, so the wallet's IS→CL fallback (which only matched InvalidInstantAssetLockProofSignatureError) never fired, and every later resume rebuilt the same InstantSend proof.

What was done?

  • rs-dpp — state_transition::validate_state_transition_size(size_bytes, platform_version): the one definition of the max_state_transition_size check, returning StateTransitionMaxSizeExceededError.
  • rs-dapi — validate_state_transition_bytes uses it; the response is unchanged.
  • rs-drive-abci — the decode_raw_state_transitions generations keep their own size check, so replay of shipped protocol versions does not depend on client-facing code. A parity test runs every protocol version from 1 to LATEST_VERSION at max and max + 1 bytes and asserts that the decoder's size verdict and error match validate_state_transition_size.
  • rs-sdk — checked_broadcast_request builds the request and refuses an oversized transition before dispatch. Every sending path goes through it: broadcast_with_retries, broadcast_identity, transfer_document, vote. From<StateTransitionMaxSizeExceededError> for Error added next to the other per-error conversions.
  • rs-sdk asset_lock_submission (where feat(sdk): move the DashPay invitation link codec and claim into rs-sdk #5471 moved the asset-lock matchers) — needs_chain_lock_proof(error, proof): retry with a ChainLock proof for an InstantSend proof when Platform rejected the IS lock, or when the size refusal would pass with a ChainLock proof for the same outpoint in its place (the proof lengths are measured; content that is too large on its own is not retried after a pointless ChainLock wait). as_state_transition_max_size_exceeded added, and re-exported from platform_wallet::error with the other matchers. The invitation claim seam in dashpay/invitation.rs uses it, renamed submit_claim_with_chain_lock_fallback.
  • platform-wallet — shielded funding, identity registration and top-up, and platform-address funding use needs_chain_lock_proof and now share the same ChainLock-proof bookkeeping, which only shielded funding had in full:
    • upgrade_to_persisted_chain_lock_proof attaches the upgraded proof to the tracked row (ChainLocked) before the resubmit, so a failed resubmit does not leave a refused InstantSend proof for every resume to rebuild (identity registration and top-up skipped this);
    • resolve_chain_proof_after_is_timeout (no longer shielded-only) does the same after an IS-lock timeout before resuming;
    • invalidate_rejected_chain_proof (no longer shielded-only) takes a ChainLock proof Platform rejects for its height off the row.
  • validate_or_upgrade_proof deliberately unchanged — upgrade_to_chain_lock_proof already builds the proof for a record that a ChainLock evicted from memory (regression test added).

How Has This Been Tested?

  • rs-dpp state_transition/mod.rs: validate_state_transition_size boundary at every protocol version (the limit passes, one byte more is refused with both numbers).
  • rs-drive-abci decode_raw_state_transitions: decoder size verdict and error match validate_state_transition_size at every protocol version.
  • rs-sdk broadcast_request.rs / broadcast.rs: a transition at the limit builds; one byte over, or past 100 KB, gets the typed error; an oversized transition is never dispatched.
  • rs-sdk asset_lock_submission.rs: retry for an oversized IS transition that fits with a chain proof, measured with the lock's own outpoint (the boundary byte is pinned); no retry when it would still be too large, for a chain proof (also on a stale-islock error), or for unrelated errors. dashpay/invitation.rs claim seam: fallback submitted on a size refusal, retry signal without a fallback, no resubmit when the fallback would not fit.
  • platform-wallet proof.rs: upgrade_to_persisted_chain_lock_proof leaves the row ChainLocked with the upgraded proof; evicted_record: chain proof built after a ChainLock evicts the record (runs with and without keep-finalized-transactions). No test drives a size refusal through each of the four funding call sites; they share the classifier and the persisting upgrade, which are tested.
  • platform-wallet rejected-proof invalidation tests now also run without the shielded feature.
  • cargo test -p platform-wallet 1573 (--all-features) / 1329 (default), -p dash-sdk 413, -p platform-wallet-ffi builds; clippy clean on the touched crates.
  • Testnet, dashwallet-ios simulator build against this SDK, wallet of 160 coins of 0.0125 tDASH:
    • new shield of ~80 coins: local size refusal (23,202 > 20,480) → ChainLock → three CL-height-too-low retries → succeeded 87 s after Confirm;
    • lock left stuck while the app was stopped: Finish now → chain proof from the record height → succeeded in ~9 s.

Breaking Changes

None. Public API is only added to. Behaviour change: an oversized transition now fails in the SDK with Error::Protocol(ConsensusError(StateTransitionMaxSizeExceededError)) before being sent, instead of DAPI's untyped InvalidArgument.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added state-transition size validation before broadcast, with clear errors for oversized transactions.
    • Asset-lock submissions now retry with a ChainLock proof when an InstantSend proof is stale, invalid, or exceeds size limits.
    • Identity registration, funding, transfers, votes, and invitations use the validated submission flow.
  • Bug Fixes

    • Prevented oversized transitions from being dispatched.
    • Improved recovery and persistence during ChainLock promotion, including transient record lookup failures.
    • Added clearer reporting for size-limit and unusable-proof failures.

PR Hygiene · 4d413cf

  • Bots — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it — /skip-bots proceeds without the ones not yet reported
  • Self-review — address shumkov requested changes, then post /self-reviewed
  • Build failed
  • Approvals
    • rust-dapi (packages/rs-dapi/src/services/platform_service/broadcast_state_transition.rs) — QuantumExplorer or lklimek or shumkov
    • dpp (packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs, packages/rs-dpp/src/state_transition/mod.rs) — QuantumExplorer or shumkov
    • rs-drive-abci (packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/mod.rs) — QuantumExplorer or shumkov
    • rs-platform-wallet (packages/rs-platform-wallet/src/error.rs, packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs and 6 more) — HashEngineering or ZocoLini or llbartekll
    • rust-sdk (packages/rs-sdk/src/error.rs, packages/rs-sdk/src/platform/dashpay/invitation.rs, packages/rs-sdk/src/platform/transition/asset_lock_submission.rs and 5 more) — lklimek or shumkov

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

…ition with a ChainLock proof

An InstantSend asset-lock proof embeds the whole funding transaction and
its InstantLock, ~184 B per funding input, while a ChainLock proof is
~40 B. A lock funded by many small coins (dashwallet-ios#1042: 83 inputs)
made the shield transition exceed max_state_transition_size; DAPI refused
it with an untyped InvalidArgument, so no fallback fired and Finish now
replayed the same proof forever.

- rs-dpp: validate_state_transition_size, the one definition of the size
  check, now used by rs-dapi and rs-sdk.
- rs-sdk: checked_broadcast_request refuses an oversized transition
  before dispatch with the typed StateTransitionMaxSizeExceededError, on
  every path that sends one.
- platform-wallet: needs_chain_lock_proof retries IS→CL when Platform
  rejects the IS lock or when a ChainLock proof would bring the
  transition under the limit; used by shielded funding, identity
  registration and top-up, invitation claims and platform-address
  funding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

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

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d39132d7-a211-4652-a16c-80e555a89ac6
📥 Commits

Reviewing files that changed from the base of the PR and between 2076ed7 and e09f238.

📒 Files selected for processing: 11
  • packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/invitation.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/registration.rs
  • packages/rs-platform-wallet/src/wallet/persister.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs
  • packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
  • packages/rs-sdk/src/platform/dashpay/invitation.rs
  • packages/rs-sdk/src/platform/transition/asset_lock_submission.rs
🚧 Files skipped from review as they are similar to previous changes: 1
  • packages/rs-sdk/src/platform/dashpay/invitation.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

State-transition broadcasts now validate serialized size against the platform limit. Asset-lock submission paths can retry eligible InstantSend proofs with ChainLock proofs when the transition exceeds that limit.

Changes: Asset-lock transition size handling

Layer / File(s) Summary
Define size validation and checked requests
packages/rs-dpp/src/state_transition/mod.rs, packages/rs-sdk/src/error.rs, packages/rs-sdk/src/platform/transition/broadcast_request.rs, packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs
A shared validator and checked request builder return a typed error when serialized transition size exceeds the platform limit. Tests cover size boundaries and InstantSend and ChainLock proof sizes.
Apply size checks at broadcast entry points
packages/rs-dapi/src/services/platform_service/broadcast_state_transition.rs, packages/rs-sdk/src/platform/transition/broadcast.rs, packages/rs-sdk/src/platform/transition/broadcast_identity.rs, packages/rs-sdk/src/platform/transition/transfer_document.rs, packages/rs-sdk/src/platform/transition/vote.rs
DAPI uses the shared validator. SDK broadcast paths use checked request construction with the platform version before dispatch.
Classify InstantSend proof retry eligibility
packages/rs-sdk/src/platform/transition/asset_lock_submission.rs, packages/rs-platform-wallet/src/error.rs, packages/rs-sdk/src/platform/dashpay/invitation.rs
A shared predicate selects InstantSend proofs for ChainLock retry when the proof is invalid or its replacement brings an oversized transition within the limit.
Persist and use ChainLock fallback in wallet flows
packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs, packages/rs-platform-wallet/src/wallet/identity/network/registration.rs, packages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs, packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs, packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs, packages/rs-platform-wallet/src/wallet/persister.rs, packages/rs-platform-wallet/src/wallet/identity/network/invitation.rs
Wallet flows use shared submission and timeout recovery. ChainLock upgrades persist tracked state before resubmission. Tests cover persistence failures, record eviction, bounded reconciliation, and resumability.

Priority: High

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant WalletAssetLockFlow
  participant needs_chain_lock_proof
  participant ChainLockProofUpgrade
  participant checked_broadcast_request
  participant PlatformDAPI
  WalletAssetLockFlow->>needs_chain_lock_proof: classify the submission error and proof
  needs_chain_lock_proof-->>WalletAssetLockFlow: select ChainLock retry when eligible
  WalletAssetLockFlow->>ChainLockProofUpgrade: upgrade and persist the funding proof
  ChainLockProofUpgrade-->>WalletAssetLockFlow: return ChainLock proof
  WalletAssetLockFlow->>checked_broadcast_request: build a checked request
  checked_broadcast_request->>PlatformDAPI: dispatch the request when within the size limit
Loading

Possibly related issues

  • dashpay/platform#5513 — The changes add a size fallback for asset-lock transitions but do not reject over-cap input counts before broadcast.
  • dashpay/platform#5518 — The changes add the oversized InstantSend-to-ChainLock wait but do not release shield_guard during that wait.
  • dashpay/dashwallet-ios#1231 — The changes centralize asset-lock submission but do not add a wallet-removal barrier.
  • dashpay/platform#5516 — The changes centralize persistence and submission but do not add wallet-removal exclusion.

Suggested reviewers: quantumexplorer

Merge Risk: Minimal · up to e09f2

Oversized asset-lock transitions are now rejected locally before dispatch and retried with a ChainLock proof when that proof fits. No concrete merge-blocking defect was identified. One known limitation remains: a fresh oversized shield can hold the shield guard while waiting for a ChainLock, and the author tracks this separately in #5518.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 79.37% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 126 functions across 21 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: retrying oversized InstantSend asset-lock transitions with a ChainLock proof.
Linked Issues check Passed Issue [#1042] requires a client-side recovery path for an oversized asset-lock transition. The PR adds typed local size validation and retries an oversized InstantSend-funded transition with a measure…
Out of Scope Changes check Passed The changes remain connected to [#1042]. Centralized size validation in rs-dpp, DAPI integration, SDK pre-dispatch checks, and shared ChainLock fallback logic support the recovery path. Related regi…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Linked Issues check

Explanation

Issue [#1042] requires a client-side recovery path for an oversized asset-lock transition. The PR adds typed local size validation and retries an oversized InstantSend-funded transition with a measured ChainLock proof when the replacement fits. The rs-dpp regression test covers the 83-input case, and SDK and wallet tests cover size boundaries, fallback, persistence, resume, and rejected-proof handling. This alternative fixes the reported failure without requiring input splitting or a warning.

Full details: Out of Scope Changes check

Explanation

The changes remain connected to [#1042]. Centralized size validation in rs-dpp, DAPI integration, SDK pre-dispatch checks, and shared ChainLock fallback logic support the recovery path. Related registration, top-up, invitation-claim, and platform-address flows reuse the same asset-lock behavior. The reported tests and documentation support these changes. No unrelated Drive or consensus change is shown.

✨ Finishing Touches: 💡 1
📝 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai ✓ · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 15, 2026
@thepastaclaw

thepastaclaw commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 1 blocking finding(s) (commit 4d413cf)

@github-actions github-actions Bot added this to the v5.0.0 milestone Oct 15, 2026

@thepastaclaw thepastaclaw left a comment

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.

Final validation — Phase 1 + Phase 2

Verified the supplied findings against head 5e35308; no actionable in-scope findings remain. The suggested proof-variant guard addresses a synthetic error/proof combination, while the adjacent observations do not establish regressions requiring changes here. Validation was static only: the supplied CI snapshot reports successful Rust workspace tests, a skipped dedicated Rust wallet job, and pending Swift validation and several JavaScript suites.

Review provenance

Source: reviewer 1: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 2: gemini-3.8-flash-high (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: normal by gpt-6.1-sol (effort low) — The diff adds a shared size check and extends existing proof-fallback paths across several funding operations, but does not substantially redesign funds movement, proof verification, or consensus rules, so it falls below the critical threshold.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 99% left, 5h 98% left
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort high); agent phase2-reviewer

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Oct 15, 2026
@romchornyi

Copy link
Copy Markdown
Contributor Author

/self-reviewed

@romchornyi
romchornyi changed the base branch from v5.0-dev to v5.1-dev October 19, 2026 08:43
@github-actions github-actions Bot modified the milestones: v5.0.0, v5.1.0 Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 19, 2026

@thepastaclaw thepastaclaw left a comment

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.

Re-review — Final validation — Phase 1 + Phase 2

The PR correctly adds client-side transition-size validation and ChainLock fallback handling. I found no blocking defect: the reported 100 KB serializer-order issue does not apply because the derive macro currently ignores the second serialization attribute, while three non-blocking API and consistency issues remain.

🟡 3 suggestion(s)

2 finding(s) not shown inline (the lines are not part of this PR's diff)

🟡 Suggestion: Reuse the shared size validator in the authoritative Drive decoders
packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v0/mod.rs:52-64

The PR describes validate_state_transition_size as the single definition of the maximum-size check, but both authoritative Drive decoder generations still duplicate the length comparison and StateTransitionMaxSizeExceededError construction. The current behavior is equivalent, but future changes to the boundary or error construction could make DAPI/SDK preflight disagree with consensus processing. Have the v0 and v1 decoders call the DPP helper and adapt its returned error into their existing ConsensusError container while preserving each generation's current wrapper behavior.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor)

🟡 Suggestion: Do not expose an unchecked request as ready for broadcast
packages/rs-sdk/src/platform/transition/broadcast_request.rs:49-61

The public BroadcastRequestForStateTransition::broadcast_request_for_state_transition method still serializes an oversized transition without applying the new typed size check. A caller can obtain that request and execute it through the public DAPI request API, receiving DAPI's untyped InvalidArgument instead of the SDK's StateTransitionMaxSizeExceededError; only the internal high-level send paths use checked_broadcast_request. Either expose a public checked request-building API that receives the active PlatformVersion, or make the unchecked serializer explicitly internal/serialization-only and retain a separate raw reconstruction path for proof waiting.

source: gpt-6.1-sol (phase2-reviewer: architecture-layering)

Review provenance

Source: reviewer 1: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 2: gemini-3.8-flash-high (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: critical by gpt-6.1-sol (effort low) — This is a large, cross-cutting change to asset-lock proof selection and wallet funding flows, directly affecting funds movement and cryptographic InstantSend/ChainLock proof handling, including platform-versioned transition-size validation.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 71% left, 5h 100% left
  • 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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-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/asset_lock/orchestration.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs:466-469: Use the actual outpoint when estimating ChainLock proof size
  The fallback-size estimate uses `OutPoint::null()` even though the replacement ChainLock proof will use the same outpoint as the InstantSend proof. With bincode's varint encoding, the null outpoint's `vout` is `u32::MAX` and takes five bytes, whereas normal asset-lock outputs such as index zero take one byte. This can overestimate the replacement proof by up to four bytes and cause a transition that would fit with the actual ChainLock proof to be rejected without attempting the fallback. Preserve the known outpoint while keeping the maximum height conservative.

In `packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v0/mod.rs:52-64: Reuse the shared size validator in the authoritative Drive decoders
  The PR describes `validate_state_transition_size` as the single definition of the maximum-size check, but both authoritative Drive decoder generations still duplicate the length comparison and `StateTransitionMaxSizeExceededError` construction. The current behavior is equivalent, but future changes to the boundary or error construction could make DAPI/SDK preflight disagree with consensus processing. Have the v0 and v1 decoders call the DPP helper and adapt its returned error into their existing `ConsensusError` container while preserving each generation's current wrapper behavior.

In `packages/rs-sdk/src/platform/transition/broadcast_request.rs`:
- [SUGGESTION] packages/rs-sdk/src/platform/transition/broadcast_request.rs:49-61: Do not expose an unchecked request as ready for broadcast
  The public `BroadcastRequestForStateTransition::broadcast_request_for_state_transition` method still serializes an oversized transition without applying the new typed size check. A caller can obtain that request and execute it through the public DAPI request API, receiving DAPI's untyped `InvalidArgument` instead of the SDK's `StateTransitionMaxSizeExceededError`; only the internal high-level send paths use `checked_broadcast_request`. Either expose a public checked request-building API that receives the active `PlatformVersion`, or make the unchecked serializer explicitly internal/serialization-only and retain a separate raw reconstruction path for proof waiting.
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.

  • The serialization derive macro ignores subsequent platform_serialize attributes — The derive implementation uses .find(...) and parses only the first platform_serialize attribute. StateTransition has separate unversioned and limit = 100000 attributes, so the current macro expansion ignores the limit. This is a pre-existing derive-macro defect rather than a regression introduced by this PR, but the new constant and test make the discrepancy relevant to future maintenance.
    • Follow-up: Track a separate fix in rs-platform-serialization-derive to parse and combine all matching platform_serialize attributes.

Comment thread packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai review

No review for 25106b56 yet, so PR Hygiene is asking once. If nothing arrives, the requirement is dropped for this commit and the pull request is labelled bot-review-skipped.

@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 20, 2026
@github-actions github-actions Bot added bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 22, 2026
@romchornyi

Copy link
Copy Markdown
Contributor Author

/self-reviewed

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Ready for review — dpp: QuantumExplorer or shumkov · rs-drive-abci: QuantumExplorer or shumkov.
Full checklist in the description.

@github-actions github-actions Bot added ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human. and removed waiting-self-review Waiting for the author to post /self-reviewed labels Oct 22, 2026
@github-actions
github-actions Bot requested a review from llbartekll October 22, 2026 14:33
lklimek
lklimek previously approved these changes Oct 22, 2026
llbartekll
llbartekll previously approved these changes Oct 23, 2026

@llbartekll llbartekll left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-approving on f7dcf5c. The rs-platform-wallet and rs-sdk changes are identical to the e09f238 I approved. Since then the PR moved the Drive decoder size check (decode_raw_state_transitions v0/v1) onto validate_state_transition_size and merged v5.1-dev. Neither touches the wallet. The in-place change looks consensus-neutral to me: same len > max_state_transition_size comparison, same two numbers, and From<StateTransitionMaxSizeExceededError> for ConsensusError maps to the same BasicError variant. The v0 merge resolution only keeps the StateTransitionNotActiveError imports from #5337. Drive/DPP sign-off stays with their code owners.

@shumkov shumkov left a comment

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.

I reviewed the dpp and drive-abci parts.

The decoder refactor doesn't change consensus today. I checked it: the comparison is still len > system_limits.max_state_transition_size, the error is still StateTransitionMaxSizeExceededError::new(len, max), and the From impl wraps it in exactly BasicError::StateTransitionMaxSizeExceededError. I'd still like it reverted; details inline.

Other notes:

  • AssetLockProof::serialized_len_in_state_transition is fine. Its config matches what #[platform_serialize] emits for StateTransition, and the top-up parity test catches any drift.
  • PR description: "In-place changes to shipped generations" says the helper is tested at versions 1, 13 and 14, but the test iterates 1..=LATEST_VERSION. If the decoder edits are reverted, the section goes away anyway.
  • Merge order: this PR and #5476 conflict in rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs and wallet/shielded/fund_from_asset_lock.rs. #5476 calls itself a follow-up, so I'd merge this one first and rebase #5476.

),
),
);
if let Err(error) = validate_state_transition_size(

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.

Please revert the decoder edits in v0 and v1 so both generations are byte-identical to the base again.

This isn't a consensus bug: the output is the same today. The problem is the shape. v0 (PV1–13, mainnet) and v1 (PV14, active on testnet, plus PV15) used to own their size check. Now replay of every shipped protocol version goes through dpp::state_transition::validate_state_transition_size, an unversioned pub fn whose doc invites the SDK and DAPI to call it before sending. If someone edits it for a client reason, every shipped protocol version changes with it, and only a "frozen" doc comment stands in the way. Your 2026-10-20 reply made exactly this argument, and I agree with it.

To keep the two in step without coupling them, add a parity test in drive-abci instead. For every protocol version in 1..=LATEST_VERSION, run decode_raw_state_transitions on inputs of max and max + 1 bytes and assert that its size verdict and error match validate_state_transition_size. That leaves one tested definition of "too large" and keeps replay independent of client code. Then drop the "Consensus depends on this" paragraph from the helper's doc, and the In-place section from the description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted in cb58c71: v0 and v1 are byte-identical to the base again. The parity test is should_agree_with_the_client_size_check_at_every_protocol_version in decode_raw_state_transitions/mod.rs: for every protocol version in 1..=LATEST_VERSION it decodes max and max + 1 bytes through the dispatcher and asserts the size verdict and error equal validate_state_transition_size's. Checked that it catches a drift (a temporary >= in v1 fails it at PV14, 20480 bytes). The "In-place" section is gone from the description. Agreed on the shape; I should have kept my 2026-10-20 position.

),
),
);
if let Err(error) = validate_state_transition_size(

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.

Same as v0: please revert this one too, since PV14 is active on testnet. The new PV14 boundary test can stay; it also passes against the original code and gives v1 the coverage v0 already had.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted in cb58c71. I dropped the PV14 boundary test as well, so v1 is fully back to the base: the parity test now covers the exact-limit pass and the one-byte-over refusal, with both numbers, at PV14 and every other version.

/// `max_state_transition_size`: a transition of `size_bytes` is refused when
/// it is larger, with the error Drive reports for one.
///
/// Consensus depends on this: Drive refuses oversized transitions through it

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.

Once the decoders go back to their own check, this paragraph no longer holds, so please remove it. Please also drop dashwallet-ios#1042 from this doc: tracker references belong in the PR or the commit, not in a DPP doc comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in cb58c71: the consensus paragraph and the tracker reference are removed. The doc now says the decoders keep their own copy and names the drive-abci parity test that keeps them in step.

}

// ---------------------------------------------------------------------
// Asset-lock proof size (dashwallet-ios#1042)

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.

Nit: please keep tracker references and incident history out of the code. That covers dashwallet-ios#1042 here, ISSUE_1042_INPUTS, the ..._issue_1042_... test names and "A wallet fed by many small payouts…". Name them by behaviour instead, e.g. MANY_INPUT_LOCK_INPUTS and should_overflow_max_size_with_a_many_input_instant_proof. The history belongs in the PR description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in cb58c71, across the PR, not only here: no tracker references or incident history left in the code (dpp, drive-abci, rs-sdk, platform-wallet comments).

}

#[tokio::test]
async fn should_exceed_max_size_with_the_issue_1042_instant_proof() {

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.

This test checks the data and today's limit, not DPP code: a per-input cost of 180–190 B, and an 83-input lock being over 20,480 B. If a later protocol version raises max_state_transition_size, the test fails with a message about the wallet's fallback, which DPP doesn't own. The invariant DPP does own, that a ChainLock proof's size doesn't depend on the input count, is already covered by the next test. I'd drop this one, or move the overflow premise into the wallet/SDK tests. Low priority.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped in cb58c71. With the overflow test gone, the 83-input chain-proof test only re-checked what seed_pool_batch_fits_max_state_transition_size already checks (a larger, 6-action shield with a ChainLock proof fits), so I dropped it as well: signing_tests.rs is back to the base.

@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: address shumkov requested changes, then post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human. labels Oct 23, 2026
…ty by test

Revert decode_raw_state_transitions v0 and v1 to the base: replay of
shipped protocol versions must not depend on the unversioned,
client-facing validate_state_transition_size. A drive-abci test now runs
every protocol version at max and max + 1 bytes and asserts the decoder's
size verdict and error match the helper.

Drop the "consensus depends on this" paragraph from the helper doc, the
tracker references from code comments, and the DPP test that pinned the
per-input cost of an InstantSend proof against today's limit (a premise
DPP does not own; the chain-proof fit is covered by the seeding test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@romchornyi
romchornyi dismissed stale reviews from llbartekll and lklimek via cb58c71 October 23, 2026 15:17
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 23, 2026

@thepastaclaw thepastaclaw left a comment

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.

Re-review — Final validation — Phase 1 + Phase 2

The size validation and InstantSend-to-ChainLock fallback are generally consistent, but rejected ChainLock proofs can be demoted into a recovery state that cannot reconstruct a replacement proof when the finalized transaction record has been evicted. This leaves automatic wallet catch-up unable to complete the asset lock, while a retained record can cause the same rejected height to be retried. The shared fallback also still lacks a test that reaches an actual funding call site; Rust workspace and suite checks were still in progress in the supplied CI snapshot.

🔴 1 blocking | 🟡 1 suggestion(s)

1 finding(s) not shown inline (the lines are not part of this PR's diff)

🔴 Blocking: Do not demote a rejected ChainLock proof into a record-only recovery path
packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:603-616

After a InvalidAssetLockProofTransactionHeightError, this code probes wait_for_proof and demotes the row to Broadcast with no proof when the record is missing or only reproduces the rejected ChainLock proof. The (Broadcast, None) resume path re-broadcasts and calls wait_for_proof, but that path only reads the in-memory/persisted transaction record; it does not invoke the mined-height locator used by upgrade_to_chain_lock_proof/wait_for_chain_lock. With the default keep-finalized-transactions feature disabled and a persistence backend returning Ok(None), a transaction can already be ChainLocked while its record has been evicted, leaving automatic catch-up unable to construct any proof and repeatedly timing out. If the record is retained, the same rejected ChainLock height can be read back and submitted again. Preserve a usable replacement only after locating a different height, or make this post-rejection recovery force the SPV-verified mined-height locator path.

source: gpt-6.1-sol (phase2-reviewer: general)

1 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: critical by gpt-6.1-sol (effort low) — This is a large, intricate change affecting funds movement and asset-lock proof handling across wallet, SDK, DPP, DAPI, and Drive state-transition decoding, including peer-facing deserialization and protocol-versioned behavior.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-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/asset_lock/sync/tracking.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:603-616: Do not demote a rejected ChainLock proof into a record-only recovery path
  After a `InvalidAssetLockProofTransactionHeightError`, this code probes `wait_for_proof` and demotes the row to `Broadcast` with no proof when the record is missing or only reproduces the rejected ChainLock proof. The `(Broadcast, None)` resume path re-broadcasts and calls `wait_for_proof`, but that path only reads the in-memory/persisted transaction record; it does not invoke the mined-height locator used by `upgrade_to_chain_lock_proof`/`wait_for_chain_lock`. With the default `keep-finalized-transactions` feature disabled and a persistence backend returning `Ok(None)`, a transaction can already be ChainLocked while its record has been evicted, leaving automatic catch-up unable to construct any proof and repeatedly timing out. If the record is retained, the same rejected ChainLock height can be read back and submitted again. Preserve a usable replacement only after locating a different height, or make this post-rejection recovery force the SPV-verified mined-height locator path.

In `packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:512-534: Exercise a real size refusal through the wallet funding fallback
  (existing thread: https://github.com/dashpay/platform/pull/5460#discussion_r4338186643)
  The shared `submit_with_chain_lock_fallback` tests now verify persistence and resumability, but they inject `oversized()` directly into a scripted closure. No test reaches this shielded funding call site with an actually oversized InstantSend transition, so a regression in the closure wiring, proof forwarding, or ChainLock rebuild could leave the production path broken while the shared-helper tests remain green. Add a deterministic wallet-level test around this funding closure, using a mocked submit/build boundary if constructing a real Halo 2 proof is too expensive.
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.

  • Separate the ChainLock wait from the shield guard — A fresh oversized shield can hold the shield guard while waiting indefinitely for ChainLock/SPV progress, blocking other shield operations. The existing InstantSend-timeout path has the same lifecycle behavior, and refactoring the guard scope requires coordinating key binding, removal, and both finality paths rather than changing only this branch.
    • Follow-up: Continue the tracked guard-scope refactor in #5518.

… record kept

A ChainLock proof Platform refuses for its height is taken off the row,
which drops to Broadcast with no proof. With the funding record evicted
and a persister that keeps none, the existing-lock resume's record-only
wait expires into the IS-timeout outcome, and the fallback rebuilds the
proof through the SPV-verified mined-height lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@romchornyi

Copy link
Copy Markdown
Contributor Author

@thepastaclaw on the 🔴 "Do not demote a rejected ChainLock proof into a record-only recovery path" (tracking.rs invalidate_rejected_chain_proof). I checked both cases. I'm keeping the demotion and added a test for the case you describe (4d413cf).

Evicted record, persister answers Ok(None). The recovery after the demotion does not stop at the record-only wait. Consuming the lock means resuming it as FromExistingAssetLock, which calls resume_asset_lock with the 300 s InstantSend bound. On the (Broadcast, None) arm that bounded wait_for_proof comes back as FinalityTimeout. resolve_funding_with_is_timeout_fallback turns that into IsTimeout, and every funding flow then calls resolve_chain_proof_after_is_timeout. That goes through upgrade_to_chain_lock_proof → wait_for_chain_lock, which uses the SPV-verified mined-height locator when there is no record. The new test, should_rebuild_a_cleared_proof_through_the_lookup_when_no_record_is_kept, covers exactly that state: no record in memory, a persister that keeps none, the row cleared by the invalidator. The lookup rebuilds the proof at the height it verifies, and the row ends ChainLocked with that proof. cleared_row_completes_the_next_resume_through_the_lookup already covered the same path with a record that supports no proof. What is true: the launch catch-up (asset_lock_manager_catch_up_blocking, a bare resume_asset_lock) does not build a proof for such a row. It never consumes a lock, though. It leaves the row for the next consuming resume, and every (Broadcast, None) row whose record is gone behaves like that already, whether or not it went through this path.

Retained record. Here the record is the only height evidence there is. wait_for_proof serves it only after record_chain_proof_height has checked it: the wallet's ChainLock covers it, and the SPV header at that height is still the block the record names. A reorg changes the record, and with it the next proof. If the header still holds, the locator would give the same height. So "a different height" is not available here. A refusal at a height our SPV chain verifies means Platform's Core view is behind, and resubmitting the same height later is the retry that can succeed. Keeping the row ChainLocked with the refused proof would do the same thing, but it would also block the lookup path above when the record really is wrong. That is why the row drops to (Broadcast, None).

Both cases are pre-existing behaviour of invalidate_rejected_chain_proof (its body is unchanged here). This PR only takes away its shielded gate, so identity registration, top-up and platform-address funding get the same cleanup.

On the 🟡 (a wallet-level size refusal through the shielded call site): same answer as in r4338186643. Reaching that closure with a really oversized transition needs a Halo 2 proof per run. Since e09f238 all four flows go through submit_with_chain_lock_fallback, and that is the code the scripted tests drive (size refusal → persisted ChainLock proof → resubmit; a failed resubmit leaves the lock resumable).

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

@romchornyi on it — I've queued this as job cj-20261024-122404-fafd and will reply here or link a PR when there's something to look at.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

Thanks for checking both paths and adding the regression test. This is follow-up to an existing review finding; reviewsys owns review disposition and will answer/re-review it, so I’m leaving this with reviewsys rather than taking further action here.

@thepastaclaw thepastaclaw left a comment •

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.

Re-review — Final validation — Phase 1 + Phase 2

Updated after discussion: 1 finding(s) of this review were settled in the inline threads; 1 blocking finding(s) still stand.

  • ✅ Withdrawn: 🔴 Keep the oversized-transition fixture below the serializer cap

The change introduces a recovery gap when a rejected ChainLock proof is demoted to a proof-less Broadcast row: the public generic resume path still waits only on transaction records and does not use the mined-height locator, so an evicted record can remain unrecoverable there. One newly added SDK test also cannot reach the typed size validator because its fixture exceeds the StateTransition serializer's 100,000-byte cap first. CI otherwise has successful relevant jobs; the Rust workspace failure is in an unchanged query initializer, and one browser shard remains in progress.

🔴 2 1 blocking | 🟡 1 suggestion(s)

1 finding(s) not shown inline (the lines are not part of this PR's diff)

🔴 Blocking: Do not demote a rejected ChainLock proof into a record-only recovery path
packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:603-616

When this invalidator clears a rejected ChainLock proof, it stores the row as (Broadcast, None). The public generic resume_asset_lock path, including asset_lock_manager_resume and launch catch-up, enters the Broadcast arm and calls wait_for_proof, which only re-reads in-memory or persisted transaction records; it does not invoke the mined-height locator used by upgrade_to_chain_lock_proof. If keep-finalized-transactions has evicted the record and the persister returns Ok(None), that generic resume can only time out or report an unconfirmed broadcast. The new regression covers the specialized funding path through resolve_chain_proof_after_is_timeout, but not this public generic resume surface. Either make generic recovery use the locator after this demotion or retain a state that routes every resume through the locator-backed ChainLock recovery.

source: gpt-6.1-sol (phase2-reviewer: architecture-layering)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: muse-spark-1.3-contributor (agent: phase1-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: critical by gpt-6.1-sol (effort low) — This is a large cross-cutting change to asset-lock proof handling and wallet funding orchestration, directly affecting funds movement and cryptographic proof selection in asset_lock_submission::needs_chain_lock_proof and the platform-wallet asset-lock flows.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-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/asset_lock/sync/tracking.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/asset_lock/sync/tracking.rs:603-616: Do not demote a rejected ChainLock proof into a record-only recovery path
  When this invalidator clears a rejected ChainLock proof, it stores the row as `(Broadcast, None)`. The public generic `resume_asset_lock` path, including `asset_lock_manager_resume` and launch catch-up, enters the Broadcast arm and calls `wait_for_proof`, which only re-reads in-memory or persisted transaction records; it does not invoke the mined-height locator used by `upgrade_to_chain_lock_proof`. If `keep-finalized-transactions` has evicted the record and the persister returns `Ok(None)`, that generic resume can only time out or report an unconfirmed broadcast. The new regression covers the specialized funding path through `resolve_chain_proof_after_is_timeout`, but not this public generic resume surface. Either make generic recovery use the locator after this demotion or retain a state that routes every resume through the locator-backed ChainLock recovery.

In `packages/rs-sdk/src/platform/transition/broadcast_request.rs`:
- [BLOCKING → WITHDRAWN in discussion; do not fix] packages/rs-sdk/src/platform/transition/broadcast_request.rs:186-200: Keep the oversized-transition fixture below the serializer cap
  The `StateTransition` derive uses `#[platform_serialize(limit = 100000)]`, and `STATE_TRANSITION_MAX_ENCODED_BYTES` is also 100,000. This fixture puts 100,000 bytes into the signature and adds the enum and transition framing, so `serialize_to_bytes()` fails at the serialization limit inside `broadcast_request_for_state_transition` before `validate_state_transition_size` runs. Consequently `checked_broadcast_request` returns a serialization `MaxEncodedBytesReachedError`, while `is_size_exceeded` expects `StateTransitionMaxSizeExceededError`; the test will fail instead of verifying the intended typed size error. Use a fixture larger than the protocol limit but below 100,000 bytes, such as a signature sized to `STATE_TRANSITION_MAX_ENCODED_BYTES / 2`, and assert that the resulting length exceeds `max_size()`.

In `packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs:514-533: Exercise a real size refusal through the wallet funding fallback
  (existing thread: https://github.com/dashpay/platform/pull/5460#discussion_r4338186643)
  The shielded call site now passes its transition-building closure to `submit_with_chain_lock_fallback`, but the added tests invoke that shared helper with a scripted error rather than exercising this closure or constructing a real `ShieldFromAssetLock` transition. The shared helper tests prove that a typed size refusal reaches a ChainLock resubmission, yet they would remain green if this call site later passed the wrong proof into the closure or bypassed the fallback while constructing the shielded transition. Add a lightweight call-site-level regression, or otherwise assert that the shielded closure receives and submits the replacement ChainLock proof.
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.

  • Rust workspace CI failure is outside this change — The Rust workspace check fails while compiling the unchanged packages/rs-sdk/src/platform/identities_contract_keys_query.rs, whose QuerySettings-related initializers lack verified_protocol_version. The base branch contains the subsequent fix, and no base commit touches the files changed by this pull request.
    • Follow-up: Rebase onto the current v5.1-dev tip or fix the query initializer separately before relying on the workspace check.

Comment on lines +186 to +200
fn should_refuse_a_transition_past_the_encoded_bytes_budget_with_the_typed_size_error() {
let transition = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(
IdentityTopUpTransitionV0 {
signature: BinaryData::new(vec![0u8; STATE_TRANSITION_MAX_ENCODED_BYTES]),
..Default::default()
},
));

let error = checked_broadcast_request(&transition, PlatformVersion::latest())
.expect_err("an oversized transition is refused");

assert!(
is_size_exceeded(&error, |actual| actual
> STATE_TRANSITION_MAX_ENCODED_BYTES as u64),
"unexpected error: {error:?}"

@thepastaclaw thepastaclaw Oct 24, 2026 •

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.

✅ Withdrawn at 4d413cf2; see the replies below.

🔴 Blocking: Keep the oversized-transition fixture below the serializer cap

The StateTransition derive uses #[platform_serialize(limit = 100000)], and STATE_TRANSITION_MAX_ENCODED_BYTES is also 100,000. This fixture puts 100,000 bytes into the signature and adds the enum and transition framing, so serialize_to_bytes() fails at the serialization limit inside broadcast_request_for_state_transition before validate_state_transition_size runs. Consequently checked_broadcast_request returns a serialization MaxEncodedBytesReachedError, while is_size_exceeded expects StateTransitionMaxSizeExceededError; the test will fail instead of verifying the intended typed size error. Use a fixture larger than the protocol limit but below 100,000 bytes, such as a signature sized to STATE_TRANSITION_MAX_ENCODED_BYTES / 2, and assert that the resulting length exceeds max_size().

Suggested change
fn should_refuse_a_transition_past_the_encoded_bytes_budget_with_the_typed_size_error() {
let transition = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(
IdentityTopUpTransitionV0 {
signature: BinaryData::new(vec![0u8; STATE_TRANSITION_MAX_ENCODED_BYTES]),
..Default::default()
},
));
let error = checked_broadcast_request(&transition, PlatformVersion::latest())
.expect_err("an oversized transition is refused");
assert!(
is_size_exceeded(&error, |actual| actual
> STATE_TRANSITION_MAX_ENCODED_BYTES as u64),
"unexpected error: {error:?}"
/// A transition larger than the protocol budget but below the
/// StateTransition serializer cap still gets the typed protocol-size error.
#[test]
fn should_refuse_a_transition_above_the_protocol_budget_with_the_typed_size_error() {
let transition = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(
IdentityTopUpTransitionV0 {
signature: BinaryData::new(vec![0u8; STATE_TRANSITION_MAX_ENCODED_BYTES / 2]),
..Default::default()
},
));
let error = checked_broadcast_request(&transition, PlatformVersion::latest())
.expect_err("an oversized transition is refused");
assert!(
is_size_exceeded(&error, |actual| actual > max_size() as u64),
"unexpected error: {error:?}"
);
}

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The test passes, locally (cargo test -p dash-sdk --lib -- transition::broadcast_request: 3 passed) and in every CI run since it was added. #[platform_serialize(limit = 100000)] does not stop encoding. serialize_enum.rs builds the config with with_limit::<100000>(), but bincode 2 checks that limit only when decoding. encode_to_vec writes into a growing Vec and never compares against it, so serialize_to_bytes() returns the full ~100 KB and validate_state_transition_size refuses it with the typed error. That is the behaviour this test pins: a transition past STATE_TRANSITION_MAX_ENCODED_BYTES (an IS proof over a funding tx of ~500 inputs) still encodes, so it gets the same typed error with its real length. A fixture under 100 KB would no longer cover that case. The 21 KB case is should_refuse_an_oversized_transition_with_the_typed_size_error just above. Keeping the test as it is.

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.

I checked the derive and bincode implementation: with_limit::<100000>() is passed to the encoder, but encode_to_vec uses a growing VecWriter whose write implementation does not enforce that byte limit. The fixture therefore reaches validate_state_transition_size and can correctly exercise the typed error with an encoded length above 100,000 bytes; my original finding was incorrect.

Withdrawing this finding.

@romchornyi

Copy link
Copy Markdown
Contributor Author

@thepastaclaw on the repeated 🔴 "Do not demote a rejected ChainLock proof into a record-only recovery path": I'm keeping the code as it is. I agree that the generic resume_asset_lock (asset_lock_manager_resume, launch catch-up) does not consult the locator on the (Broadcast, None) arm. Here is why that does not block this PR:

  • The generic resume never consumes the lock. It reports a proof back to the host. Every path that spends the lock (identity registration and top-up, platform-address funding, shielded funding, all as FromExistingAssetLock) turns the bounded wait's FinalityTimeout into IsTimeout and rebuilds the proof through the locator. should_rebuild_a_cleared_proof_through_the_lookup_when_no_record_is_kept (4d413cf) pins that for exactly this state. A demoted row is never stranded: the next funding call completes it.
  • The gap is not new, and this PR does not widen it. Any (Broadcast, None) row whose funding record was evicted on a backend that keeps none gets the same record-only generic resume. That holds for a lock broadcast before an app restart too, whether or not a proof was ever refused. Before the invalidator, the same row kept a ChainLock proof Platform had refused, so every resume handed it back for another refusal. The demotion trades that replay for the recovery above.
  • Changing it means changing the generic resume for every (Broadcast, None) row, for example by racing the record wait against the locator-backed ChainLock wait. That is a behaviour change for all callers, with its own DAPI lookups, and it is out of scope for an asset-lock size fallback.

The 🔴 on broadcast_request.rs is answered in its thread (r4367264002): that test passes, because bincode 2 does not enforce the encode limit.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

@romchornyi on it — I've queued this as job cj-20261024-141603-02fb and will reply here or link a PR when there's something to look at.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

Thanks for the detailed clarification. I understand the generic-resume behavior and the scope rationale; no code change is needed from me on these points. reviewsys handles the review and will re-review any pushed changes.

This branch has not been deployed

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

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Shield from Asset Lock" fails with "State Transition exceeds maximum size of 20480 bytes" — funds locked, unable to complete shielded transfer

7 participants