Skip to content

fix(dpp): bind unshielding_amount to sighash in client builders#3362

Open
QuantumExplorer wants to merge 3 commits intov3.1-devfrom
fix/sighash-mismatch-unshield-withdrawal
Open

fix(dpp): bind unshielding_amount to sighash in client builders#3362
QuantumExplorer wants to merge 3 commits intov3.1-devfrom
fix/sighash-mismatch-unshield-withdrawal

Conversation

@QuantumExplorer
Copy link
Member

@QuantumExplorer QuantumExplorer commented Mar 17, 2026

Issue Being Fixed

Security audit identified a critical sighash mismatch between client-side Orchard bundle signing and server-side proof verification for Unshield and ShieldedWithdrawal transitions.

Server (shielded_proof.rs): binds output_address || unshielding_amount to the sighash
Client (builders + FFI): only bound output_address — missing the unshielding_amount

This caused every Unshield and ShieldedWithdrawal transition built with rs-dpp or rs-sdk-ffi to fail proof verification on the platform. Integration tests passed because they construct bundles with the server-side sighash computation directly.

What was done

Appended unshielding_amount.to_le_bytes() to extra_sighash_data in all 4 client-side locations:

  • rs-dpp/src/shielded/builder/unshield.rs — DPP builder
  • rs-dpp/src/shielded/builder/shielded_withdrawal.rs — DPP builder
  • rs-sdk-ffi/src/shielded/crypto/bundle_build.rs — FFI unshield path
  • rs-sdk-ffi/src/shielded/crypto/bundle_build.rs — FFI withdrawal path

The required variable (unshield_amount + fee) equals value_balance which becomes unshielding_amount in the state transition — exactly what the server expects.

Test plan

  • All 9 builder tests pass (cargo test -p dpp --features shielded-client -- builder)
  • Both crates compile cleanly (cargo check -p dpp && cargo check -p rs-sdk-ffi)
  • End-to-end test: build an Unshield transition with the SDK and verify it passes platform validation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed shielded withdrawal and unshielding so cryptographic inputs now include the unshielding amount, aligning client-side signatures with server-side verification.
  • Documentation
    • Added a comprehensive security audit report detailing findings, risk levels, and recommended remediations for the shielded pool.

The server-side proof verification (shielded_proof.rs) binds both the
destination and unshielding_amount to the Orchard sighash, but the
client-side builders only bound the destination. This mismatch caused
every Unshield and ShieldedWithdrawal transition built with rs-dpp or
rs-sdk-ffi to fail proof verification on the platform.

Fixed in 4 locations:
- rs-dpp builder: unshield.rs, shielded_withdrawal.rs
- rs-sdk-ffi FFI: bundle_build.rs (unshield + withdrawal paths)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added this to the v3.1.0 milestone Mar 17, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Extra sighash input now includes the unshielding/withdrawal amount (little-endian) appended to existing output bytes in the shielded bundle builders; a security audit document for the shielded pool was also added.

Changes

