You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue proposes a new Dash Platform state transition, IdentityCreateFromShieldedPool (StateTransitionType = 20), that spends Orchard shielded-pool notes (an Orchard proof + nullifiers, exactly like Unshield) to exit a fixed denomination of value and uses that value to create a brand-new Platform identity with a variable set of public keys (like IdentityCreate).
Today there are two ways to create an identity (IdentityCreate from an asset lock, IdentityCreateFromAddresses from transparent address inputs) and a family of shielded transitions (Shield=15, ShieldedTransfer=16, Unshield=17, ShieldFromAssetLock=18, ShieldedWithdrawal=19). There is no way to create an identity directly out of the shielded pool. This transition closes that gap: a user holding shielded notes can bootstrap a fresh identity without first unshielding to a transparent address (which would deanonymize the funding source and require a second transition).
Because the exit amount is restricted to a small versioned set of denominations (0.1 / 0.3 / 0.5 / 1.0 DASH), every identity-creation exit of a given size is indistinguishable on-chain, maximizing the anonymity set — mirroring the uniformity already enforced for ShieldedTransfer (whose value_balance must equal the min fee exactly so no fee fingerprint distinguishes senders).
It is version-gated to protocol v12 (SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 12, packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs:4), like every other shielded transition.
The transition is a hybrid: its spend mechanics come from Unshield/ShieldedWithdrawal, its output mechanics (variable-key identity insert) come from IdentityCreate, and its merged prove/verify proof is taken from the strict ShieldFromAssetLock surplus pattern.
High-level design — the Unshield-exit + IdentityCreate hybrid
Flow (in words):
Client selects shielded notes summing to (at least) a chosen fixed denomination, generates the new identity's keys, and builds an Orchard spend bundle whose value_balance exits exactly the denomination from the pool. The transparent payload (identity id + the full public-key set + denomination) is committed into the Orchard sighash via extra_sighash_data, and any excess re-enters the pool as a change note.
Drive-abci reconstructs and verifies the Orchard bundle (Halo2 proof + per-action spend-auth sigs + binding sig), checks the anchor exists, checks the nullifiers are unspent (and not duplicated intra-bundle), checks the pool has enough balance, validates denomination ∈ versioned set, validates the identity key structure + per-key proofs-of-possession, and re-derives + checks the identity id.
The action is converted to GroveDB ops: insert nullifiers + AddNewIdentity (identity + N keys + balance) + AddToSystemCredits(denomination) + (optional) insert change notes + UpdateTotalBalance(pool − denomination).
At execution, the metered cost of those writes plus the flat shielded verification fee is computed; that total_fee is deducted from the new identity's balance and routed to the fee pools. The new identity ends with denomination − total_fee.
Reused from IdentityCreate: the Vec<IdentityPublicKeyInCreation> payload, key structure validation, per-key proof-of-possession, and the AddNewIdentity op (identity + N keys + balance in one metered op).
Novel (must be built): a new transition struct + StateTransitionType = 20; a new extra_sighash_data helper binding the identity payload; an identity_create_from_shielded_pool fee function pricing the variable N-key write; a new ExecutionEvent variant (no existing variant both debits the pool AND credits a new identity); the strict merged identity+nullifier prove/verify and a new StateTransitionProofResult variant; the versioned denomination set; and a new identity-id derivation from nullifiers (no asset-lock outpoint exists).
Suggested implementation staging
This is a large feature; implement and review it in stages (multiple PRs), mirroring how the shielded family itself was built — and settle Open decisions (a) identity-id derivation and (b) the sighash binding first, since they shape the struct and the conservation:
DPP foundation — the transition struct + StateTransitionType = 20 (TAIL) + serialization/PlatformSignable + the extra_sighash_data helper + the versioned denomination set + the fee function.
Action + booking — the StateTransitionAction, the converter, the new ExecutionEvent variant, and the credit-conservation booking — gated by a block-level sum-tree conservation test before anything else lands.
Strict merged prove/verify + the new StateTransitionProofResult variant (+ padded-proof negative test).
SDK + wasm-dpp2 + FFI/wallet surface (Swift via the swift-rust-ffi-engineer agent).
Book & docs.
Proposed transition structure
Create packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/, mirroring the unshield_transition/ layout. The V0 struct (modeled on UnshieldTransitionV0 at .../unshield_transition/v0/mod.rs:32-45 plus the IdentityCreate key set):
#[platform_serialize(unversioned)]pubstructIdentityCreateFromShieldedPoolTransitionV0{// Identity payload (signed/bound into the Orchard sighash — see Open decision (b))#[platform_signable(into = "Vec<IdentityPublicKeyInCreationSignable>")]pubpublic_keys:Vec<IdentityPublicKeyInCreation>,// VARIABLE (1..=max_public_keys_in_creation)pubdenomination:u64,// credits; MUST equal value_balance exactly// Orchard spend (mirrors Unshield)pubactions:Vec<SerializedAction>,// spend/output pairspubanchor:[u8;32],// Sinsemilla rootpubproof:Vec<u8>,// Halo2pubbinding_signature:[u8;64],// RedPallas// Derived, NOT serialized, NOT in sig hash (recomputed on deserialize)#[cfg_attr(feature = "serde-conversion", serde(skip))]#[platform_signable(exclude_from_sig_hash)]pubidentity_id:Identifier,}
Signing / signed-field notes:
There is NO platform identity signature field — like Unshield/ShieldedWithdrawal, authorization is 100% the Orchard proof + per-action spend_auth_sig + binding signature over the platform sighash. (This is the key divergence from ShieldFromAssetLock, which does carry an ECDSA signature for the asset-lock key.)
denomination and public_keys are the transparent state-determining fields; they MUST be committed into the Orchard extra_sighash_data so the bundle cannot be redirected to a different identity/keys (see Open decision (b)).
identity_id mirrors IdentityCreate's pattern: serde(skip), excluded from sig hash, decoded via an inner struct, and re-derived on deserialization (see Open decision (a)). Add an advanced-structure check that the supplied id matches the re-derived value.
unique_identifiers() (for mempool replay protection) = hex of each action nullifier, exactly as Unshield does (.../unshield_transition/v0/state_transition_like.rs:36-41).
user_fee_increase / signature / owner_id / inputs / required_number_of_private_keys are all 0/None (no fee bidding, no transparent inputs).
Use &'static [u64] (NOT Vec) because every DRIVE_ABCI_VALIDATION_VERSIONS_V* is a const. Populate it in v8.rs's event_constants block (the shielded constants live at v8.rs:317-329):
Leave it empty/disabled in v1.rs…v7.rs so it is gated off pre-v12.
event_constants is the correct home (over SystemLimits) because the validation path already reaches platform_version.drive_abci.validation_and_processing.event_constants for every other shielded constant (shielded_implicit_fee_cap, shielded_proof_verification_fee).
The Orchard value_balance MUST equal the chosen denomination EXACTLY (not >=). This follows the ShieldedTransferamount_is_pure_fee exact-equality model (shielded_proof.rs:269-284), NOT the Unshield>= model. The basic-structure validator checks denomination ∈ shielded_identity_create_denominations; the proof verifier passes value_balance = denomination as i64 to reconstruct_and_verify_bundle, and the binding signature proves the value commitments sum to exactly that. Pinning the pool debit, the identity credit + fee, and the ZK-proven balance to the same fixed number leaves no slack to leak a fingerprint.
compute_shielded_verification_fee_v0 (packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs:27-45) = shielded_proof_verification_fee + num_actions × shielded_per_action_processing_fee. GroveDB cannot meter the proof/sig verification, so this is added on top as a fixed processing cost (the pattern transparent Shield and IdentityCreateFromAddresses use via additional_fixed_fee_cost).
The metered part is obtained from apply_drive_operations(...) — apply=false for the pre-execution affordability estimate, apply=true for the authoritative execution cost. It grows monotonically with N keys (each IdentityPublicKey adds GroveDB inserts into the key subtrees), which is exactly why the flat PaidFromShieldedPool model does NOT fit and a metered booking is required.
A new fee function compute_shielded_identity_create_fee(num_actions, num_keys, platform_version) should be added next to compute_shielded_unshield_fee in compute_minimum_shielded_fee/v0/mod.rs and its mod.rs dispatcher (keyed on dpp.methods.compute_minimum_shielded_fee), so the client can predict the fee offline and size its bundle. Model it on compute_shielded_unshield_fee_v0 (:167-189): base shielded fee + flat identity-byte component + num_keys × per-key-byte component, OR meter AddNewIdentity directly and add only the compute fee. New byte constants go in packages/rs-dpp/src/shielded/mod.rs alongside SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES = 222 (:92), calibrated the same way.
Conservation deltas (must hold at the sum-tree level):
Shielded pool sum tree: −= denomination (UpdateTotalBalance { current_total_balance − denomination }).
System/identity credits: += denomination (AddToSystemCredits(denomination)), then identity balance reduced by total_fee → net identity credits += (denomination − total_fee).
Fee pools: += total_fee (the returned FeeResult, distributed by process_block_fees_and_validate_sum_trees).
CRITICAL — AddToSystemCredits must equal denomination, not denomination − total_fee. The identity is created holding the full denomination; the fee is then moved from the identity balance into the fee pools (it never leaves the credit supply). This mirrors IdentityCreate's AddToSystemCredits { amount: initial_balance } (packages/rs-drive/src/state_transition_action/action_convert_to_operations/identity/identity_create_transition.rs:46-48). Note this differs subtly from Unshield, which does NOT call AddToSystemCredits (its sink is a transparent address already in circulation). Here, because the identity is freshly created and the pool is decremented, the credit accounting requires the AddToSystemCredits(denomination) link. Getting this wrong mints/burns credits and halts the chain (the exact class of bug fixed by the shield credit-conservation work).
Reject-if total_fee >= denomination: if the metered + compute fee meets or exceeds the fixed denomination, the new identity would be credited ≤ 0. This must be a clean consensus rejection (no nullifier written), not an underflow.
Recommended error: reuse IdentityInsufficientBalanceError (dpp::consensus::state::identity), via the PaidFromAssetLock-style affordability check at validate_fees_of_event/v0/mod.rs:84-100 with previous_balance = denomination, added_balance = 0. Zero new error surface.
Alternative: a dedicated ShieldedDenominationCannotCoverIdentityFeeError under dpp::consensus::state::shielded (clearer reason string; costs a new state-error variant + code 40905 + version gating).
Design implication: the smallest denomination (0.1 DASH = 10×10⁹ credits) practically caps N — shielded_proof_verification_fee alone is ~100M credits, and large N could approach the floor. Document the per-denomination key budget and consider capping keys for the smallest denomination in basic-structure validation so the client fails fast at build time.
Guards:
Per-transition fee-coverage guard at execution (model: paid_from_address_inputs_and_outputsfee_fully_covered check, execute_event/v0/mod.rs:186-190; here the IdentityInsufficientBalanceError path is the analog).
Block-level sum-tree conservation checkprocess_block_fees_and_validate_sum_trees_v0 (packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs:171-190): calculate_total_credits_balance(...).ok() → CorruptedCreditsNotBalanced chain-halt if violated. A full-block test MUST exercise and pass this (see Testing).
The proof must attest both the created identity and the spent nullifiers in a single merged multi-root GroveDB proof. Build it on the STRICT merged-query pattern from day one — NOT the Unshield subset pattern.
The Unshield verify side uses GroveDb::verify_subset_query_with_absence_proof (packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs:32-44, subset flag true), which accepts a proof containing the queried keys but does NOT reject extra branches — a malicious prover can pad the proof. Issue #3812 tracks tightening these. The correct reference is the STRICT ShieldFromAssetLock surplus path introduced in PR #3793, commit 63c31f7c6e (verify_state_transition_was_executed_with_proof/v0/mod.rs:1293-1507), which uses GroveDb::verify_query_with_absence_proof against the prover's exact merged query.
PROVE side (packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs, new arm modeled on the Unshield arm :322 and the ShieldFromAssetLock surplus-merge :411):
Build the nullifier PathQuery over shielded_credit_pool_nullifiers_path_vec().
Build the identity PathQuery via Drive::full_identity_query(&identity_id, ...) (as IdentityCreateFromAddresses does at :252).
Clear each sub-query's limit to None before merging — PathQuery::merge rejects limited sub-queries.
PathQuery::merge(vec![&nullifier_pq, &identity_pq], grove_version), feed to grove_get_proved_path_query.
VERIFY side (verify_state_transition_was_executed_with_proof/v0/mod.rs, new arm modeled on the strict ShieldFromAssetLock arm :1293-1507):
Rebuild the byte-identical merged PathQuery (same roots, same sub-queries, limit = None).
Set the merged query limit = Some(u16::MAX) (the :1346-1358 gotcha): verify_query_with_absence_proof requires a limit, and merge leaves it None. Use an unreachableu16::MAX so the per-layer succinctness check (which rejects extra proof branches) runs fully on every layer. A smaller limit would break the result loop early and falsely reject honest proofs; the limit does NOT relax extra-data rejection.
Partition the proved key/values. Unlike the ShieldFromAssetLock reference (which partitions by key length, 36-byte outpoint vs 21-byte address), the nullifier keys and the identity id are BOTH 32 bytes — so partition by PATH/root prefix (the (path, key, element) tuple's path segment: nullifier tree vs RootTree::Identities), not key length. This is the one place the new transition diverges from the reference.
Assert root-hash unity, require the identity element present, and return a new StateTransitionProofResult variant.
New proof-result variant (packages/rs-dpp/src/state_transition/proof_result.rs, append at TAIL after :76): e.g. VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec<u8>, bool)>). The closest existing template is VerifiedShieldedNullifiersWithWithdrawalDocument (:65-68) — replace the document map with the verified Identity.
Open design decisions
(a) Identity-id derivation — HARD, UNRESOLVED
There is no asset-lock outpoint (IdentityCreate) and no address+nonce inputs (IdentityCreateFromAddresses). The id must be unique, deterministic, non-malleable, and bound to the Orchard authorization.
Option
Tradeoffs
A. id = double_sha256(sorted nullifiers) (RECOMMENDED)
+ Unique (nullifiers are globally-unique one-time spend tags, enforced by validate_nullifiers, shielded_common/mod.rs:278-316). + Deterministic. + Already covered by the Orchard bundle commitment (reconstruct_and_verify_bundle:211), so binding is free. + Sorting the nullifier set makes action-ordering non-malleable. − Minor: links the id to a public nullifier (the nullifier is public anyway).
B. id = double_sha256(platform_sighash)
+ Unique per bundle, automatically bound. − The id is derived FROM the sighash, so it must NOT also be inside extra_data (avoid circularity).
C. Dedicated committed identity_id field, required ∈ extra_sighash_data
+ Client can choose a vanity/known id. − Requires a uniqueness-against-state GroveDB read (client controls it) + more validation surface.
Recommended: Option A — re-derive at consensus and reject a mismatch exactly like IdentityCreate (identity_create/advanced_structure/v0/mod.rs:43-78), but since there is no asset lock to penalize, a mismatch is a plain consensus rejection (no nullifier consumed). hash_double is at crate::util::hash::hash_double.
(b) Authorization / signing + sighash-binding requirement — HARD
Two authorization boundaries must BOTH hold and be cross-bound:
Orchard authorizes the SPEND — proof + per-action spend_auth_sig + binding sig (no platform signature field). Transparent fields are bound via extra_sighash_data fed into compute_platform_sighash (packages/rs-dpp/src/shielded/mod.rs:111-117).
Each identity key proves possession — per-key signatures over signable_bytes (model: identity_create/identity_and_signatures/v0/mod.rs:21-42).
CRITICAL binding (the surplus_output lesson): in ShieldFromAssetLock, surplus_output is a signed field placed BEFORE the excluded signature so the ECDSA sig commits to it and it cannot be redirected (.../shield_from_asset_lock_transition/v0/mod.rs:49-59). The Orchard signature here is the analog of that ECDSA signature. The identity id AND the full public-key set AND the denomination MUST be inside the Orchard extra_sighash_data. Otherwise a relayer/validator could take a valid bundle exiting a denomination and re-point it at a different identity id or swap in different keys they control, stealing the credited balance — the exact redirection attack the unshieldoutput_address and surplus_output bindings prevent. Per-key proofs-of-possession alone do NOT prevent this (a relayer keeps valid PoP sigs for their own keys while swapping the bundle); the extra_sighash_data binding is what makes (this spend → these exact keys → this id) atomic.
Add a new helperidentity_create_from_shielded_extra_sighash_data(...) in packages/rs-dpp/src/shielded/mod.rs (model: unshield_extra_sighash_data:161-166, shielded_withdrawal_extra_sighash_data:140-152) producing:
identity_id (32) || denomination (u64 LE) || num_keys (u16 LE)
|| for each key in canonical order: key_id || purpose || security_level || key_type || len-prefixed key_data
Length-prefix the variable key list (unlike the withdrawal helper, whose output_script is fixed-length). Call it from both the builder and the verifier.
Penalty model: there is no asset lock to burn (unlike IdentityCreate). A bad Orchard proof or id/key mismatch is simply rejected (transition invalid, no nullifier written) — the Unshield model. Decide whether proof verification runs in validate_shielded_proof (Unshield) or inside transform_into_action (ShieldFromAssetLock); since there is no collateral to penalize, the Unshield validate_shielded_proof placement fits, BUT note Research finding 4 argues for keeping it in transform_into_action so a bad proof can't be spammed cheaply against pool state — this placement is itself an open sub-decision to settle during implementation.
(c) Fixed denominations
Restrict the exit to {0.1, 0.3, 0.5, 1.0} DASH (versioned). Recommended: yes — a variable exit amount is a distinguishing fingerprint; a small fixed set maximizes the anonymity set (mirrors ShieldedTransfer's exact-fee uniformity). Tradeoff: clients must hold/select notes that cover a denomination, and the smallest denomination caps N keys (decision (b)/fee section). The set is versioned so it can grow without a hard fork beyond a protocol bump.
(d) Change-note vs no-change
Option
Tradeoffs
Change note back to pool (RECOMMENDED)
+ Client can spend any notes ≥ denomination (no pre-splitting). + Mirrors Unshield, which already insert_notes for change (unshield_transition.rs:59). + The change note is an ordinary indistinguishable Orchard output. − Extra output action raises num_actions → higher compute + note-storage cost.
No change
+ Simplest, smallest proof, lowest fee → more of the denomination reaches the identity. + Most uniform bundle shape. − Client must hold notes summing exactly to a denomination.
Recommended: allow a change note — it matches the established Unshield mechanics and avoids forcing exact-denomination note pre-splitting. Either way, value_balance (the net leaving the pool) must equal the denomination exactly; the change stays internal to the bundle.
(e) Replay
Resolved: nullifiers provide replay protection; no outpoint needed. The converter writes each spent note's nullifier (insert_nullifiers, using insert_only_known_to_not_already_exist_op); validate_nullifiers (shielded_common/mod.rs:278-316) rejects any nullifier already in state or duplicated intra-bundle (read-your-own-writes within the block). Re-submitting fails at the nullifier check. Because the id (decision (a), Option A) is derived from those same nullifiers, it is single-use by construction. unique_identifiers() = nullifier hexes gives mempool-level dedup.
Implementation checklist
Two cross-cutting rules apply throughout:
bincode TAIL-APPEND.StateTransition, StateTransitionType, BasicError, StateError are encoded by positional discriminant under bincode (Encode, Decode derives). A new variant MUST be appended at the TAIL or every later variant's wire discriminant shifts, breaking deserialization of all historical/in-flight data. StateTransitionProofResult / StateTransitionAction / ExecutionEvent are not bincode-positional but append at tail anyway by convention.
v12 version-gating. Every new validation/serialization/method version field must be enabled only from the v12-activation version file and disabled (None/empty) earlier; runtime gating goes through is_allowed.rs.
DPP
New transition source tree packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/ (mirror the shield_from_asset_lock_transition/ 17-file layout: mod.rs, version.rs, fields.rs, state_transition_like.rs, state_transition_validation.rs, state_transition_estimated_fee_validation.rs, proved.rs, signing_tests.rs, methods/{mod,v0/mod}.rs, v0/{mod,types,version,v0_methods,state_transition_like,state_transition_validation,proved}.rs).
Register pub mod identity_create_from_shielded_pool_transition; in .../shielded/mod.rs.
StateTransition enum + ALL macro/match arms in packages/rs-dpp/src/state_transition/mod.rs: import + ...Signable (~:151); call_method! arms (:160-209); identity-signed macros → None/no-op/CorruptedCodeExecution (:211-411); append the enum variant at TAIL after ShieldedWithdrawal (:451); active_version_range 12..=LATEST shielded group (:535-539); is_identity_signed exclusion (:543); name→"IdentityCreateFromShieldedPool" (:588); signature→None; required_number_of_private_keys→0; user_fee_increase→0; owner_id/inputs→None; set_signature/set_user_fee_increase no-ops.
StateTransitionType enum (packages/rs-dpp/src/state_transition/state_transition_types.rs:20): add IdentityCreateFromShieldedPool = 20 at TAIL, plus Display, TryFrom<u8>, all-variants list, and update the in-file roundtrip tests.
New extra_sighash_data helper in packages/rs-dpp/src/shielded/mod.rs (decision (b)).
New compute_shielded_identity_create_fee in compute_minimum_shielded_fee/{v0/mod.rs,mod.rs} + new storage-byte constants in shielded/mod.rs (keyed on dpp.methods.compute_minimum_shielded_fee).
PlatformSignable derive on the V0 struct; mirror ShieldFromAssetLock/Unshield field attributes. Add a "signable_bytes differ when public_keys / denomination differ" test.
rs-drive
New action tree packages/rs-drive/src/state_transition_action/shielded/identity_create_from_shielded_pool/{mod,transformer,v0/mod,v0/transformer}.rs (carry built Identity/keys/balance, nullifiers, denomination, fee_amount); register in .../shielded/mod.rs.
StateTransitionAction enum (packages/rs-drive/src/state_transition_action/mod.rs): import (~:35), append IdentityCreateFromShieldedPoolAction(...) at TAIL after :109, user_fee_increase arm (:114).
New converter action_convert_to_operations/shielded/identity_create_from_shielded_pool_transition.rs emitting: insert_nullifiers + AddNewIdentity{ balance: denomination } + AddToSystemCredits(denomination) + (optional) insert_notes change + UpdateTotalBalance(pool − denomination). NO AddToSystemCredits mistake: it IS needed here and equals denomination (see Conservation). Register in .../shielded/mod.rs and add a dispatch arm in action_convert_to_operations/mod.rs:35. Add a new converter version key identity_create_from_shielded_pool_transition to drive_state_transition_method_versions.
Prove arm (prove/prove_state_transition/v0/mod.rs) and strict verify arm (verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs) — see Execution proof. (Verify side builds under --features verify; check with cargo check -p drive --no-default-features --features verify.)
rs-drive-abci
New validation module execution/validation/state_transition/state_transitions/identity_create_from_shielded_pool/{mod,tests,transform_into_action/{mod,v0/mod}}.rs (reuse shielded_common helpers + reconstruct_and_verify_bundle; do anchor/nullifier/pool checks, denomination check, key-structure + PoP validation, id re-derivation, fee computation). Register in state_transitions/mod.rs.
Transformer dispatch arm in execution/validation/state_transition/transformer/mod.rs (~:270).
Processor predicate/dispatch traits (processor/traits/*.rs): add an arm to each — is_allowed.rs (true, gate on SHIELDED_POOL_INITIAL_PROTOCOL_VERSION in both match sites), basic_structure.rs (call V0 validate_structure), shielded_proof.rs (has_shielded_proof_validation / has_shielded_minimum_fee_validation / validate_shielded_proof arm with FLAGS_SPENDS_AND_OUTPUTS, value_balance = denomination as i64, new extra_sighash_data; new ShieldedMinFeeKind::IdentityCreate tuple arm checking denomination ∈ set and denomination >= compute_shielded_identity_create_fee(...)), state.rs/identity_based_signature.rs/identity_nonces.rs/address_*/identity_balance.rs (all false/not-identity-signed), advanced_structure_with_state.rs (decide true if key-uniqueness-in-state is done there).
New ExecutionEvent variantPaidFromShieldedPoolToNewIdentity { identity, operations, execution_operations, denomination, additional_fixed_fee_cost } in execution/types/execution_event/mod.rs (append at tail) + a create_from_state_transition_action arm (after :555).
validate_fees_of_event/v0/mod.rs arm (~:50): affordability check denomination >= total_fee → IdentityInsufficientBalanceError (or new error); update no_fee_events tests.
execute_event/v0/mod.rs: classify the new variant in maybe_fee_validation_result (~:367); main match event arm (after :624) that applies ops, meters, adds additional_fixed_fee_cost to processing, deducts total_fee from the new identity via into_balance_change + apply_balance_change_from_fee_to_identity, returns SuccessfulPaidExecution.
rs-platform-version
Add shielded_identity_create_denominations: &'static [u64] to DriveAbciValidationConstants (drive_abci_validation_versions/mod.rs:27-53); populate in v8.rs event_constants, empty in v1–v7.
Add identity_create_from_shielded_pool_state_transition: DriveAbciStateTransitionValidationVersion to DriveAbciStateTransitionValidationVersions (mod.rs:66); set per-version in v1.rs…v8.rs (disabled pre-v12, Some(0)/0 in v8).
Add identity_create_from_shielded_pool_state_transition: FeatureVersionBounds to DPPStateTransitionSerializationVersions (dpp_state_transition_serialization_versions/{mod,v1,v2}.rs).
Add the converter method version key to drive_state_transition_method_versions/v2.rs.
Consensus errors
If using a dedicated denomination error: ShieldedInvalidDenominationError (basic) — struct under packages/rs-dpp/src/consensus/basic/state_transition/ (clone ShieldedInvalidValueBalanceError); append to BasicError at TAIL (basic/basic_error.rs); code 10827 (next free; 10819–10826 used, codes.rs:236-243).
If using a dedicated under-coverage error: ShieldedDenominationCannotCoverIdentityFeeError (state) — under packages/rs-dpp/src/consensus/state/shielded/; append to StateError at TAIL (state/state_error.rs); code 40905 (next free, codes.rs:383-388). Otherwise reuse IdentityInsufficientBalanceError (zero new surface — recommended).
Register From/code mappings in codes.rs.
Proof prove/verify
Covered under rs-drive above. New StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec<u8>, bool)>) (append at tail, proof_result.rs:76). STRICT pattern only (PR feat(drive): shielded fees for Shield/ShieldFromAssetLock + shield credit conservation #379363c31f7c6e); add an empty-proof-returns-error test (clone verify_unshield_empty_proof_returns_error).
rs-sdk
New packages/rs-sdk/src/platform/transition/identity_create_from_shielded_pool.rs (clone unshield.rs: build via Orchard prover → ensure_valid_state_transition_structure → broadcast, returns the new proof-result variant); register in transition/mod.rs. No new Fetch query is added, so the README.md Fetch/FetchMany checklist (:125-162) does not apply.
wasm-dpp2
New packages/wasm-dpp2/src/shielded/identity_create_from_shielded_pool_transition.rs (clone unshield_transition.rs); declare in shielded/mod.rs; re-export in lib.rs:71.
state_transitions/base/state_transition.rs: type-code map (:313, => 20) + the five grouping matches that list Unshield(_) (:402,:429,:571,:648,:745).
packages/wasm-dpp/src/state_transition/state_transition_factory.rs:84 — add to the shielded group.
FFI + platform-wallet
rs-platform-wallet: new pub async fn identity_create_from_shielded_pool(...) in wallet/shielded/operations.rs (clone unshield/withdraw note-reservation + build + broadcast); add a ShieldedFeeKind variant in note_selection.rs for the denomination − fee reservation math; update module doc header.
rs-platform-wallet-ffi: new #[no_mangle] extern "C" fn platform_wallet_manager_shielded_identity_create_from_pool(...) in src/shielded_send.rs (clone platform_wallet_manager_shielded_unshield).
Swift surface MUST go through the swift-rust-ffi-engineer agent (per CLAUDE.md). Keep all business logic in rs-platform-wallet behind the FFI; Swift only persists/loads/bridges. No DAPI/.proto change is needed — broadcastStateTransition is opaque bytes dispatched by variant.
Book & docs
book/src/state-transitions/{lifecycle,validation-pipeline,transform-into-action,drive-operations}.md — add the transition to the per-transition tables.
book/src/fees/shielded-fees.md — document total_fee composition + the denomination set.
book/src/error-handling/{error-codes,consensus-errors}.md — add any new error codes.
book/src/evo-sdk/state-transitions.md — add the SDK usage entry.
Testing requirements
Block-level sum-tree conservation (full block pipeline). Drive a full block containing the transition and assert process_block_fees_and_validate_sum_trees_v0 passes (:171-190): pool −= denomination, new identity balance == denomination − total_fee, fee pools += total_fee, fee_refunds empty. Model on the ShieldFromAssetLock surplus block-level conservation test and strategy_tests/test_cases/shielded_tests.rs (run_chain_unshield_transitions) + verify_state_transitions.rs. Also keep an op-level delta test (model unshield_transition.rs:190-222).
Denomination validity / boundary. Each of {10,30,50,100}×10⁹ accepted; any other amount rejected with the denomination error; value_balance != denomination rejected.
total_fee >= denomination rejection. Construct a case (large N and/or smallest denomination) where the metered + verification fee meets/exceeds the denomination; assert clean rejection (no nullifier written), not underflow.
Variable key-count coverage. Exercise N = 1, a mid count, and max_public_keys_in_creation; assert metered total_fee grows monotonically with N and conservation holds at each.
Malleability / redirect-rejection. Mutating the bound identity payload (id or any key) while keeping the same valid Orchard bundle is rejected (model: test_different_output_address_with_same_valid_bundle_is_rejected, unshield/tests.rs:688). Also test_valid_proof_with_mutated_value_balance_is_rejected and test_duplicate_nullifiers_in_same_bundle analogs.
Fee-metering invariant. Clone test_minimum_shielded_fee_covers_actual_grovedb_write_cost (unshield_transition.rs:276-344): meter real GroveDB cost in estimation mode (apply=false) for the variable-N identity write and assert fee >= cost and fee > storage so the booking-split min() never binds.
Activates at SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 12 (feature_initial_protocol_versions.rs:4), the same gate as the entire shielded family.
Runtime enforcement: add StateTransition::IdentityCreateFromShieldedPool(_) to both has_is_allowed_validation and validate_is_allowed (processor/traits/is_allowed.rs:35-39,:77-94); pre-v12 it returns StateTransitionNotActiveError.
active_version_range in state_transition/mod.rs places it in the 12..=LATEST_VERSION shielded group.
All new version fields are enabled only in the v12-activation version file (v8.rs / STATE_TRANSITION_SERIALIZATION_VERSIONS_V2 / STATE_TRANSITION_VERSIONS_V3 / DPP_METHOD_VERSIONS_V2, wired by packages/rs-platform-version/src/version/v12.rs) and disabled in earlier version files.
Post-implementation checks: cargo check -p dpp then --workspace; cargo check -p drive --no-default-features --features verify; cargo fmt --all before committing.
Summary & motivation
This issue proposes a new Dash Platform state transition,
IdentityCreateFromShieldedPool(StateTransitionType = 20), that spends Orchard shielded-pool notes (an Orchard proof + nullifiers, exactly likeUnshield) to exit a fixed denomination of value and uses that value to create a brand-new Platform identity with a variable set of public keys (likeIdentityCreate).Today there are two ways to create an identity (
IdentityCreatefrom an asset lock,IdentityCreateFromAddressesfrom transparent address inputs) and a family of shielded transitions (Shield=15,ShieldedTransfer=16,Unshield=17,ShieldFromAssetLock=18,ShieldedWithdrawal=19). There is no way to create an identity directly out of the shielded pool. This transition closes that gap: a user holding shielded notes can bootstrap a fresh identity without first unshielding to a transparent address (which would deanonymize the funding source and require a second transition).Because the exit amount is restricted to a small versioned set of denominations (0.1 / 0.3 / 0.5 / 1.0 DASH), every identity-creation exit of a given size is indistinguishable on-chain, maximizing the anonymity set — mirroring the uniformity already enforced for
ShieldedTransfer(whosevalue_balancemust equal the min fee exactly so no fee fingerprint distinguishes senders).It is version-gated to protocol v12 (
SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 12,packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs:4), like every other shielded transition.The transition is a hybrid: its spend mechanics come from
Unshield/ShieldedWithdrawal, its output mechanics (variable-key identity insert) come fromIdentityCreate, and its merged prove/verify proof is taken from the strictShieldFromAssetLocksurplus pattern.High-level design — the Unshield-exit + IdentityCreate hybrid
Flow (in words):
value_balanceexits exactly the denomination from the pool. The transparent payload (identity id + the full public-key set + denomination) is committed into the Orchard sighash viaextra_sighash_data, and any excess re-enters the pool as a change note.denomination ∈ versioned set, validates the identity key structure + per-key proofs-of-possession, and re-derives + checks the identity id.AddNewIdentity(identity + N keys + balance) +AddToSystemCredits(denomination)+ (optional) insert change notes +UpdateTotalBalance(pool − denomination).total_feeis deducted from the new identity's balance and routed to the fee pools. The new identity ends withdenomination − total_fee.Reused verbatim (from
Unshield/shielded_common): Orchard bundle reconstruction + proof verify (reconstruct_and_verify_bundle,packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs:83-231), nullifier/anchor/pool-notes-floor stateful checks, nullifier replay protection,insert_nullifiers/insert_notes/UpdateTotalBalanceconverter ops, theSerializedActionwire format.Reused from
IdentityCreate: theVec<IdentityPublicKeyInCreation>payload, key structure validation, per-key proof-of-possession, and theAddNewIdentityop (identity + N keys + balance in one metered op).Novel (must be built): a new transition struct +
StateTransitionType = 20; a newextra_sighash_datahelper binding the identity payload; anidentity_create_from_shielded_poolfee function pricing the variable N-key write; a newExecutionEventvariant (no existing variant both debits the pool AND credits a new identity); the strict merged identity+nullifier prove/verify and a newStateTransitionProofResultvariant; the versioned denomination set; and a new identity-id derivation from nullifiers (no asset-lock outpoint exists).Suggested implementation staging
This is a large feature; implement and review it in stages (multiple PRs), mirroring how the shielded family itself was built — and settle Open decisions (a) identity-id derivation and (b) the sighash binding first, since they shape the struct and the conservation:
StateTransitionType = 20(TAIL) + serialization/PlatformSignable+ theextra_sighash_datahelper + the versioned denomination set + the fee function.StateTransitionAction, the converter, the newExecutionEventvariant, and the credit-conservation booking — gated by a block-level sum-tree conservation test before anything else lands.total_fee >= denominationgate.StateTransitionProofResultvariant (+ padded-proof negative test).swift-rust-ffi-engineeragent).Proposed transition structure
Create
packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/, mirroring theunshield_transition/layout. The V0 struct (modeled onUnshieldTransitionV0at.../unshield_transition/v0/mod.rs:32-45plus theIdentityCreatekey set):Signing / signed-field notes:
signaturefield — likeUnshield/ShieldedWithdrawal, authorization is 100% the Orchard proof + per-actionspend_auth_sig+ binding signature over the platform sighash. (This is the key divergence fromShieldFromAssetLock, which does carry an ECDSAsignaturefor the asset-lock key.)denominationandpublic_keysare the transparent state-determining fields; they MUST be committed into the Orchardextra_sighash_dataso the bundle cannot be redirected to a different identity/keys (see Open decision (b)).identity_idmirrorsIdentityCreate's pattern:serde(skip), excluded from sig hash, decoded via an inner struct, and re-derived on deserialization (see Open decision (a)). Add an advanced-structure check that the supplied id matches the re-derived value.unique_identifiers()(for mempool replay protection) = hex of each action nullifier, exactly asUnshielddoes (.../unshield_transition/v0/state_transition_like.rs:36-41).user_fee_increase/signature/owner_id/inputs/required_number_of_private_keysare all0/None(no fee bidding, no transparent inputs).Version enum (
mod.rs):pub enum IdentityCreateFromShieldedPoolTransition { V0(IdentityCreateFromShieldedPoolTransitionV0) }, derivesPlatformVersioned, From,#[platform_version_path_bounds("dpp.state_transition_serialization_versions.identity_create_from_shielded_pool_state_transition")].SerializedAction(packages/rs-dpp/src/shielded/mod.rs:214-258):nullifier[32], rk[32], cmx[32], encrypted_note(216), cv_net[32], spend_auth_sig[64]— reused unchanged.Fixed exit denominations
The exit amount is restricted to a small versioned array of denominations. 1 DASH = 100,000,000 duffs;
CREDITS_PER_DUFF = 1000, so:10_000_000_000(10 × 10⁹)30_000_000_000(30 × 10⁹)50_000_000_000(50 × 10⁹)100_000_000_000(100 × 10⁹)Exact location — add to
DriveAbciValidationConstantsinpackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs:27-53, alongsideshielded_implicit_fee_cap(line 52):Use
&'static [u64](NOTVec) because everyDRIVE_ABCI_VALIDATION_VERSIONS_V*is aconst. Populate it inv8.rs'sevent_constantsblock (the shielded constants live atv8.rs:317-329):Leave it empty/disabled in
v1.rs…v7.rsso it is gated off pre-v12.event_constantsis the correct home (overSystemLimits) because the validation path already reachesplatform_version.drive_abci.validation_and_processing.event_constantsfor every other shielded constant (shielded_implicit_fee_cap,shielded_proof_verification_fee).The Orchard
value_balanceMUST equal the chosen denomination EXACTLY (not>=). This follows theShieldedTransferamount_is_pure_feeexact-equality model (shielded_proof.rs:269-284), NOT theUnshield>=model. The basic-structure validator checksdenomination ∈ shielded_identity_create_denominations; the proof verifier passesvalue_balance = denomination as i64toreconstruct_and_verify_bundle, and the binding signature proves the value commitments sum to exactly that. Pinning the pool debit, the identity credit + fee, and the ZK-proven balance to the same fixed number leaves no slack to leak a fingerprint.Fee & credit model + conservation
Total fee:
compute_shielded_verification_fee_v0(packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs:27-45) =shielded_proof_verification_fee + num_actions × shielded_per_action_processing_fee. GroveDB cannot meter the proof/sig verification, so this is added on top as a fixed processing cost (the pattern transparentShieldandIdentityCreateFromAddressesuse viaadditional_fixed_fee_cost).apply_drive_operations(...)—apply=falsefor the pre-execution affordability estimate,apply=truefor the authoritative execution cost. It grows monotonically with N keys (eachIdentityPublicKeyadds GroveDB inserts into the key subtrees), which is exactly why the flatPaidFromShieldedPoolmodel does NOT fit and a metered booking is required.A new fee function
compute_shielded_identity_create_fee(num_actions, num_keys, platform_version)should be added next tocompute_shielded_unshield_feeincompute_minimum_shielded_fee/v0/mod.rsand itsmod.rsdispatcher (keyed ondpp.methods.compute_minimum_shielded_fee), so the client can predict the fee offline and size its bundle. Model it oncompute_shielded_unshield_fee_v0(:167-189): base shielded fee + flat identity-byte component +num_keys × per-key-bytecomponent, OR meterAddNewIdentitydirectly and add only the compute fee. New byte constants go inpackages/rs-dpp/src/shielded/mod.rsalongsideSHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES = 222(:92), calibrated the same way.Conservation deltas (must hold at the sum-tree level):
−= denomination(UpdateTotalBalance { current_total_balance − denomination }).+= denomination(AddToSystemCredits(denomination)), then identity balance reduced bytotal_fee→ net identity credits+= (denomination − total_fee).+= total_fee(the returnedFeeResult, distributed byprocess_block_fees_and_validate_sum_trees).Net:
−denomination + (denomination − total_fee) + total_fee = 0. Credits conserved.Reject-if
total_fee >= denomination: if the metered + compute fee meets or exceeds the fixed denomination, the new identity would be credited≤ 0. This must be a clean consensus rejection (no nullifier written), not an underflow.IdentityInsufficientBalanceError(dpp::consensus::state::identity), via thePaidFromAssetLock-style affordability check atvalidate_fees_of_event/v0/mod.rs:84-100withprevious_balance = denomination,added_balance = 0. Zero new error surface.ShieldedDenominationCannotCoverIdentityFeeErrorunderdpp::consensus::state::shielded(clearer reason string; costs a new state-error variant + code 40905 + version gating).Guards:
paid_from_address_inputs_and_outputsfee_fully_coveredcheck,execute_event/v0/mod.rs:186-190; here theIdentityInsufficientBalanceErrorpath is the analog).process_block_fees_and_validate_sum_trees_v0(packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs:171-190):calculate_total_credits_balance(...).ok()→CorruptedCreditsNotBalancedchain-halt if violated. A full-block test MUST exercise and pass this (see Testing).Execution proof — merged identity + nullifier proof (STRICT)
The proof must attest both the created identity and the spent nullifiers in a single merged multi-root GroveDB proof. Build it on the STRICT merged-query pattern from day one — NOT the
Unshieldsubset pattern.PROVE side (
packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs, new arm modeled on theUnshieldarm:322and theShieldFromAssetLocksurplus-merge:411):PathQueryovershielded_credit_pool_nullifiers_path_vec().PathQueryviaDrive::full_identity_query(&identity_id, ...)(asIdentityCreateFromAddressesdoes at:252).limittoNonebefore merging —PathQuery::mergerejects limited sub-queries.PathQuery::merge(vec![&nullifier_pq, &identity_pq], grove_version), feed togrove_get_proved_path_query.VERIFY side (
verify_state_transition_was_executed_with_proof/v0/mod.rs, new arm modeled on the strictShieldFromAssetLockarm:1293-1507):PathQuery(same roots, same sub-queries,limit = None).limit = Some(u16::MAX)(the:1346-1358gotcha):verify_query_with_absence_proofrequires a limit, andmergeleaves itNone. Use an unreachableu16::MAXso the per-layer succinctness check (which rejects extra proof branches) runs fully on every layer. A smaller limit would break the result loop early and falsely reject honest proofs; the limit does NOT relax extra-data rejection.GroveDb::verify_query_with_absence_proof(proof, &merged_pq, grove_version).ShieldFromAssetLockreference (which partitions by key length, 36-byte outpoint vs 21-byte address), the nullifier keys and the identity id are BOTH 32 bytes — so partition by PATH/root prefix (the(path, key, element)tuple's path segment: nullifier tree vsRootTree::Identities), not key length. This is the one place the new transition diverges from the reference.StateTransitionProofResultvariant.New proof-result variant (
packages/rs-dpp/src/state_transition/proof_result.rs, append at TAIL after:76): e.g.VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec<u8>, bool)>). The closest existing template isVerifiedShieldedNullifiersWithWithdrawalDocument(:65-68) — replace the document map with the verifiedIdentity.Open design decisions
(a) Identity-id derivation — HARD, UNRESOLVED
There is no asset-lock outpoint (
IdentityCreate) and no address+nonce inputs (IdentityCreateFromAddresses). The id must be unique, deterministic, non-malleable, and bound to the Orchard authorization.id = double_sha256(sorted nullifiers)(RECOMMENDED)validate_nullifiers,shielded_common/mod.rs:278-316). + Deterministic. + Already covered by the Orchard bundle commitment (reconstruct_and_verify_bundle:211), so binding is free. + Sorting the nullifier set makes action-ordering non-malleable. − Minor: links the id to a public nullifier (the nullifier is public anyway).id = double_sha256(platform_sighash)extra_data(avoid circularity).identity_idfield, required ∈extra_sighash_dataRecommended: Option A — re-derive at consensus and reject a mismatch exactly like
IdentityCreate(identity_create/advanced_structure/v0/mod.rs:43-78), but since there is no asset lock to penalize, a mismatch is a plain consensus rejection (no nullifier consumed).hash_doubleis atcrate::util::hash::hash_double.(b) Authorization / signing + sighash-binding requirement — HARD
Two authorization boundaries must BOTH hold and be cross-bound:
spend_auth_sig+ binding sig (no platform signature field). Transparent fields are bound viaextra_sighash_datafed intocompute_platform_sighash(packages/rs-dpp/src/shielded/mod.rs:111-117).signable_bytes(model:identity_create/identity_and_signatures/v0/mod.rs:21-42).Add a new helper
identity_create_from_shielded_extra_sighash_data(...)inpackages/rs-dpp/src/shielded/mod.rs(model:unshield_extra_sighash_data:161-166,shielded_withdrawal_extra_sighash_data:140-152) producing:Length-prefix the variable key list (unlike the withdrawal helper, whose
output_scriptis fixed-length). Call it from both the builder and the verifier.Penalty model: there is no asset lock to burn (unlike
IdentityCreate). A bad Orchard proof or id/key mismatch is simply rejected (transition invalid, no nullifier written) — theUnshieldmodel. Decide whether proof verification runs invalidate_shielded_proof(Unshield) or insidetransform_into_action(ShieldFromAssetLock); since there is no collateral to penalize, the Unshieldvalidate_shielded_proofplacement fits, BUT note Research finding 4 argues for keeping it intransform_into_actionso a bad proof can't be spammed cheaply against pool state — this placement is itself an open sub-decision to settle during implementation.(c) Fixed denominations
Restrict the exit to
{0.1, 0.3, 0.5, 1.0}DASH (versioned). Recommended: yes — a variable exit amount is a distinguishing fingerprint; a small fixed set maximizes the anonymity set (mirrorsShieldedTransfer's exact-fee uniformity). Tradeoff: clients must hold/select notes that cover a denomination, and the smallest denomination caps N keys (decision (b)/fee section). The set is versioned so it can grow without a hard fork beyond a protocol bump.(d) Change-note vs no-change
Unshield, which alreadyinsert_notesfor change (unshield_transition.rs:59). + The change note is an ordinary indistinguishable Orchard output. − Extra output action raisesnum_actions→ higher compute + note-storage cost.Recommended: allow a change note — it matches the established
Unshieldmechanics and avoids forcing exact-denomination note pre-splitting. Either way,value_balance(the net leaving the pool) must equal the denomination exactly; the change stays internal to the bundle.(e) Replay
Resolved: nullifiers provide replay protection; no outpoint needed. The converter writes each spent note's nullifier (
insert_nullifiers, usinginsert_only_known_to_not_already_exist_op);validate_nullifiers(shielded_common/mod.rs:278-316) rejects any nullifier already in state or duplicated intra-bundle (read-your-own-writes within the block). Re-submitting fails at the nullifier check. Because the id (decision (a), Option A) is derived from those same nullifiers, it is single-use by construction.unique_identifiers() = nullifier hexesgives mempool-level dedup.Implementation checklist
DPP
packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/(mirror theshield_from_asset_lock_transition/17-file layout:mod.rs,version.rs,fields.rs,state_transition_like.rs,state_transition_validation.rs,state_transition_estimated_fee_validation.rs,proved.rs,signing_tests.rs,methods/{mod,v0/mod}.rs,v0/{mod,types,version,v0_methods,state_transition_like,state_transition_validation,proved}.rs).pub mod identity_create_from_shielded_pool_transition;in.../shielded/mod.rs.StateTransitionenum + ALL macro/match arms inpackages/rs-dpp/src/state_transition/mod.rs: import +...Signable(~:151);call_method!arms (:160-209); identity-signed macros →None/no-op/CorruptedCodeExecution(:211-411); append the enum variant at TAIL afterShieldedWithdrawal(:451);active_version_range12..=LATEST shielded group (:535-539);is_identity_signedexclusion (:543);name→"IdentityCreateFromShieldedPool"(:588);signature→None;required_number_of_private_keys→0;user_fee_increase→0;owner_id/inputs→None;set_signature/set_user_fee_increaseno-ops.StateTransitionTypeenum (packages/rs-dpp/src/state_transition/state_transition_types.rs:20): addIdentityCreateFromShieldedPool = 20at TAIL, plusDisplay,TryFrom<u8>, all-variants list, and update the in-file roundtrip tests.extra_sighash_datahelper inpackages/rs-dpp/src/shielded/mod.rs(decision (b)).compute_shielded_identity_create_feeincompute_minimum_shielded_fee/{v0/mod.rs,mod.rs}+ new storage-byte constants inshielded/mod.rs(keyed ondpp.methods.compute_minimum_shielded_fee).PlatformSignablederive on the V0 struct; mirrorShieldFromAssetLock/Unshieldfield attributes. Add a "signable_bytes differ when public_keys / denomination differ" test.rs-drive
packages/rs-drive/src/state_transition_action/shielded/identity_create_from_shielded_pool/{mod,transformer,v0/mod,v0/transformer}.rs(carry builtIdentity/keys/balance, nullifiers,denomination,fee_amount); register in.../shielded/mod.rs.StateTransitionActionenum (packages/rs-drive/src/state_transition_action/mod.rs): import (~:35), appendIdentityCreateFromShieldedPoolAction(...)at TAIL after:109,user_fee_increasearm (:114).action_convert_to_operations/shielded/identity_create_from_shielded_pool_transition.rsemitting:insert_nullifiers+AddNewIdentity{ balance: denomination }+AddToSystemCredits(denomination)+ (optional)insert_noteschange +UpdateTotalBalance(pool − denomination). NOAddToSystemCreditsmistake: it IS needed here and equalsdenomination(see Conservation). Register in.../shielded/mod.rsand add a dispatch arm inaction_convert_to_operations/mod.rs:35. Add a new converter version keyidentity_create_from_shielded_pool_transitiontodrive_state_transition_method_versions.prove/prove_state_transition/v0/mod.rs) and strict verify arm (verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs) — see Execution proof. (Verify side builds under--features verify; check withcargo check -p drive --no-default-features --features verify.)rs-drive-abci
execution/validation/state_transition/state_transitions/identity_create_from_shielded_pool/{mod,tests,transform_into_action/{mod,v0/mod}}.rs(reuseshielded_commonhelpers +reconstruct_and_verify_bundle; do anchor/nullifier/pool checks, denomination check, key-structure + PoP validation, id re-derivation, fee computation). Register instate_transitions/mod.rs.execution/validation/state_transition/transformer/mod.rs(~:270).processor/traits/*.rs): add an arm to each —is_allowed.rs(true, gate onSHIELDED_POOL_INITIAL_PROTOCOL_VERSIONin both match sites),basic_structure.rs(call V0validate_structure),shielded_proof.rs(has_shielded_proof_validation/has_shielded_minimum_fee_validation/validate_shielded_proofarm withFLAGS_SPENDS_AND_OUTPUTS,value_balance = denomination as i64, newextra_sighash_data; newShieldedMinFeeKind::IdentityCreatetuple arm checkingdenomination ∈ setanddenomination >= compute_shielded_identity_create_fee(...)),state.rs/identity_based_signature.rs/identity_nonces.rs/address_*/identity_balance.rs(allfalse/not-identity-signed),advanced_structure_with_state.rs(decidetrueif key-uniqueness-in-state is done there).ExecutionEventvariantPaidFromShieldedPoolToNewIdentity { identity, operations, execution_operations, denomination, additional_fixed_fee_cost }inexecution/types/execution_event/mod.rs(append at tail) + acreate_from_state_transition_actionarm (after:555).validate_fees_of_event/v0/mod.rsarm (~:50): affordability checkdenomination >= total_fee→IdentityInsufficientBalanceError(or new error); updateno_fee_eventstests.execute_event/v0/mod.rs: classify the new variant inmaybe_fee_validation_result(~:367); mainmatch eventarm (after:624) that applies ops, meters, addsadditional_fixed_fee_costto processing, deductstotal_feefrom the new identity viainto_balance_change+apply_balance_change_from_fee_to_identity, returnsSuccessfulPaidExecution.rs-platform-version
shielded_identity_create_denominations: &'static [u64]toDriveAbciValidationConstants(drive_abci_validation_versions/mod.rs:27-53); populate inv8.rsevent_constants, empty in v1–v7.identity_create_from_shielded_pool_state_transition: DriveAbciStateTransitionValidationVersiontoDriveAbciStateTransitionValidationVersions(mod.rs:66); set per-version inv1.rs…v8.rs(disabled pre-v12,Some(0)/0in v8).identity_create_from_shielded_pool_state_transition: FeatureVersionBoundstoDPPStateTransitionSerializationVersions(dpp_state_transition_serialization_versions/{mod,v1,v2}.rs).drive_state_transition_method_versions/v2.rs.Consensus errors
ShieldedInvalidDenominationError(basic) — struct underpackages/rs-dpp/src/consensus/basic/state_transition/(cloneShieldedInvalidValueBalanceError); append toBasicErrorat TAIL (basic/basic_error.rs); code10827(next free; 10819–10826 used,codes.rs:236-243).ShieldedDenominationCannotCoverIdentityFeeError(state) — underpackages/rs-dpp/src/consensus/state/shielded/; append toStateErrorat TAIL (state/state_error.rs); code40905(next free,codes.rs:383-388). Otherwise reuseIdentityInsufficientBalanceError(zero new surface — recommended).From/code mappings incodes.rs.Proof prove/verify
StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec<u8>, bool)>)(append at tail,proof_result.rs:76). STRICT pattern only (PR feat(drive): shielded fees for Shield/ShieldFromAssetLock + shield credit conservation #379363c31f7c6e); add an empty-proof-returns-error test (cloneverify_unshield_empty_proof_returns_error).rs-sdk
packages/rs-sdk/src/platform/transition/identity_create_from_shielded_pool.rs(cloneunshield.rs: build via Orchard prover →ensure_valid_state_transition_structure→ broadcast, returns the new proof-result variant); register intransition/mod.rs. No new Fetch query is added, so theREADME.mdFetch/FetchMany checklist (:125-162) does not apply.wasm-dpp2
packages/wasm-dpp2/src/shielded/identity_create_from_shielded_pool_transition.rs(cloneunshield_transition.rs); declare inshielded/mod.rs; re-export inlib.rs:71.state_transitions/base/state_transition.rs: type-code map (:313,=> 20) + the five grouping matches that listUnshield(_)(:402,:429,:571,:648,:745).packages/wasm-dpp/src/state_transition/state_transition_factory.rs:84— add to the shielded group.FFI + platform-wallet
rs-platform-wallet: newpub async fn identity_create_from_shielded_pool(...)inwallet/shielded/operations.rs(cloneunshield/withdrawnote-reservation + build + broadcast); add aShieldedFeeKindvariant innote_selection.rsfor the denomination − fee reservation math; update module doc header.rs-platform-wallet-ffi: new#[no_mangle] extern "C" fn platform_wallet_manager_shielded_identity_create_from_pool(...)insrc/shielded_send.rs(cloneplatform_wallet_manager_shielded_unshield).swift-rust-ffi-engineeragent (per CLAUDE.md). Keep all business logic inrs-platform-walletbehind the FFI; Swift only persists/loads/bridges. No DAPI/.protochange is needed —broadcastStateTransitionis opaque bytes dispatched by variant.Book & docs
book/src/state-transitions/{lifecycle,validation-pipeline,transform-into-action,drive-operations}.md— add the transition to the per-transition tables.book/src/fees/shielded-fees.md— documenttotal_feecomposition + the denomination set.book/src/error-handling/{error-codes,consensus-errors}.md— add any new error codes.book/src/evo-sdk/state-transitions.md— add the SDK usage entry.Testing requirements
process_block_fees_and_validate_sum_trees_v0passes (:171-190): pool−= denomination, new identity balance== denomination − total_fee, fee pools+= total_fee,fee_refundsempty. Model on theShieldFromAssetLocksurplus block-level conservation test andstrategy_tests/test_cases/shielded_tests.rs(run_chain_unshield_transitions) +verify_state_transitions.rs. Also keep an op-level delta test (modelunshield_transition.rs:190-222).{10,30,50,100}×10⁹accepted; any other amount rejected with the denomination error;value_balance != denominationrejected.total_fee >= denominationrejection. Construct a case (large N and/or smallest denomination) where the metered + verification fee meets/exceeds the denomination; assert clean rejection (no nullifier written), not underflow.test_unshield_prove_and_verify_nullifiers_and_address). A padded-proof negative test that confirms the STRICT verifier rejects a proof with extra branches (the very thingverify_subset_query_with_absence_proofwould have accepted — Tighten Unshield & ShieldedWithdrawal execution proofs to a single strict merged query (parity with ShieldFromAssetLock) #3812).max_public_keys_in_creation; assert meteredtotal_feegrows monotonically with N and conservation holds at each.test_different_output_address_with_same_valid_bundle_is_rejected,unshield/tests.rs:688). Alsotest_valid_proof_with_mutated_value_balance_is_rejectedandtest_duplicate_nullifiers_in_same_bundleanalogs.test_minimum_shielded_fee_covers_actual_grovedb_write_cost(unshield_transition.rs:276-344): meter real GroveDB cost in estimation mode (apply=false) for the variable-N identity write and assertfee >= costandfee > storageso the booking-splitmin()never binds.IdentityCreate).Version gating & rollout (v12)
SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 12(feature_initial_protocol_versions.rs:4), the same gate as the entire shielded family.StateTransition::IdentityCreateFromShieldedPool(_)to bothhas_is_allowed_validationandvalidate_is_allowed(processor/traits/is_allowed.rs:35-39,:77-94); pre-v12 it returnsStateTransitionNotActiveError.active_version_rangeinstate_transition/mod.rsplaces it in the12..=LATEST_VERSIONshielded group.v8.rs/STATE_TRANSITION_SERIALIZATION_VERSIONS_V2/STATE_TRANSITION_VERSIONS_V3/DPP_METHOD_VERSIONS_V2, wired bypackages/rs-platform-version/src/version/v12.rs) and disabled in earlier version files.cargo check -p dppthen--workspace;cargo check -p drive --no-default-features --features verify;cargo fmt --allbefore committing.References
packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/,packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/unshield/, converterpackages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/unshield_transition.rs.packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/,.../identity_create_from_addresses_transition/, converter.../action_convert_to_operations/identity/identity_create_transition.rs, advanced structurepackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/advanced_structure/v0/mod.rs.surplus_outputbinding pattern):packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/,.../state_transitions/shield_from_asset_lock/transform_into_action/v0/mod.rs, strict verifypackages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs:1293-1507.63c31f7c6e) — the single STRICT merged-query verification pattern (GroveDb::verify_query_with_absence_proof,limit=Some(u16::MAX), partition-by-key) that this transition's proof must follow.Unshield) off subset verification; build this transition strict from day one to avoid that debt.