Shielded Transaction Fees
Introduced in protocol version 12. For the general fee system overview, see Fee System Overview. For address-based fees (protocol versions 10--11), see Platform Address Fees.
Shielded transactions use the Orchard protocol's zero-knowledge proofs to hide transaction amounts. Because amounts are hidden, the platform cannot inspect the transaction to compute fees the way it does for transparent transitions. Instead, the fee is embedded into the cryptographic structure of the bundle itself, and the platform enforces a minimum.
This chapter explains the fee model, how it is validated, and how it differs from the transparent and address-based fee systems.
The Problem: Fees in a Privacy System
In a transparent state transition like AddressFundsTransfer, the platform can see
the transfer amount, compute the cost of storage and processing, and deduct the fee
from the sender's balance. The fee calculation happens after the transition is
applied.
Shielded transitions break this model. The amounts inside the ZK proof are hidden. The platform cannot look inside the proof to determine how much was sent or received. It can only see two things from the public fields:
value_balance— the net amount leaving the shielded pool (positive means credits flow out of the pool into the transparent world or to proposers as fees)num_actions— the number of spend+output pairs in the bundle
The fee must therefore be encoded into value_balance by the client and validated
by the platform before execution.
Fee Extraction by Transition Type
The fee is derived differently depending on the shielded transition type:
| Transition | Fee Formula | Explanation |
|---|---|---|
| Shield | fee = metered(storage + processing) + shielded_verification_fee, paid from transparent address inputs | Charged on the transparent side (not from value_balance), on top of the shielded amount. The storage and processing of the note/nullifier writes are metered by GroveDB; only the ZK compute fee (proof + num_actions × per_action_processing) is added on top. Skipped by the value_balance-based shielded fee validation; enforced through the address-input fee path. See Entry-Transition Fees. |
| ShieldedTransfer | fee = value_balance (pinned to the minimum) | The entire value_balance is the fee and must equal compute_minimum_shielded_fee(num_actions) exactly (overpayment is rejected). Nothing leaves the pool except the fee. |
| Unshield | fee = compute_minimum_shielded_fee(num_actions) + unshield_address_storage_fee | value_balance (the transition's unshielding_amount) is the gross amount leaving the pool. The output address receives unshielding_amount − fee; validation requires unshielding_amount ≥ fee. Unshield also writes the net to the output platform address (AddBalanceToAddress), a real storage write priced on top of the base shielded minimum (unshield_address_storage_fee = 222 × per_byte_rate, ≈6.08M credits, flat regardless of action count — 222 bytes is the storage portion of the ≈6.24M metered address write) so the address write is covered and the proof fee isn't diverted to pay for it. See Per-Action Storage Fee. |
| ShieldedWithdrawal | fee = compute_minimum_shielded_fee(num_actions) + withdrawal_document_storage_fee | value_balance (unshielding_amount) is the gross amount leaving the pool. The Core withdrawal document receives unshielding_amount − fee (which must also clear MIN_WITHDRAWAL_AMOUNT). Unlike the other pool-paid transitions, ShieldedWithdrawal also writes a Core withdrawal document — a real document insert into the withdrawals contract plus its index entries (AddWithdrawalDocument), with a real metered cost of ≈110M credits that is flat regardless of action count. That cost is priced on top of the base shielded minimum as a flat ~4,100-byte storage component (withdrawal_document_storage_fee = 4100 × per_byte_rate), so the document write is covered and the proof-verification fee isn't diverted from the proposer to pay for it. See Per-Action Storage Fee. |
| ShieldFromAssetLock | pool_fee = compute_minimum_shielded_fee(num_actions) + asset_lock_base_cost, paid from the asset lock | The flat shielded minimum plus the asset-lock processing base cost is routed to the fee pools. Any remaining asset-lock value (the surplus) goes to an optional signed surplus_output platform address, or — if none is set — folds into the fee pools up to shielded_implicit_fee_cap. See Entry-Transition Fees. |
| IdentityCreateFromShieldedPool | total_fee = metered(insert_nullifiers + AddNewIdentity(identity + N keys)) + shielded_verification_fee, moved from the new identity's balance | value_balance is a fixed denomination (a member of the versioned set {0.1, 0.3, 0.5, 1.0} DASH) and must equal it EXACTLY. The new identity is created holding the full denomination, funded by decrementing the shielded pool by exactly that amount — a move between two balance trees (like Unshield's pool→address), so the global system-credit supply is unchanged (no AddToSystemCredits); the fee is then moved from that balance into the fee pools, so the identity ends with denomination − total_fee. Unlike the flat pool-paid transitions, the AddNewIdentity write grows with the key count, so the cost is metered (not a flat carve) — only the ZK compute fee (compute_shielded_verification_fee) is added on top, exactly like the transparent Shield. The client predicts it offline with compute_shielded_identity_create_fee(num_actions, num_keys); consensus rejects denomination < total_fee with IdentityInsufficientBalanceError. |
| ShieldFromIdentity | fee = metered(storage + processing) + shielded_verification_fee, paid from the funding identity's balance | Identity balance to pool (protocol version 14). Charged exactly like Shield, but on the identity side: the identity signature covers the whole outputs-only bundle, the metered note writes and identity writes go through the standard identity-paid path (IdentityCreditTransferToAddresses model), and only the ZK compute fee is added as additional_fixed_fee_cost. user_fee_increase applies. The identity must hold amount + fee; consensus rejects a short balance with IdentityInsufficientBalanceError. The pool and the identity are both balance trees, so no system-credit adjustment is emitted. See Entry-Transition Fees. |
| IdentityTopUpFromShieldedPool | fee = compute_shielded_identity_top_up_fee(num_actions) = compute_minimum_shielded_fee(num_actions) + identity_balance_storage_fee, carved from value_balance | Shielded pool to an EXISTING identity's balance (protocol version 14). value_balance (the transition's topUpAmount) is the gross amount leaving the pool; the identity receives topUpAmount - fee and validation requires topUpAmount >= fee. Same flat pool-paid model as Unshield, with the identity balance write as a flat component built like Unshield's address write but calibrated to its measured cost: the top-up rewrites the existing identity's balance element and its Merk path (320 replaced bytes, 175,320 credits of processing, no storage), folded into one flat figure with headroom like the other shielded components, so identity_balance_storage_fee = 8 x per_byte_rate (SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES). The target identity and gross amount are bound into the Orchard sighash; the identity must already exist; no system-credit adjustment. |
Token shielded pool fees
Token pools (protocol version 14, see Token Shielded Pools)
hold tokens, and tokens cannot pay fees, so none of the three token pool transitions carves a
fee from the bundle. They are TokenTransition variants inside a Batch, and the batch's
signing identity pays in credits through the standard identity-paid path.
| Transition | Fee Formula | Explanation |
|---|---|---|
| TokenShield | fee = metered(storage + processing) + shielded_verification_fee, paid by the signing identity | Same model as ShieldFromIdentity: the dummy nullifier inserts, the note appends, the identity token balance write and the pool balance write are metered, and compute_shielded_verification_fee(num_actions) is added as a precalculated operation before the proof is verified. value_balance is -amount in tokens and carries no fee. |
| TokenUnshield | fee = metered(storage + processing) + shielded_verification_fee, paid by the signing identity | value_balance equals the unshielded token amount exactly; the recipient receives the full amount. Nullifier inserts, note appends and the two balance writes are metered. |
| TokenShieldedTransfer | fee = metered(storage + processing) + shielded_verification_fee, paid by the signing identity | value_balance is exactly zero; consensus rejects any other value. Only the nullifier inserts and note appends are metered. |
| TokenMintToPool, TokenClaimToPool, TokenDirectPurchaseToPool | same model | Outputs-only bundles; the dummy nullifier inserts, the note appends and the supply and pool balance writes are metered, the verification fee is charged by the action transformer so CheckTx and block execution price the bundle identically. |
| TokenBurnFromPool | same model | Nullifier inserts, change note appends and the supply and pool balance writes are metered. |
| Document with a TokenPaymentInfo::V1 | same model, on top of the document's own fee | The shielded payment bundle's verification fee is added by the batch transformer before anything can fail; the pool writes it causes are metered. |
Because the verification fee is charged before the proof is checked, an invalid proof is a paid failure: the identity is charged, its identity contract nonce advances, and no token moves.
Identity-less token pool transitions
TokenShieldedTransferWithShieldedFee (26), TokenUnshieldWithShieldedFee (27) and
TokenPurchaseFromShieldedPool (28) have no identity: the fee is carved from a second bundle
spent in the credit shielded pool, exactly as ShieldedTransfer carves its own fee.
| Transition | Fee Formula | Explanation |
|---|---|---|
| TokenShieldedTransferWithShieldedFee | credit_amount == base(fee_actions) + base(token_actions) | Two bundles are verified and stored, so the fee is the base shielded fee of each (compute_token_pool_paid_shielded_fee). Pure fee: overpayment is rejected. |
| TokenUnshieldWithShieldedFee | credit_amount == base(fee_actions) + base(token_actions) + identity balance bytes | Adds the flat storage of the recipient's token balance item. Pure fee. |
| TokenPurchaseFromShieldedPool | credit_amount == total_agreed_price + base(fee_actions) + base(token_actions) + balance write + supply bytes | The agreed price rides on top of the fee and is credited to the contract owner; credit_amount - total_agreed_price must equal the fee exactly. |
The fee bundle's value balance is credit_amount; the minimum-fee validation, the SDK builders
and the transformer all use the same compute_token_* function so the threshold never drifts
from what is carved. An invalid proof or a spent nullifier is an unpaid rejection: nothing is
committed and no fee is charged, the same as for the credit pool's pool-paid transitions.
For ShieldedTransfer, the client constructs the bundle so that total_spent − total_output = desired_fee. The Orchard circuit proves that value is conserved
(inputs = outputs + value_balance), and the binding signature cryptographically
commits to the value_balance. Mutating value_balance after signing will cause
the binding signature to fail verification.
Entry-Transition Fees (Shield, ShieldFromAssetLock, and ShieldFromIdentity)
The entry transitions — Shield (transparent → shielded), ShieldFromAssetLock
(Core asset lock → shielded), and, from protocol version 14, ShieldFromIdentity
(identity balance → shielded) — move value into the pool, so
there is no spent note from which value_balance could carry a fee. Their fees are
therefore charged from the funding side, and both cover the same Halo 2 proof
verification and per-action work the other shielded transitions pay for — but they
account for it differently. Shield debits a state-queryable transparent address
balance, so GroveDB meters its real storage/processing and only the compute portion
(compute_shielded_verification_fee, no storage term) is added on top. ShieldFromAssetLock
is funded by a consumed asset lock with no metering anchor, so it pays the flat
compute_minimum_shielded_fee(num_actions) (plus the asset-lock base cost). num_actions
is the on-wire action count of the bundle (a single-output, spends-disabled Orchard bundle
pads to 2 actions, so the minimum is the 2-action fee).
Every action of an outputs-only bundle still reveals a nullifier, that of a dummy spend,
which becomes the new note's rho. From protocol version 14 the entry transitions record
those nullifiers and refuse one repeated inside the bundle or already recorded
(NullifierAlreadySpentError), as the spends do, so every revealed nullifier is recorded
once. Shield and ShieldFromIdentity meter the nullifier writes like the rest of their
storage; ShieldFromAssetLock's flat fee already prices a note and a nullifier write per
action (see Per-Action Storage Fee).
Shield
Shield is charged like any other address-funded transition: GroveDB meters the
real storage and processing cost of applying it (the note-commitment and nullifier
writes plus the address-balance updates), and the shielded compute fee is added on
top:
fee = metered_storage + metered_processing + shielded_verification_fee
shielded_verification_fee = proof_verification_fee + num_actions × per_action_processing_fee
shielded_verification_fee is the ZK-verification cost (Halo 2 proof + per-action spend-auth
verification) that GroveDB metering cannot see. It is added as the transition's
additional_fixed_fee_cost — exactly the mechanism IdentityCreateFromAddresses uses
for its registration cost. It carries no storage term: storage comes entirely from
metering, so it is never double-counted. The address inputs must cover shield_amount + fee, and the booked storage/processing equals the deducted amount, so credits are
conserved by the standard machinery (no special-case override).
Shield is skipped by the value_balance-based minimum-fee validation (its
value_balance is the amount entering the pool, not a fee). The stateless structure
floor requires only shield_amount + shielded_verification_fee (a conservative lower bound,
since metered storage is unknowable without state); the authoritative metered + compute funding gate is validate_fees_of_event.
ShieldFromIdentity
ShieldFromIdentity (protocol version 14) is Shield with the identity balance as
the funding side. It is identity-signed (TRANSFER key, identity nonce) like
IdentityCreditTransferToAddresses, and carries the same outputs-only Orchard bundle
as Shield. The fee model is identical to Shield's: GroveDB meters the note and
nullifier inserts and the identity balance and nonce writes, and the shielded compute fee is
added as additional_fixed_fee_cost:
fee = metered_storage + metered_processing + shielded_verification_fee
identity_balance_after = identity_balance_before - amount - fee
user_fee_increase applies to the metered processing portion. The stateless
floor requires identity_balance >= amount + compute_shielded_identity_balance_write_fee,
the conservative complete admission estimate: compute_minimum_shielded_fee, plus
PV14's versioned shielded_identity_action_write_storage_bytes (400 effective
bytes per action) and shielded_identity_balance_write_storage_bytes (500 flat
bytes), each priced at the storage rate. The allowances cover the complete
execution-event estimate: note/nullifier writes at the estimator's depth 16,
identity nonce/balance writes at its maximum-element depth, and the signature
and state-read validation context. They are admission reserves, not physical
payload sizes or changes to the actual metered charge. For two actions the
base wallet estimate is 114,140,000 + (2 × 400 + 500) × 27,400 = 149,760,000
credits. Historical tables preserve the preceding zero per-action and 20-byte
flat allowance. An identity that could not pay the
complete fee is therefore refused before the Orchard proof is verified. The authoritative gate is the identity-paid fee
validation of the execution event (Paid), which rejects with
IdentityInsufficientBalanceError. The identity balance and the pool total are
both terms of the block conservation equation, so the converter emits no
AddToSystemCredits (the same rule Unshield and IdentityCreateFromShieldedPool
follow).
A failed Orchard proof is a paid failure, not a free rejection. Like
ShieldFromAssetLock, the proof is verified inside the transition's own transform
rather than in the shared stateless proof step: on failure the transition executes
as a BumpIdentityNonceAction, consuming the identity nonce and charging the
identity the versioned shielded_proof_verification_failure penalty on top of the
processing metered so far. This closes the path where a funded identity could
resubmit invalid proofs indefinitely with its nonce and balance left untouched.
CheckTx never charges; it admits the verification under its node-local proof
budget only after the cheap checks passed, and rejects on failure.
ShieldFromAssetLock
The asset lock funds the pool, so the fee is taken from the consumed asset-lock value. The pool fee is:
pool_fee = compute_minimum_shielded_fee(num_actions) + asset_lock_base_cost
asset_lock_base_cost is the same asset-lock-proof processing base cost charged to
every asset-lock-funded transition (e.g. IdentityCreate):
required_asset_lock_duff_balance_for_processing_start_for_address_funding
(50,000 duffs) × CREDITS_PER_DUFF (1,000) = 50,000,000 credits. Adding it makes
the ShieldFromAssetLock pool fee strictly greater than the bare F paid by the
transparent Shield, pricing the extra cost of verifying the Core asset-lock proof.
The asset lock must cover shield_amount + pool_fee; the remainder is the surplus:
surplus = consumed_asset_lock_value − shield_amount − pool_fee (always ≥ 0)
The surplus is disposed of in one of two ways:
surplus_outputset — the transition carries an optionalOption<PlatformAddress>surplus_output. When present,surplusis credited to that platform address (via anAddBalanceToAddressdrive operation). This field is part of the signed payload (it sits before thesignaturefield, which alone is excluded from the sighash), so a surplus recipient cannot be substituted or truncated after signing.surplus_outputunset — the surplus folds into the fee pools, but only up toshielded_implicit_fee_cap(20,000,000,000 credits = 0.2 DASH, a versioned constant). If the unclaimed surplus would exceed the cap, the transition is rejected withShieldedImplicitFeeCapExceededErrorso a client cannot accidentally donate a large remainder to proposers. To intentionally over-fund, the client must setsurplus_output(which has no cap).
Value conservation across the whole transition is exact:
consumed_asset_lock_value = shield_amount + surplus_amount + fee_amount
where surplus_amount is surplus when surplus_output is set and 0 otherwise (in
which case the surplus is part of fee_amount).
The Three-Component Fee Model
The minimum shielded fee has three components:
min_fee = proof_verification_fee + num_actions × (processing_fee + storage_fee)
1. Proof Verification Fee (per bundle)
A single Halo 2 ZK proof covers the entire bundle regardless of action count. The bundle's base verification work is benchmarked at approximately 5 ms. This is a fixed cost per bundle.
Protocol version 14 value: 40,000,000 credits (40M)
2. Per-Action Processing Fee
The per-action processing fee prices the marginal Halo 2 verification work that each additional action adds to the bundle (≈1.1 ms/action measured against a ≈5 ms bundle base): a bundle with more actions is a larger circuit and a longer batch verification. For a spend-bearing action that marginal work includes:
- RedPallas spend authorization signature verification
- Nullifier duplicate check (hash + tree lookup)
- Note commitment insertion into the Sinsemilla-based Merkle tree
Output-only entry transitions (Shield / ShieldFromAssetLock / ShieldFromIdentity) do no spends, but each output action still enlarges the proof and so carries the same per-action processing charge — this fee tracks the marginal verification work, not a fixed per-action checklist. From protocol version 14 it also prices the check of the nullifier each of their actions reveals.
At protocol version 14, the proof and per-action fees are versioned independently. Their numerical ratio is about 1.8:1 (40M : 22M).
Current value: 22,000,000 credits (22M)
3. Per-Action Storage Fee
Each action permanently stores data in two places:
| Storage | Bytes | Contents |
|---|---|---|
| BulkAppendTree (commitment tree) | 312 | 32 cmx + 32 rho + 32 cv_net + 216 encrypted note |
| Nullifier tree | 32 | nullifier key (value is empty) |
| Total physical payload | 344 |
Protocol version 14 prices a 550-byte allowance per action, covering the physical payload and database framing, at the platform's per-byte storage rates:
storage_fee_per_action = 550 × (storage_disk_usage_credit_per_byte
+ storage_processing_credit_per_byte)
= 550 × (27,000 + 400)
= 550 × 27,400
= 15,070,000
The byte allowance is versioned. Its per-byte rates come from the storage fee version, so the fee tracks changes to those rates.
Fee Table
Combining all three components at protocol version 14:
| Actions | Proof Fee | Processing | Storage | Total Minimum Fee |
|---|---|---|---|---|
| 2 | 40,000,000 | 44,000,000 | 30,140,000 | 114,140,000 |
| 3 | 40,000,000 | 66,000,000 | 45,210,000 | 151,210,000 |
| 4 | 40,000,000 | 88,000,000 | 60,280,000 | 188,280,000 |
Note: The Orchard protocol requires a minimum of 2 actions per bundle for privacy (even a single-input single-output transfer produces 2 actions with a dummy padding action). Bundles with 1 action are structurally invalid.
The totals above are the base compute_minimum_shielded_fee and apply directly to
ShieldedTransfer. The three pool-paid transitions that write one extra per-transition output
add a flat component on top of this base:
Unshieldadds the output-address write cost: a flatunshield_address_storage_fee = 222 × per_byte_rate = 222 × 27,400 = 6,082,800credits, independent of action count. So the 2-action Unshield fee is114,140,000 + 6,082,800 = 120,222,800credits (and likewise+6,082,800at every action count). See the Fee Extraction Unshield row for why this component exists.ShieldedWithdrawaladds the Core withdrawal-document storage cost: a flatwithdrawal_document_storage_fee = 4100 × per_byte_rate = 4100 × 27,400 = 112,340,000credits, independent of action count. So the 2-action ShieldedWithdrawal fee is114,140,000 + 112,340,000 = 226,480,000credits (and likewise+112,340,000at every action count). See the Fee Extraction ShieldedWithdrawal row for why this component exists.IdentityTopUpFromShieldedPooladds the identity balance write cost: a flatidentity_balance_storage_fee = 8 × per_byte_rate = 8 × 27,400 = 219,200credits, independent of action count, so the top-up fee at any action count is the base plus219,200. See the Fee Extraction IdentityTopUpFromShieldedPool row for why this component is so much smaller than the address write: it rewrites an existing balance element instead of storing a new entry.
Where Fee Validation Runs
Fee validation is integrated into the processor pipeline (see Validation Pipeline) between basic structure validation and ZK proof verification:
... → Basic Structure → Minimum Fee Check → ZK Proof Verification → ...
The ordering is deliberate. The fee check is stateless and cheap — it only reads
value_balance and actions.len() from the transition, with no GroveDB lookups.
Placing it before proof verification means that bundles with insufficient fees are
rejected instantly, without spending ~100ms on Halo 2 verification.
The implementation lives in
packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs:
#![allow(unused)] fn main() { pub(crate) trait StateTransitionShieldedMinimumFeeValidationV0 { fn validate_minimum_shielded_fee( &self, platform_version: &PlatformVersion, ) -> Result<SimpleConsensusValidationResult, Error>; } }
If the fee is below the minimum, the transition is rejected with
InsufficientShieldedFeeError — an unpaid consensus error. The sender is not
charged (there is no identity to charge), and the transition produces no execution
event.
Fee Constants in the Version System
The fee parameters are stored in the platform version under
drive_abci.validation_and_processing.event_constants:
#![allow(unused)] fn main() { pub struct DriveAbciValidationConstants { pub maximum_vote_polls_to_process: u16, pub maximum_contenders_to_consider: u16, pub minimum_pool_notes_for_outgoing: u64, pub shielded_anchor_retention_blocks: u64, pub shielded_anchor_pruning_interval: u64, pub shielded_proof_verification_fee: u64, // 40_000_000 at protocol 14 pub shielded_per_action_processing_fee: u64, // 22_000_000 pub shielded_storage_bytes_per_action: u64, // 550 at protocol 14 pub shielded_implicit_fee_cap: u64, // 20_000_000_000 (0.2 DASH) } }
The shielded_implicit_fee_cap bounds the surplus that a ShieldFromAssetLock may
implicitly donate to the fee pools when no surplus_output is set (see
Entry-Transition Fees).
The storage component is derived at runtime from
fee_version.storage.storage_disk_usage_credit_per_byte and
fee_version.storage.storage_processing_credit_per_byte, multiplied by the
versioned shielded_storage_bytes_per_action allowance.
This design means:
- Proof and processing fees can be tuned independently via version bumps
- Storage fees automatically track changes to the platform-wide storage rates
- Storage allowances can be calibrated independently of the per-byte rates
How Fees Flow After Validation
Once the fee check passes and the transition is fully validated and executed, the
shielded pool's total balance is decremented and the fee is booked via the
PaidFromShieldedPool execution event:
ShieldedTransfer: pool_balance -= fee_amount // fee == value_balance
Unshield: pool_balance -= unshielding_amount // gross
ShieldedWithdrawal: pool_balance -= unshielding_amount // gross
IdentityTopUpFromShieldedPool: pool_balance -= top_up_amount // gross
For Unshield and ShieldedWithdrawal, unshielding_amount is the gross amount
leaving the pool. Of that, unshielding_amount − fee_amount is credited to the output
platform address (Unshield) or written into the Core withdrawal document
(ShieldedWithdrawal), and fee_amount is booked as the transition fee. For Unshield
that fee is compute_shielded_unshield_fee — the base fee plus the flat
AddBalanceToAddress output-write storage cost (+6,082,800 credits), since Unshield
also writes the net to a transparent platform address. For ShieldedWithdrawal it is
compute_shielded_withdrawal_fee — the same base fee plus the flat Core
withdrawal-document storage cost (+112,340,000 credits), since ShieldedWithdrawal also
writes a real document into the withdrawals contract. Validation guarantees
unshielding_amount ≥ fee_amount (and, for ShieldedWithdrawal, that the net also clears
MIN_WITHDRAWAL_AMOUNT), so the subtraction never underflows. Because each fee prices its
extra write, the booking split (storage routed to the storage pool, the remainder paid to
the proposer) covers that write instead of zeroing the proposer's processing reward to
cover it.
For IdentityTopUpFromShieldedPool, top_up_amount is likewise the gross amount leaving the
pool: top_up_amount − fee_amount is added to the existing identity's balance and
fee_amount (compute_shielded_identity_top_up_fee, the base fee plus the flat identity
balance write component) is booked as the transition fee; validation guarantees
top_up_amount ≥ fee_amount. The identity balance and the pool total are both terms of the
block conservation equation, so no system-credit adjustment is emitted.
For ShieldedTransfer, the pool decreases by exactly the fee (the sender's notes are
spent and the recipient's notes are created, but the pool's aggregate balance only drops
by the fee).
In all cases the booked fee_amount is split the same way as every other transition's
fee: the storage cost of the permanent shielded writes is routed to the storage pool
(amortized across epochs and subject to the per-epoch fee multiplier at payout), and the
remainder — proof verification plus per-action processing — is the processing fee paid to
the current block proposer.
The three entry transitions (Shield, ShieldFromAssetLock, ShieldFromIdentity) do not decrement the pool (they add to it), so their fees are
booked from the funding side instead:
Shield: fee_amount = metered + shielded_verification_fee // from transparent address inputs
ShieldFromAssetLock: fee_amount = pool_fee (+ unclaimed surplus) // from the consumed asset lock
For Shield, the fee is deducted from the transparent address inputs and booked through
the standard PaidFromAddressInputs event (deducted == booked, no override): metered
storage and processing, plus the shielded_verification_fee folded into processing. For
ShieldFromAssetLock, the consumed asset-lock value is partitioned into shield_amount
(into the pool), surplus_amount (to surplus_output, or 0), and fee_amount (to the
fee pools); see Entry-Transition Fees.
Cryptographic Binding
The fee is not just a field that the platform trusts. It is cryptographically bound to the ZK proof through two mechanisms:
-
Value commitments (cv_net): Each action contains a Pedersen commitment to the note value. The sum of all value commitments must equal
value_balance(modulo the blinding factors). The binding signature proves this relationship holds. -
Platform sighash: The bundle commitment (which includes
value_balance) is hashed into the sighash that the spend authorization signatures sign over:sighash = SHA-256("DashPlatformSighash" || bundle_commitment || extra_data)Mutating
value_balanceafter signing changes the sighash, invalidating all signatures. TheBatchValidatorchecks both the Halo 2 proof and all signatures, so any tampering is caught.
This means a client cannot claim a lower fee than what the ZK proof actually commits to — the proof and signatures would fail verification.
Rules and Guidelines
Do:
- Always set
value_balanceto at least the minimum fee when building a shielded bundle on the client side. Usemin_fee = proof_verification_fee + num_actions × (processing_fee + storage_fee)with the current platform version constants. - Include the fee in the note arithmetic:
total_spent = total_output + fee. The Orchard builder handles this when you set the output amount tospend_amount − desired_fee. - Remember that the minimum action count is 2 (Orchard privacy requirement).
Do not:
- Assume the fee is free for
Shieldtransitions — the fee comes from transparent address inputs and is validated through the address balance system, not here. - Mutate
value_balanceafter building the bundle. The binding signature and sighash will be invalidated. - Hardcode fee amounts. Always read from
PlatformVersion— the constants are versioned and will change as the protocol evolves.