Cohort / File(s) Summary
DPP Shielded Builder
packages/rs-dpp/src/shielded/builder/shielded_withdrawal.rs, packages/rs-dpp/src/shielded/builder/unshield.rs
Adjusted construction of extra_sighash_data: previously only output bytes, now `output_bytes
SDK FFI Bundle Building
packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs
Updated both dash_sdk_shielded_build_unshield_bundle and dash_sdk_shielded_build_withdrawal_bundle to append the unshielding/withdrawal amount (little-endian) to the address/script bytes used as extra_sighash_data; comments updated accordingly.
Security Audit
SECURITY_AUDIT_ZK_2026_03_17.md
Added a new security audit document describing critical/high/medium/low findings for shielded pool components, including the sighash mismatch and fee-related issues, with recommended actions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I stitched the bytes with careful paws,

appended little-ends without a pause.
Sighashes hum, proofs now sing,
Hop—securely bound in cryptic ring.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(dpp): bind unshielding_amount to sighash in client builders' directly and specifically describes the main change: binding the unshielding_amount to sighash computation in client-side builders to fix a critical sighash mismatch vulnerability identified by security audit.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sighash-mismatch-unshield-withdrawal
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs (1)

847-850: Consider centralizing extra_sighash_data packing.

This byte-layout logic is now duplicated across unshield/withdrawal paths; a small helper (e.g., fn pack_extra_sighash_data(prefix: &[u8], required: u64) -> Vec<u8>) would reduce future mismatch risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs` around lines 847 -
850, Extract the byte-packing logic that builds extra_sighash_data (currently:
output_address.to_bytes() then extend with required.to_le_bytes()) into a small
helper like fn pack_extra_sighash_data(prefix: &[u8], required: u64) -> Vec<u8>,
place it next to the related crypto helpers, and replace the inline construction
in bundle_build.rs (and the duplicate in the unshield/withdrawal code paths and
shielded_proof.rs if present) to call
pack_extra_sighash_data(&output_address.to_bytes(), required) so all callers use
the same implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs`:
- Around line 847-850: Extract the byte-packing logic that builds
extra_sighash_data (currently: output_address.to_bytes() then extend with
required.to_le_bytes()) into a small helper like fn
pack_extra_sighash_data(prefix: &[u8], required: u64) -> Vec<u8>, place it next
to the related crypto helpers, and replace the inline construction in
bundle_build.rs (and the duplicate in the unshield/withdrawal code paths and
shielded_proof.rs if present) to call
pack_extra_sighash_data(&output_address.to_bytes(), required) so all callers use
the same implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab341bda-5b65-4418-a835-e1f5460960da

📥 Commits

Reviewing files that changed from the base of the PR and between 95bdf2c and 09a2ad7.

📒 Files selected for processing (3)
  • packages/rs-dpp/src/shielded/builder/shielded_withdrawal.rs
  • packages/rs-dpp/src/shielded/builder/unshield.rs
  • packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs

@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.87%. Comparing base (2fb112c) to head (6414576).
⚠️ Report is 4 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...rs-dpp/src/shielded/builder/shielded_withdrawal.rs 50.00% 2 Missing ⚠️
packages/rs-dpp/src/shielded/builder/unshield.rs 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           v3.1-dev    #3362   +/-   ##
=========================================
  Coverage     75.87%   75.87%           
=========================================
  Files          2912     2912           
  Lines        283860   283934   +74     
=========================================
+ Hits         215375   215438   +63     
- Misses        68485    68496   +11     
Components Coverage Δ
dpp 65.74% <50.00%> (+0.03%) ⬆️
drive 81.64% <ø> (-0.01%) ⬇️
drive-abci 85.99% <ø> (ø)
sdk 31.25% <ø> (ø)
dapi-client 79.06% <ø> (ø)
platform-version ∅ <ø> (∅)
platform-value 58.46% <ø> (ø)
platform-wallet 60.40% <ø> (ø)
drive-proof-verifier 48.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

✅ DashSDKFFI.xcframework built for this PR.

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

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

Xcode manual integration:

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

Copy link
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Correct and well-targeted security fix. The PR consistently appends required.to_le_bytes() (= amount + fee = value_balance = state transition's unshielding_amount) to extra_sighash_data in all 4 client-side bundle builders, aligning them with the server-side verification. The codex-general blocking findings about the FFI two-step flow are false positives — the referenced code is unchanged by this PR, and the fix strictly improves a previously always-broken flow. Main gaps are test coverage and documentation.

Reviewed commit: 09a2ad7

🟡 3 suggestion(s) | 💬 2 nitpick(s)

4 additional finding(s) omitted (not in diff).

🤖 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-ffi/src/shielded/transitions/builders.rs`:
- [SUGGESTION] lines 136-194: FFI two-step flow doesn't expose computed `required` to callers
  The bundle builders (`build_unshield_bundle`, `build_withdrawal_bundle`) sign with `required = amount + fee`, but the returned `DashSDKOrchardBundleParams` struct doesn't include `value_balance`. The transition builders (`build_unshield`, `build_withdrawal`) and broadcast paths (`unshield_funds`, `withdraw_shielded`) take a separate `unshielding_amount` parameter that must equal `required` for verification to pass. A caller naturally passing the same net amount to both steps would get a sighash mismatch.

This is a pre-existing API design concern, not a regression from this PR (before the fix, the flow always failed). However, this PR makes it actionable — consider either exposing `value_balance` in the bundle params struct or adding a `debug_assert` in `try_from_bundle` that the passed amount matches the bundle's implicit value_balance.

In `packages/rs-dpp/src/shielded/builder/unshield.rs`:
- [SUGGESTION] lines 113-193: No happy-path test verifying extra_sighash_data construction
  Both `unshield.rs` and `shielded_withdrawal.rs` only test error paths (fee below minimum, insufficient funds). There is no test that constructs the bundle and verifies the `extra_sighash_data` includes `output_address || required.to_le_bytes()` matching the server expectation. A round-trip test (build → serialize → reconstruct_and_verify) would prevent regression of this exact class of mismatch.

In `packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs`:
- [SUGGESTION] lines 65-68: Doc comment omits ShieldedWithdrawal extra_sighash_data format
  The doc comment for `reconstruct_and_verify_bundle` documents the `extra_sighash_data` format for Shield, ShieldedTransfer, and Unshield but omits ShieldedWithdrawal. Since this exact omission pattern contributed to the original mismatch, completing the documentation reduces future regression risk.

Comment on lines +83 to +86
// Unshield extra_data = output_address || unshielding_amount (le bytes)
// Must match server-side sighash in shielded_proof.rs
let mut extra_sighash_data = output_address.to_bytes();
extra_sighash_data.extend_from_slice(&required.to_le_bytes());
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: Comment says 'unshielding_amount' but code binds required (amount + fee)

