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}