Skip to main content

dpp/shielded/compute_minimum_shielded_fee/
mod.rs

1mod v0;
2
3use crate::fee::Credits;
4use crate::shielded::{
5    SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES,
6    SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES,
7};
8use crate::ProtocolError;
9use platform_version::version::PlatformVersion;
10use v0::compute_minimum_shielded_fee_v0;
11use v0::compute_shielded_identity_balance_write_fee_v0;
12use v0::compute_shielded_identity_create_fee_v0;
13use v0::compute_shielded_identity_top_up_fee_v0;
14use v0::compute_shielded_unshield_fee_v0;
15use v0::compute_shielded_verification_fee_v0;
16use v0::compute_shielded_withdrawal_fee_v0;
17
18/// Computes the minimum **flat** fee (in credits) for a pool-paid / asset-lock shielded
19/// transition.
20///
21/// Dispatches on the platform-versioned `dpp.methods.compute_minimum_shielded_fee` so the
22/// fee formula can evolve across protocol versions without breaking older ones.
23///
24/// This is the **base** flat shielded fee for the pool-paid / asset-lock transitions whose storage
25/// cannot be metered against an address balance. ShieldedTransfer and ShieldFromAssetLock charge
26/// exactly this base (ShieldFromAssetLock adds the asset-lock base cost on the asset-lock side); the
27/// other two pool-paid transitions add one flat per-transition storage component on top of this
28/// base — Unshield via [`compute_shielded_unshield_fee`] (the `AddBalanceToAddress` output write)
29/// and ShieldedWithdrawal via [`compute_shielded_withdrawal_fee`] (the Core withdrawal document).
30/// For each transition, its SDK builder, its transformer (for the fee actually carved from the
31/// pool), and the consensus gate `validate_minimum_shielded_fee` all call the SAME one of these
32/// functions, so the carved fee and the validation threshold can never drift.
33///
34/// The transparent `Shield` is the exception: it meters its note/nullifier storage via GroveDB and
35/// adds only the COMPUTE portion via the sibling [`compute_shielded_verification_fee`] (which carries no
36/// storage term). Both functions dispatch on the SAME version key, so the flat fee and the compute
37/// fee always evolve together and cannot drift.
38///
39/// # Parameters
40/// - `num_actions` — number of Orchard actions in the bundle
41/// - `platform_version` — protocol version (determines the formula version and fee constants)
42pub fn compute_minimum_shielded_fee(
43    num_actions: usize,
44    platform_version: &PlatformVersion,
45) -> Result<Credits, ProtocolError> {
46    match platform_version.dpp.methods.compute_minimum_shielded_fee {
47        0 => compute_minimum_shielded_fee_v0(num_actions, platform_version),
48        version => Err(ProtocolError::UnknownVersionMismatch {
49            method: "compute_minimum_shielded_fee".to_string(),
50            known_versions: vec![0],
51            received: version,
52        }),
53    }
54}
55
56/// Computes the **ShieldedWithdrawal** fee (in credits): [`compute_minimum_shielded_fee`] PLUS the
57/// flat storage cost of the Core withdrawal document a `ShieldedWithdrawal` inserts.
58///
59/// A `ShieldedWithdrawal` additionally writes a Core withdrawal document into the withdrawals
60/// contract (the document plus its index entries — `AddWithdrawalDocument`), a real,
61/// GroveDB-metered insert (≈110M credits, FLAT regardless of action count) that
62/// [`compute_minimum_shielded_fee`] does NOT price. This function adds that document cost as a
63/// flat `SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES`-byte storage component (priced at the same
64/// per-byte storage rate the per-action note storage uses).
65///
66/// Used ONLY by `ShieldedWithdrawal`: its SDK builder, the withdrawal transformer (for the fee
67/// carved from the pool), and the consensus gate `validate_minimum_shielded_fee` all call this
68/// function, so the carved fee and the validation threshold can never drift. ShieldedTransfer keeps
69/// using [`compute_minimum_shielded_fee`], Unshield uses [`compute_shielded_unshield_fee`], and the
70/// entry transitions use [`compute_minimum_shielded_fee`] / [`compute_shielded_verification_fee`].
71///
72/// Dispatches on the SAME version key (`dpp.methods.compute_minimum_shielded_fee`) as
73/// [`compute_minimum_shielded_fee`] so the two formulas evolve together across protocol versions.
74///
75/// # Parameters
76/// - `num_actions` — number of Orchard actions in the bundle
77/// - `platform_version` — protocol version (determines the formula version and fee constants)
78pub fn compute_shielded_withdrawal_fee(
79    num_actions: usize,
80    platform_version: &PlatformVersion,
81) -> Result<Credits, ProtocolError> {
82    match platform_version.dpp.methods.compute_minimum_shielded_fee {
83        0 => compute_shielded_withdrawal_fee_v0(num_actions, platform_version),
84        version => Err(ProtocolError::UnknownVersionMismatch {
85            method: "compute_shielded_withdrawal_fee".to_string(),
86            known_versions: vec![0],
87            received: version,
88        }),
89    }
90}
91
92/// Computes the flat fee for `IdentityTopUpFromShieldedPool` (base minimum plus the flat
93/// identity-balance write component).
94pub fn compute_shielded_identity_top_up_fee(
95    num_actions: usize,
96    platform_version: &PlatformVersion,
97) -> Result<Credits, ProtocolError> {
98    match platform_version.dpp.methods.compute_minimum_shielded_fee {
99        0 => compute_shielded_identity_top_up_fee_v0(num_actions, platform_version),
100        version => Err(ProtocolError::UnknownVersionMismatch {
101            method: "compute_shielded_identity_top_up_fee".to_string(),
102            known_versions: vec![0],
103            received: version,
104        }),
105    }
106}
107
108/// Computes the **Unshield** fee (in credits): [`compute_minimum_shielded_fee`] PLUS the flat
109/// storage cost of the single `AddBalanceToAddress` write an `Unshield` performs.
110///
111/// An `Unshield` additionally credits the net (`unshielding_amount − fee`) to the output platform
112/// address via `AddBalanceToAddress`, a real, GroveDB-metered write (≈6.24M credits, FLAT
113/// regardless of action count) that [`compute_minimum_shielded_fee`] does NOT price. This function
114/// adds that address cost as a flat `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES`-byte storage
115/// component (priced at the same per-byte storage rate the per-action note storage uses).
116///
117/// Used ONLY by `Unshield`: its SDK builder, the unshield transformer (for the fee carved from the
118/// pool), and the consensus gate `validate_minimum_shielded_fee` all call this function, so the
119/// carved fee and the validation threshold can never drift. ShieldedTransfer, ShieldedWithdrawal,
120/// and the entry transitions keep using [`compute_minimum_shielded_fee`] /
121/// [`compute_shielded_withdrawal_fee`] / [`compute_shielded_verification_fee`].
122///
123/// Dispatches on the SAME version key (`dpp.methods.compute_minimum_shielded_fee`) as
124/// [`compute_minimum_shielded_fee`] so the two formulas evolve together across protocol versions.
125///
126/// # Parameters
127/// - `num_actions` — number of Orchard actions in the bundle
128/// - `platform_version` — protocol version (determines the formula version and fee constants)
129pub fn compute_shielded_unshield_fee(
130    num_actions: usize,
131    platform_version: &PlatformVersion,
132) -> Result<Credits, ProtocolError> {
133    match platform_version.dpp.methods.compute_minimum_shielded_fee {
134        0 => compute_shielded_unshield_fee_v0(num_actions, platform_version),
135        version => Err(ProtocolError::UnknownVersionMismatch {
136            method: "compute_shielded_unshield_fee".to_string(),
137            known_versions: vec![0],
138            received: version,
139        }),
140    }
141}
142
143/// Computes the conservative **admission floor** (in credits) for `ShieldFromIdentity`:
144/// [`compute_minimum_shielded_fee`] plus the versioned per-action and flat
145/// identity-write allowances, priced at the per-byte storage rate. The allowances
146/// cover the complete execution-event admission estimate, including the estimated
147/// note/nullifier and identity writes and the validation context.
148///
149/// The transition's authoritative fee is metered at execution; this floor stands in for it where
150/// state is not yet available, so that an identity that could not pay the complete fee is refused
151/// before the expensive Orchard proof verification runs. It is also the client-side estimate of
152/// the total fee.
153///
154/// Dispatches on the SAME version key (`dpp.methods.compute_minimum_shielded_fee`) as
155/// [`compute_minimum_shielded_fee`] so the formulas evolve together across protocol versions.
156///
157/// # Parameters
158/// - `num_actions`: number of Orchard actions in the bundle
159/// - `platform_version`: protocol version (determines the formula version and fee constants)
160pub fn compute_shielded_identity_balance_write_fee(
161    num_actions: usize,
162    platform_version: &PlatformVersion,
163) -> Result<Credits, ProtocolError> {
164    match platform_version.dpp.methods.compute_minimum_shielded_fee {
165        0 => compute_shielded_identity_balance_write_fee_v0(num_actions, platform_version),
166        version => Err(ProtocolError::UnknownVersionMismatch {
167            method: "compute_shielded_identity_balance_write_fee".to_string(),
168            known_versions: vec![0],
169            received: version,
170        }),
171    }
172}
173
174/// Computes the **compute-only** shielded fee (in credits): the ZK-compute portion (Halo 2 proof
175/// verification + per-action spend-auth/nullifier processing) that GroveDB metering cannot see.
176///
177/// Unlike [`compute_minimum_shielded_fee`] this carries **no storage term**. It is used by the
178/// transparent `Shield`, which meters its note/nullifier storage writes via GroveDB and adds only
179/// this compute fee on top (as the event's `additional_fixed_fee_cost`), so storage is never
180/// double-counted.
181///
182/// Dispatches on the SAME version key (`dpp.methods.compute_minimum_shielded_fee`) as
183/// [`compute_minimum_shielded_fee`] so the two formulas evolve together across protocol versions.
184///
185/// # Parameters
186/// - `num_actions` — number of Orchard actions in the bundle
187/// - `platform_version` — protocol version (determines the formula version and fee constants)
188pub fn compute_shielded_verification_fee(
189    num_actions: usize,
190    platform_version: &PlatformVersion,
191) -> Result<Credits, ProtocolError> {
192    match platform_version.dpp.methods.compute_minimum_shielded_fee {
193        0 => compute_shielded_verification_fee_v0(num_actions, platform_version),
194        version => Err(ProtocolError::UnknownVersionMismatch {
195            method: "compute_shielded_verification_fee".to_string(),
196            known_versions: vec![0],
197            received: version,
198        }),
199    }
200}
201
202/// Computes the **IdentityCreateFromShieldedPool** fee (in credits): [`compute_minimum_shielded_fee`]
203/// PLUS the variable storage cost of the `AddNewIdentity` write (identity record + balance +
204/// revision + N key subtrees), which scales with the number of public keys.
205///
206/// Unlike the flat per-transition components of [`compute_shielded_unshield_fee`] /
207/// [`compute_shielded_withdrawal_fee`], the identity write grows monotonically with the key count.
208/// This is the **client-side predictor** + the **cheap floor** the `denomination >= min_fee` gate
209/// uses; the authoritative consensus fee is METERED by GroveDB at execution (the transition's
210/// `ExecutionEvent` meters its ops and adds only the compute fee via `additional_fixed_fee_cost`).
211///
212/// Dispatches on the SAME version key (`dpp.methods.compute_minimum_shielded_fee`) as
213/// [`compute_minimum_shielded_fee`] so the formulas evolve together across protocol versions.
214///
215/// # Parameters
216/// - `num_actions` — number of Orchard actions in the bundle
217/// - `num_keys` — number of public keys the new identity is created with
218/// - `platform_version` — protocol version (determines the formula version and fee constants)
219pub fn compute_shielded_identity_create_fee(
220    num_actions: usize,
221    num_keys: usize,
222    platform_version: &PlatformVersion,
223) -> Result<Credits, ProtocolError> {
224    match platform_version.dpp.methods.compute_minimum_shielded_fee {
225        0 => compute_shielded_identity_create_fee_v0(num_actions, num_keys, platform_version),
226        version => Err(ProtocolError::UnknownVersionMismatch {
227            method: "compute_shielded_identity_create_fee".to_string(),
228            known_versions: vec![0],
229            received: version,
230        }),
231    }
232}
233
234/// Computes the fee of an identity-less token pool transition (in credits): two bundles are
235/// verified and stored, so it is [`compute_minimum_shielded_fee`] of the fee bundle PLUS the
236/// same base for the token bundle, plus `extra_storage_bytes` of flat per-transition storage
237/// priced at the storage rate (the balance items the transition writes outside the pools).
238///
239/// The fee bundle's value balance must equal this exactly (plus the agreed price for a
240/// purchase); the SDK builders and the minimum-fee validation both use it.
241pub fn compute_token_pool_paid_shielded_fee(
242    token_actions: usize,
243    fee_actions: usize,
244    extra_storage_bytes: u64,
245    platform_version: &PlatformVersion,
246) -> Result<Credits, ProtocolError> {
247    match platform_version.dpp.methods.compute_minimum_shielded_fee {
248        0 => v0::compute_token_pool_paid_shielded_fee_v0(
249            token_actions,
250            fee_actions,
251            extra_storage_bytes,
252            platform_version,
253        ),
254        version => Err(ProtocolError::UnknownVersionMismatch {
255            method: "compute_token_pool_paid_shielded_fee".to_string(),
256            known_versions: vec![0],
257            received: version,
258        }),
259    }
260}
261
262/// The fee of a `TokenShieldedTransferWithShieldedFee`: both bundles, nothing written outside
263/// the pools.
264pub fn compute_token_shielded_transfer_with_shielded_fee_fee(
265    token_actions: usize,
266    fee_actions: usize,
267    platform_version: &PlatformVersion,
268) -> Result<Credits, ProtocolError> {
269    compute_token_pool_paid_shielded_fee(token_actions, fee_actions, 0, platform_version)
270}
271
272/// The fee of a `TokenUnshieldWithShieldedFee`: both bundles plus the recipient's token
273/// balance item, priced as the insert it is for a recipient who has never held this token.
274///
275/// The recipient only has to be an existing identity, not an existing holder, so the balance item
276/// usually does not exist yet and the write creates it. The component is the insert's cost for
277/// every recipient alike: `credit_amount` is public and must equal this fee exactly, so a fee that
278/// distinguished the two cases would publish whether the recipient is holding this token for the
279/// first time. See [`SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES`].
280pub fn compute_token_unshield_with_shielded_fee_fee(
281    token_actions: usize,
282    fee_actions: usize,
283    platform_version: &PlatformVersion,
284) -> Result<Credits, ProtocolError> {
285    compute_token_pool_paid_shielded_fee(
286        token_actions,
287        fee_actions,
288        SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES,
289        platform_version,
290    )
291}
292
293/// The fee of a `TokenPurchaseFromShieldedPool`: both bundles plus the contract owner's credit
294/// balance write and the token supply item.
295pub fn compute_token_purchase_from_shielded_pool_fee(
296    token_actions: usize,
297    fee_actions: usize,
298    platform_version: &PlatformVersion,
299) -> Result<Credits, ProtocolError> {
300    compute_token_pool_paid_shielded_fee(
301        token_actions,
302        fee_actions,
303        SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES
304            .saturating_add(SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES),
305        platform_version,
306    )
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn should_preserve_token_purchase_fee_when_identity_shield_floor_changes() {
315        let platform_version = PlatformVersion::latest();
316        for (token_actions, fee_actions) in [(2, 2), (3, 4), (16, 16)] {
317            let historical_fee = compute_token_pool_paid_shielded_fee(
318                token_actions,
319                fee_actions,
320                28,
321                platform_version,
322            )
323            .expect("historical token purchase fee");
324            assert_eq!(
325                compute_token_purchase_from_shielded_pool_fee(
326                    token_actions,
327                    fee_actions,
328                    platform_version,
329                )
330                .expect("token purchase fee"),
331                historical_fee,
332                "identity shielding must not change the token purchase allowance"
333            );
334        }
335    }
336}