The comment // Unshield extra_data = output_address || unshielding_amount (le bytes) is semantically correct (the state transition field unshielding_amount equals required), but a reader unfamiliar with the invariant might think a different variable is being bound. Same pattern appears in shielded_withdrawal.rs:91-94 and both FFI locations in bundle_build.rs:847-850,1022-1025.

Suggested change
// Unshield extra_data = output_address || unshielding_amount (le bytes)
// Must match server-side sighash in shielded_proof.rs
let mut extra_sighash_data = output_address.to_bytes();
extra_sighash_data.extend_from_slice(&required.to_le_bytes());
// Unshield extra_data = output_address || value_balance (le bytes)
// value_balance = unshield_amount + fee, becomes v0.unshielding_amount in the state transition
let mut extra_sighash_data = output_address.to_bytes();
extra_sighash_data.extend_from_slice(&required.to_le_bytes());

source: ['claude-general', 'claude-ffi-engineer']

Clarify that `required` = value_balance = unshield_amount + fee,
which becomes v0.unshielding_amount in the state transition.

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
SECURITY_AUDIT_ZK_2026_03_17.md (2)

19-23: Consider noting that line references reflect pre-fix state.

The line numbers (e.g., unshield.rs:84, bundle_build.rs:848,1021) reference the code before this PR's fix is applied. Since this document is committed alongside the fix, readers may be confused when line numbers don't match. Consider either:

  1. Updating line numbers to reflect post-fix locations, or
  2. Adding a note that "Line references are from the original audit commit (95bdf2c) before the fix was applied."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY_AUDIT_ZK_2026_03_17.md` around lines 19 - 23, The document
references pre-fix line numbers for several files (e.g.,
rs-dpp/src/shielded/builder/unshield.rs,
rs-dpp/src/shielded/builder/shielded_withdrawal.rs,
rs-sdk-ffi/src/shielded/crypto/bundle_build.rs, and the server trait in
rs-drive-abci) which will mismatch after the PR; update the audit file to either
adjust the line numbers to their post-fix locations or add a clear note such as
"Line references are from the original audit commit (95bdf2c08) before the fix
was applied" so readers can map references correctly.

27-30: Add language identifier to fenced code block.

The code block on lines 27-30 lacks a language specifier. Since this is pseudocode/notation rather than actual code, consider using text or plaintext to satisfy linters and improve rendering consistency.

📝 Proposed fix
-```
+```text
 Client (signing):   sighash = SHA256(domain || commitment || output_address)
 Server (verifying): sighash = SHA256(domain || commitment || output_address || unshielding_amount_le_bytes)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @SECURITY_AUDIT_ZK_2026_03_17.md around lines 27 - 30, The fenced code block
containing the sighash pseudocode ("Client (signing): sighash = SHA256(domain
|| commitment || output_address)" and "Server (verifying): sighash =
SHA256(domain || commitment || output_address || unshielding_amount_le_bytes)")
is missing a language identifier; update that triple-backtick block to include a
language tag such as text or plaintext (e.g., ```text) so linters and renderers
treat it as plain text.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @SECURITY_AUDIT_ZK_2026_03_17.md:

  • Around line 19-23: The document references pre-fix line numbers for several
    files (e.g., rs-dpp/src/shielded/builder/unshield.rs,
    rs-dpp/src/shielded/builder/shielded_withdrawal.rs,
    rs-sdk-ffi/src/shielded/crypto/bundle_build.rs, and the server trait in
    rs-drive-abci) which will mismatch after the PR; update the audit file to either
    adjust the line numbers to their post-fix locations or add a clear note such as
    "Line references are from the original audit commit (95bdf2c) before the fix
    was applied" so readers can map references correctly.
  • Around line 27-30: The fenced code block containing the sighash pseudocode
    ("Client (signing): sighash = SHA256(domain || commitment || output_address)"
    and "Server (verifying): sighash = SHA256(domain || commitment || output_address
    || unshielding_amount_le_bytes)") is missing a language identifier; update that
    triple-backtick block to include a language tag such as text or plaintext (e.g.,

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30bf0d8b-d9ce-43cd-a37a-9dbfe0845a4e

📥 Commits

Reviewing files that changed from the base of the PR and between 09a2ad7 and 77d29c6.

📒 Files selected for processing (4)
  • SECURITY_AUDIT_ZK_2026_03_17.md
  • packages/rs-dpp/src/shielded/builder/shielded_withdrawal.rs
  • packages/rs-dpp/src/shielded/builder/unshield.rs
  • packages/rs-sdk-ffi/src/shielded/crypto/bundle_build.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-dpp/src/shielded/builder/shielded_withdrawal.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Trivial incremental: improved sighash comments to use 'value_balance' terminology per prior review feedback, and removed an accidentally committed security audit document. All changes are comment-only — no logic modifications.

Reviewed commit: 6414576

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants