Skip to content

fix(platform): derive shielded identity-create id from the padded bundle's published nullifiers#3843

Merged
QuantumExplorer merged 2 commits into
v3.1-devfrom
claude/trusting-wescoff-91010b
Jun 10, 2026
Merged

fix(platform): derive shielded identity-create id from the padded bundle's published nullifiers#3843
QuantumExplorer merged 2 commits into
v3.1-devfrom
claude/trusting-wescoff-91010b

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jun 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Two client-side shielded bugs found during the paloma E2E run of the full shielded transition matrix (types 15–20), both blocking real-world flows:

  1. Type-20 (IdentityCreateFromShieldedPool) was broken for single-note spends. The builder derived the new identity's id from the requested spends' nullifiers, but Orchard's BundleType::DEFAULT pads a single-spend bundle to the 2-action minimum — and the padding action's random dummy nullifier is published on the wire. Consensus re-derives the id over all published action nullifiers, so the builder's id (and the id bound into the Orchard extra_sighash_data) diverged from the consensus derivation whenever padding occurred. The builder's own post-proving id-consistency check caught the mismatch and failed the build — but only after paying the full Halo 2 proving cost, every time, making every 1-note identity creation unusable.

  2. Unshield was unusable on devnet/regtest. PlatformWallet::shielded_unshield_to compared the parsed output address's network against the SDK network with raw Network equality — but bech32m parsing can only ever yield Mainnet or Testnet (testnet/devnet/regtest all share the tdash HRP per DIP-0018), so a Devnet wallet rejected every address, including its own.

What was done?

Commit 1 — type-20 id derivation from the padded bundle (rs-dpp):

  • New build_spend_bundle_with / prove_and_sign_bundle_with variants that take a closure FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError> computing the extra sighash data from the built bundle's published action nullifiers — extracted after Builder::build fixes the action set (padding included) and before the sighash is bound / proof created. The existing fixed-data functions delegate to the _with variants.
  • build_identity_create_from_shielded_pool_transition now derives the identity id inside that hook (identity_id_from_nullifiers(published_nullifiers)) and binds the extra sighash data there, so the bound id always equals the consensus re-derivation. The post-build id-consistency assert is kept as a true invariant guard.
  • Doc comment corrected (the id is not known before the bundle is built when padding occurs) and a stale duplicated comment paragraph deduplicated in drive-abci's state.rs type-20 arm (comment-only).

Commit 2 — unshield address network check by HRP (rs-platform-wallet):

  • shielded_unshield_to now compares PlatformAddress::hrp_for_network(addr_network) against hrp_for_network(self.sdk.network) instead of raw networks, matching what the bech32m encoding can actually represent.

How Has This Been Tested?

New regression tests, all with real Halo 2 proving (the cached TestProver pattern):

  • single_spend_padded_bundle_derives_id_from_published_nullifiers — the bug's exact shape: a 1-spend build (witnessed via the same merkle_path.root(cmx) anchor pattern production uses) must succeed, be padded to 2 actions, carry the real nullifier among the published set, and produce identity_id == derive_identity_id_from_actions(actions) while != identity_id_from_nullifiers([real_nullifier]) — proving the dummy participates. Pre-fix this failed the post-proving consistency check.
  • two_spend_unpadded_bundle_id_matches_real_nullifier_derivation — no padding: the published set equals the real spends', and both derivations agree.
  • prove_and_sign_bundle_with_closure_receives_published_nullifiers — the closure contract: it runs once and receives exactly the authorized bundle's published nullifiers (one per action, padding included, on-wire order).
  • prove_and_sign_bundle_with_closure_error_short_circuits_before_proving — closure errors propagate before any proving work.
  • hrp_is_shared_across_all_non_mainnet_networks — pins the DIP-0018 premise the wallet check now relies on (testnet/devnet/regtest share tdash; mainnet differs).

cargo check --workspace --all-targets green; cargo check -p dpp / -p platform-wallet --all-features --tests green; clippy clean on the touched crates; existing shielded builder suites still pass.

Both fixes were also exercised end-to-end on devnet paloma during the shielded matrix run: post-fix, unshield (type 17) completed pool → platform-address, and the type-20 built, proved, and broadcast successfully with a single-note spend.

Breaking Changes

None. Client-side builder/wallet fixes only — no consensus, serialization, or protocol-version changes. build_spend_bundle / prove_and_sign_bundle keep their existing signatures (the _with variants are additive, crate-internal).

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

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed network address validation that was incorrectly rejecting valid testnet/devnet/regtest addresses due to HRP handling
  • Refactor

    • Refactored shielded bundle builders for improved sighash computation
    • Updated identity derivation logic in shielded pool operations
  • Tests

    • Added network address validation tests

QuantumExplorer and others added 2 commits June 10, 2026 15:34
… published nullifiers

The type-20 builder derived the new identity's id from the requested
spends' nullifiers, but BundleType::DEFAULT pads a single-spend bundle
to the 2-action minimum — and the padding action's random dummy
nullifier is published on the wire. Consensus re-derives the id over
ALL published action nullifiers, so every 1-note identity creation
failed the builder's own post-proving id-consistency check (after
paying the full Halo 2 proving cost).

Fix: new build_spend_bundle_with / prove_and_sign_bundle_with variants
take a closure that computes the extra sighash data from the BUILT
bundle's published nullifiers — after Builder::build fixes the action
set (padding included), before the sighash is bound. The identity id
is now derived inside that hook, so it always equals the consensus
re-derivation. The post-build consistency assert is kept as an
invariant guard.

Regression tests (real Halo 2 proving): a single-spend padded build
succeeds with id == derive_identity_id_from_actions(actions) and
!= the real-spends-only derivation; a two-spend unpadded build matches
both; the closure contract (published set, on-wire order, padding
included) and closure-error short-circuit are pinned. Also dedupes a
doubled comment paragraph in drive-abci's type-20 state arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shielded_unshield_to rejected the parsed output address with a raw
Network equality check, but bech32m parsing can only yield Mainnet or
Testnet (testnet/devnet/regtest share the "tdash" HRP per DIP-0018) —
so a Devnet wallet rejected every address, including its own, making
unshield unusable on devnet/regtest.

Compare hrp_for_network() of both sides instead, matching what the
encoding can actually represent. A new test pins the DIP-0018 premise
(testnet/devnet/regtest share the HRP; mainnet differs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d023a07-bd98-4f66-831e-e68ee5c1b4a9

📥 Commits

Reviewing files that changed from the base of the PR and between ba94110 and 0bd3395.

📒 Files selected for processing (5)
  • packages/rs-dpp/src/address_funds/platform_address.rs
  • packages/rs-dpp/src/shielded/builder/identity_create_from_shielded_pool.rs
  • packages/rs-dpp/src/shielded/builder/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs
  • packages/rs-platform-wallet/src/wallet/platform_wallet.rs

📝 Walkthrough

Walkthrough

This PR refactors Orchard shielded bundle builders to accept closures for extra sighash computation and updates identity creation to derive IDs from published action nullifiers. HRP-based network validation is added to platform address handling and integrated into wallet logic.

Changes

Shielded Bundle Builder and Network Validation

Layer / File(s) Summary
HRP Network Validation Foundation
packages/rs-dpp/src/address_funds/platform_address.rs
Unit test verifies PlatformAddress::hrp_for_network returns shared tdash HRP for all non-mainnet networks (Testnet/Devnet/Regtest) while Mainnet HRP remains distinct, emphasizing HRP string comparison over raw network equality.
Bundle Builder Closure Refactoring
packages/rs-dpp/src/shielded/builder/mod.rs
build_spend_bundle and prove_and_sign_bundle delegate to *_with variants accepting closures that receive published action nullifiers in on-wire order. New public function prove_and_sign_bundle_with extracts unauthorized action nullifiers from the bundle, invokes the closure for extra sighash computation, then computes platform sighash and applies signatures. Tests verify closure receives exact nullifier set including randomized dummy nullifiers for padded actions and that errors propagate without reaching proof generation.
Identity Creation from Published Nullifiers
packages/rs-dpp/src/shielded/builder/identity_create_from_shielded_pool.rs
Derives new identity identity_id from published action nullifiers (including padding dummy nullifiers) via closure passed to build_spend_bundle_with, rather than from pre-build spend nullifiers. Defers id derivation until bundle construction hook can access finalized published action set. Updated tests assert single-spend padded bundles derive id from all published actions while differing from real-spend-only derivation; two-spend unpadded bundles derive from real nullifiers only.
Wallet Integration and Validation Documentation
packages/rs-platform-wallet/src/wallet/platform_wallet.rs, packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs
Wallet shielded_unshield_to now compares HRPs via PlatformAddress::hrp_for_network instead of raw network equality, allowing addresses with shared HRP across testnet/devnet/regtest. Validation documentation clarified to explain optimistic SUCCESS action construction, rejection forwarding, and fail-closed behavior if pre-building occurs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

ready for final review

Suggested reviewers

  • shumkov

Poem

A rabbit hops through nullifier trees,
Where bundles bloom and closures please,
With published action sets aligned,
HRP harmonies intertwined, ✨
Identity flows through shielded streams.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: deriving shielded identity-create id from the padded bundle's published nullifiers, which is the primary fix for the Type-20 single-note spend failure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/trusting-wescoff-91010b

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

❤️ Share

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

@thepastaclaw

thepastaclaw commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 0bd3395)

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed

@QuantumExplorer QuantumExplorer merged commit c5d9d55 into v3.1-dev Jun 10, 2026
32 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/trusting-wescoff-91010b branch June 10, 2026 13:47
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.60305% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.12%. Comparing base (af5611e) to head (0bd3395).
⚠️ Report is 8 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...lded/builder/identity_create_from_shielded_pool.rs 89.10% 17 Missing ⚠️
packages/rs-dpp/src/shielded/builder/mod.rs 95.87% 4 Missing ⚠️
...kages/rs-dpp/src/address_funds/platform_address.rs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3843      +/-   ##
============================================
+ Coverage     87.04%   87.12%   +0.08%     
============================================
  Files          2677     2641      -36     
  Lines        329918   327535    -2383     
============================================
- Hits         287182   285370    -1812     
+ Misses        42736    42165     -571     
Components Coverage Δ
dpp 87.66% <92.27%> (+0.24%) ⬆️
drive 86.03% <100.00%> (+0.04%) ⬆️
drive-abci 89.30% <ø> (+<0.01%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.20% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.55% <ø> (ø)
🚀 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
Copy Markdown
Contributor

✅ 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: "31fc381d66878fad5f7ac47075f9e2f924a4c9a9bb52e54ee2c3191a5bf5cc62"
)

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.

@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.

Code Review

Two narrowly-scoped client-side fixes: (1) derive the shielded identity-create id from the built bundle's published action nullifiers via a post-build closure so it matches the consensus rederivation over padded actions, and (2) compare unshield destination by HRP instead of raw Network so devnet/regtest work. Both changes are correct, well-tested, and have no consensus surface. Only minor test-coverage and documentation nitpicks remain; nothing blocking.

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

1 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-dpp/src/shielded/builder/mod.rs`:
- [SUGGESTION] packages/rs-dpp/src/shielded/builder/mod.rs:685-700: Short-circuit test does not actually enforce "before proving"
  The test name and intent — that the closure error propagates before Halo 2 proving runs — is the exact invariant motivating the closure refactor (the original bug burned a full ~30 s proof before failing). But the test passes `TestProver`, so it only verifies that the closure error propagates. If `prove_and_sign_bundle_with` were ever reordered to call `create_proof` before invoking the closure and then return the same closure error, this test would still pass. Use a test-only prover whose `proving_key()` panics so any accidental call into proving fails the test deterministically.

Comment on lines +685 to +700
fn prove_and_sign_bundle_with_closure_error_short_circuits_before_proving() {
let builder = output_only_builder(10_000);

let result = prove_and_sign_bundle_with(builder, &TestProver, &[], |_| {
Err(ProtocolError::ShieldedBuildError(
"closure rejected".to_string(),
))
});

match result {
Err(ProtocolError::ShieldedBuildError(msg)) => {
assert_eq!(msg, "closure rejected", "closure error must pass through");
}
other => panic!("expected the closure's error to propagate, got {:?}", other),
}
}

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.

🟡 Suggestion: Short-circuit test does not actually enforce "before proving"

The test name and intent — that the closure error propagates before Halo 2 proving runs — is the exact invariant motivating the closure refactor (the original bug burned a full ~30 s proof before failing). But the test passes TestProver, so it only verifies that the closure error propagates. If prove_and_sign_bundle_with were ever reordered to call create_proof before invoking the closure and then return the same closure error, this test would still pass. Use a test-only prover whose proving_key() panics so any accidental call into proving fails the test deterministically.

source: ['codex']

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