Skip to main content

dpp/fee/fee_result/
mod.rs

1// MIT LICENSE
2//
3// Copyright (c) 2021 Dash Core Group
4//
5// Permission is hereby granted, free of charge, to any
6// person obtaining a copy of this software and associated
7// documentation files (the "Software"), to deal in the
8// Software without restriction, including without
9// limitation the rights to use, copy, modify, merge,
10// publish, distribute, sublicense, and/or sell copies of
11// the Software, and to permit persons to whom the Software
12// is furnished to do so, subject to the following
13// conditions:
14//
15// The above copyright notice and this permission notice
16// shall be included in all copies or substantial portions
17// of the Software.
18//
19// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
20// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
21// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
22// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
23// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27// DEALINGS IN THE SOFTWARE.
28//
29
30//! Fee Result
31//!
32//! Each drive operation returns FeeResult after execution.
33//! This result contains fees which are required to pay for
34//! computation and storage. It also contains fees to refund
35//! for removed data from the state.
36//!
37
38use crate::consensus::fee::balance_is_not_enough_error::BalanceIsNotEnoughError;
39use crate::consensus::fee::fee_error::FeeError;
40
41use crate::fee::fee_result::refunds::FeeRefunds;
42use crate::fee::fee_result::BalanceChange::{AddToBalance, NoBalanceChange, RemoveFromBalance};
43use crate::fee::Credits;
44use crate::prelude::UserFeeIncrease;
45use crate::ProtocolError;
46use platform_value::Identifier;
47use std::cmp::Ordering;
48use std::collections::BTreeMap;
49use std::convert::TryFrom;
50
51pub mod refunds;
52
53/// The part of a storage fee paid for storage that lives a known number of epochs, keyed by
54/// that number of epochs.
55pub type LifetimeStorageFees = BTreeMap<u16, Credits>;
56
57/// Fee Result
58#[derive(Debug, Clone, Eq, PartialEq, Default)]
59pub struct FeeResult {
60    /// Storage fee
61    pub storage_fee: Credits,
62    /// Processing fee
63    pub processing_fee: Credits,
64    /// Credits to refund to identities
65    pub fee_refunds: FeeRefunds,
66    /// Removed bytes not needing to be refunded to identities
67    pub removed_bytes_from_system: u32,
68    /// The part of `storage_fee` paid for storage that lives a known number of epochs, keyed
69    /// by that number: the writes of a document whose type declares a `ttl` (protocol version
70    /// 14). The pools pay it out over those epochs, where the rest of `storage_fee` goes to
71    /// the perpetual storage distribution. Empty before protocol version 14.
72    pub lifetime_storage_fees: LifetimeStorageFees,
73}
74
75impl TryFrom<Vec<FeeResult>> for FeeResult {
76    type Error = ProtocolError;
77    fn try_from(value: Vec<FeeResult>) -> Result<Self, Self::Error> {
78        let mut aggregate_fee_result = FeeResult::default();
79        value
80            .into_iter()
81            .try_for_each(|fee_result| aggregate_fee_result.checked_add_assign(fee_result))?;
82        Ok(aggregate_fee_result)
83    }
84}
85
86impl TryFrom<Vec<Option<FeeResult>>> for FeeResult {
87    type Error = ProtocolError;
88    fn try_from(value: Vec<Option<FeeResult>>) -> Result<Self, Self::Error> {
89        let mut aggregate_fee_result = FeeResult::default();
90        value.into_iter().try_for_each(|fee_result| {
91            if let Some(fee_result) = fee_result {
92                aggregate_fee_result.checked_add_assign(fee_result)
93            } else {
94                Ok(())
95            }
96        })?;
97        Ok(aggregate_fee_result)
98    }
99}
100
101/// The balance change for an identity
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub enum BalanceChange {
104    /// Add Balance
105    AddToBalance(Credits),
106    /// Remove Balance
107    RemoveFromBalance {
108        /// the required removed balance
109        required_removed_balance: Credits,
110        /// the desired removed balance
111        desired_removed_balance: Credits,
112    },
113    /// There was no balance change
114    NoBalanceChange,
115}
116
117/// The fee expense for the identity from a fee result
118#[derive(Clone, Debug)]
119pub struct BalanceChangeForIdentity {
120    /// The identifier of the identity
121    pub identity_id: Identifier,
122
123    fee_result: FeeResult,
124    change: BalanceChange,
125}
126
127impl BalanceChangeForIdentity {
128    /// Balance change
129    pub fn change(&self) -> &BalanceChange {
130        &self.change
131    }
132
133    /// Returns refund amount of credits for other identities
134    pub fn other_refunds(&self) -> BTreeMap<Identifier, Credits> {
135        self.fee_result
136            .fee_refunds
137            .calculate_all_refunds_except_identity(self.identity_id)
138    }
139
140    /// Convert into a fee result
141    pub fn into_fee_result(self) -> FeeResult {
142        self.fee_result
143    }
144
145    /// Convert into a fee result minus some processing
146    fn into_fee_result_less_processing_debt(self, processing_debt: u64) -> FeeResult {
147        FeeResult {
148            processing_fee: self.fee_result.processing_fee - processing_debt,
149            ..self.fee_result
150        }
151    }
152
153    /// The fee result outcome based on user balance
154    pub fn fee_result_outcome<E>(self, user_balance: u64) -> Result<FeeResult, E>
155    where
156        E: From<FeeError>,
157    {
158        match self.change {
159            AddToBalance { .. } => {
160                // when we add balance we are sure that all the storage fee and processing fee has
161                // been paid
162                Ok(self.into_fee_result())
163            }
164            RemoveFromBalance {
165                required_removed_balance,
166                desired_removed_balance,
167            } => {
168                if user_balance >= desired_removed_balance {
169                    Ok(self.into_fee_result())
170                } else if user_balance >= required_removed_balance {
171                    // We do not take into account balance debt for total credits balance verification
172                    // so we shouldn't add them to pools
173                    Ok(self.into_fee_result_less_processing_debt(
174                        desired_removed_balance - user_balance,
175                    ))
176                } else {
177                    // The user could not pay for required storage space
178                    Err(
179                        FeeError::BalanceIsNotEnoughError(BalanceIsNotEnoughError::new(
180                            user_balance,
181                            required_removed_balance,
182                        ))
183                        .into(),
184                    )
185                }
186            }
187            NoBalanceChange => {
188                // while there might be no balance change we still need to deal with refunds
189                Ok(self.into_fee_result())
190            }
191        }
192    }
193}
194
195impl FeeResult {
196    /// Convenience method to create a fee result from processing credits
197    pub fn new_from_processing_fee(credits: Credits) -> Self {
198        Self {
199            storage_fee: 0,
200            processing_fee: credits,
201            fee_refunds: Default::default(),
202            removed_bytes_from_system: 0,
203            lifetime_storage_fees: Default::default(),
204        }
205    }
206
207    /// Apply a fee multiplier to a fee result
208    pub fn apply_user_fee_increase(&mut self, add_fee_percentage_multiplier: UserFeeIncrease) {
209        let additional_processing_fee = (self.processing_fee as u128)
210            .saturating_mul(add_fee_percentage_multiplier as u128)
211            .saturating_div(100);
212        if additional_processing_fee > u64::MAX as u128 {
213            self.processing_fee = u64::MAX;
214        } else {
215            self.processing_fee = self
216                .processing_fee
217                .saturating_add(additional_processing_fee as u64);
218        }
219    }
220
221    /// Convenience method to get total fee
222    pub fn total_base_fee(&self) -> Credits {
223        self.storage_fee.saturating_add(self.processing_fee)
224    }
225
226    /// Convenience method to get required removed balance
227    pub fn into_balance_change(self, identity_id: Identifier) -> BalanceChangeForIdentity {
228        let storage_credits_returned = self
229            .fee_refunds
230            .calculate_refunds_amount_for_identity(identity_id)
231            .unwrap_or_default();
232
233        let base_required_removed_balance = self.storage_fee;
234        let base_desired_removed_balance = self.storage_fee + self.processing_fee;
235
236        let balance_change = match storage_credits_returned.cmp(&base_desired_removed_balance) {
237            Ordering::Less => {
238                // If we refund more than require to pay we should nil the required
239                let required_removed_balance =
240                    base_required_removed_balance.saturating_sub(storage_credits_returned);
241
242                let desired_removed_balance =
243                    base_desired_removed_balance - storage_credits_returned;
244
245                RemoveFromBalance {
246                    required_removed_balance,
247                    desired_removed_balance,
248                }
249            }
250            Ordering::Equal => NoBalanceChange,
251            Ordering::Greater => {
252                // Credits returned are greater than our spend
253                AddToBalance(storage_credits_returned - base_desired_removed_balance)
254            }
255        };
256
257        BalanceChangeForIdentity {
258            identity_id,
259            fee_result: self,
260            change: balance_change,
261        }
262    }
263
264    /// Creates a FeeResult instance with specified storage and processing fees
265    pub fn default_with_fees(storage_fee: Credits, processing_fee: Credits) -> Self {
266        FeeResult {
267            storage_fee,
268            processing_fee,
269            ..Default::default()
270        }
271    }
272
273    /// Adds and self assigns result between two Fee Results
274    pub fn checked_add_assign(&mut self, rhs: Self) -> Result<(), ProtocolError> {
275        self.storage_fee = self
276            .storage_fee
277            .checked_add(rhs.storage_fee)
278            .ok_or(ProtocolError::Overflow("storage fee overflow error"))?;
279        self.processing_fee = self
280            .processing_fee
281            .checked_add(rhs.processing_fee)
282            .ok_or(ProtocolError::Overflow("processing fee overflow error"))?;
283        self.fee_refunds.checked_add_assign(rhs.fee_refunds)?;
284        self.removed_bytes_from_system = self
285            .removed_bytes_from_system
286            .checked_add(rhs.removed_bytes_from_system)
287            .ok_or(ProtocolError::Overflow(
288                "removed_bytes_from_system overflow error",
289            ))?;
290        for (lifetime_epochs, credits) in rhs.lifetime_storage_fees {
291            let lifetime_credits = self
292                .lifetime_storage_fees
293                .entry(lifetime_epochs)
294                .or_default();
295            *lifetime_credits =
296                lifetime_credits
297                    .checked_add(credits)
298                    .ok_or(ProtocolError::Overflow(
299                        "lifetime storage fee overflow error",
300                    ))?;
301        }
302        Ok(())
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::consensus::fee::fee_error::FeeError;
310    use crate::fee::epoch::CreditsPerEpoch;
311    use crate::fee::fee_result::refunds::{CreditsPerEpochByIdentifier, FeeRefunds};
312
313    fn make_id(byte: u8) -> Identifier {
314        Identifier::from([byte; 32])
315    }
316
317    /// Build a FeeRefunds that gives `credits` to `identity_id` (all in epoch 0).
318    fn fee_refunds_for_identity(identity_id: Identifier, credits: Credits) -> FeeRefunds {
319        let mut credits_per_epoch = CreditsPerEpoch::default();
320        credits_per_epoch.insert(0, credits);
321        let mut map = CreditsPerEpochByIdentifier::new();
322        map.insert(*identity_id.as_bytes(), credits_per_epoch);
323        FeeRefunds(map)
324    }
325
326    // --- BalanceChangeForIdentity::change() ---
327
328    #[test]
329    fn balance_change_for_identity_change_returns_correct_ref() {
330        let id = make_id(1);
331        let fee_result = FeeResult::default_with_fees(100, 50);
332        let bci = fee_result.into_balance_change(id);
333        // No refunds, so it should be RemoveFromBalance
334        match bci.change() {
335            BalanceChange::RemoveFromBalance {
336                required_removed_balance,
337                desired_removed_balance,
338            } => {
339                assert_eq!(*required_removed_balance, 100);
340                assert_eq!(*desired_removed_balance, 150);
341            }
342            other => panic!("Expected RemoveFromBalance, got {:?}", other),
343        }
344    }
345
346    // --- BalanceChangeForIdentity::other_refunds() ---
347
348    #[test]
349    fn other_refunds_empty_when_no_refunds() {
350        let id = make_id(1);
351        let fee_result = FeeResult::default_with_fees(100, 50);
352        let bci = fee_result.into_balance_change(id);
353        let refunds = bci.other_refunds();
354        assert!(refunds.is_empty());
355    }
356
357    #[test]
358    fn other_refunds_excludes_own_identity() {
359        let id = make_id(1);
360        let other_id = make_id(2);
361        // Build refunds for both identities
362        let mut credits_per_epoch_self = CreditsPerEpoch::default();
363        credits_per_epoch_self.insert(0, 200);
364        let mut credits_per_epoch_other = CreditsPerEpoch::default();
365        credits_per_epoch_other.insert(0, 300);
366        let mut map = CreditsPerEpochByIdentifier::new();
367        map.insert(*id.as_bytes(), credits_per_epoch_self);
368        map.insert(*other_id.as_bytes(), credits_per_epoch_other);
369        let refunds = FeeRefunds(map);
370
371        let fee_result = FeeResult {
372            storage_fee: 100,
373            processing_fee: 50,
374            fee_refunds: refunds,
375            removed_bytes_from_system: 0,
376            lifetime_storage_fees: Default::default(),
377        };
378        let bci = fee_result.into_balance_change(id);
379        let other = bci.other_refunds();
380        assert_eq!(other.len(), 1);
381        assert_eq!(*other.get(&other_id).unwrap(), 300);
382    }
383
384    // --- BalanceChangeForIdentity::into_fee_result() ---
385
386    #[test]
387    fn into_fee_result_preserves_original() {
388        let fee_result = FeeResult {
389            storage_fee: 42,
390            processing_fee: 58,
391            fee_refunds: FeeRefunds::default(),
392            removed_bytes_from_system: 10,
393            lifetime_storage_fees: Default::default(),
394        };
395        let id = make_id(1);
396        let bci = fee_result.clone().into_balance_change(id);
397        let recovered = bci.into_fee_result();
398        assert_eq!(recovered.storage_fee, 42);
399        assert_eq!(recovered.processing_fee, 58);
400        assert_eq!(recovered.removed_bytes_from_system, 10);
401    }
402
403    // --- BalanceChangeForIdentity::fee_result_outcome() ---
404
405    #[test]
406    fn fee_result_outcome_add_to_balance_returns_fee_result() {
407        let id = make_id(1);
408        // Refund more than storage + processing so we get AddToBalance
409        let refunds = fee_refunds_for_identity(id, 500);
410        let fee_result = FeeResult {
411            storage_fee: 100,
412            processing_fee: 50,
413            fee_refunds: refunds,
414            removed_bytes_from_system: 0,
415            lifetime_storage_fees: Default::default(),
416        };
417        let bci = fee_result.into_balance_change(id);
418        match bci.change() {
419            BalanceChange::AddToBalance(amount) => assert_eq!(*amount, 350),
420            other => panic!("Expected AddToBalance, got {:?}", other),
421        }
422        // Cannot access change after move, re-create
423        let refunds2 = fee_refunds_for_identity(id, 500);
424        let fee_result2 = FeeResult {
425            storage_fee: 100,
426            processing_fee: 50,
427            fee_refunds: refunds2,
428            removed_bytes_from_system: 0,
429            lifetime_storage_fees: Default::default(),
430        };
431        let bci2 = fee_result2.into_balance_change(id);
432        let result: Result<FeeResult, FeeError> = bci2.fee_result_outcome(0);
433        assert!(result.is_ok());
434    }
435
436    #[test]
437    fn fee_result_outcome_remove_balance_sufficient_desired() {
438        let id = make_id(1);
439        let fee_result = FeeResult::default_with_fees(100, 50);
440        let bci = fee_result.into_balance_change(id);
441        // User has enough for desired_removed_balance (150)
442        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(200);
443        let fr = result.unwrap();
444        assert_eq!(fr.storage_fee, 100);
445        assert_eq!(fr.processing_fee, 50);
446    }
447
448    #[test]
449    fn fee_result_outcome_remove_balance_sufficient_required_but_not_desired() {
450        let id = make_id(1);
451        let fee_result = FeeResult::default_with_fees(100, 50);
452        let bci = fee_result.into_balance_change(id);
453        // User has 120: enough for required (100) but not desired (150)
454        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(120);
455        let fr = result.unwrap();
456        assert_eq!(fr.storage_fee, 100);
457        // processing_fee should be reduced by (desired - user_balance) = 150 - 120 = 30
458        assert_eq!(fr.processing_fee, 20);
459    }
460
461    #[test]
462    fn fee_result_outcome_remove_balance_insufficient_returns_error() {
463        let id = make_id(1);
464        let fee_result = FeeResult::default_with_fees(100, 50);
465        let bci = fee_result.into_balance_change(id);
466        // User has less than required (100)
467        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(50);
468        assert!(result.is_err());
469        match result.unwrap_err() {
470            FeeError::BalanceIsNotEnoughError(e) => {
471                assert_eq!(e.balance(), 50);
472                assert_eq!(e.fee(), 100);
473            }
474        }
475    }
476
477    #[test]
478    fn fee_result_outcome_no_balance_change_returns_fee_result() {
479        let id = make_id(1);
480        // Refund exactly storage + processing = 150
481        let refunds = fee_refunds_for_identity(id, 150);
482        let fee_result = FeeResult {
483            storage_fee: 100,
484            processing_fee: 50,
485            fee_refunds: refunds,
486            removed_bytes_from_system: 0,
487            lifetime_storage_fees: Default::default(),
488        };
489        let bci = fee_result.into_balance_change(id);
490        match bci.change() {
491            BalanceChange::NoBalanceChange => {}
492            other => panic!("Expected NoBalanceChange, got {:?}", other),
493        }
494        // Re-create for outcome check
495        let refunds2 = fee_refunds_for_identity(id, 150);
496        let fee_result2 = FeeResult {
497            storage_fee: 100,
498            processing_fee: 50,
499            fee_refunds: refunds2,
500            removed_bytes_from_system: 0,
501            lifetime_storage_fees: Default::default(),
502        };
503        let bci2 = fee_result2.into_balance_change(id);
504        let result: Result<FeeResult, FeeError> = bci2.fee_result_outcome(0);
505        assert!(result.is_ok());
506    }
507
508    // --- FeeResult::into_balance_change() with 3 ordering branches ---
509
510    #[test]
511    fn into_balance_change_less_refund_than_fees() {
512        let id = make_id(1);
513        // Refund 50, but storage=100 processing=50 total=150
514        let refunds = fee_refunds_for_identity(id, 50);
515        let fee_result = FeeResult {
516            storage_fee: 100,
517            processing_fee: 50,
518            fee_refunds: refunds,
519            removed_bytes_from_system: 0,
520            lifetime_storage_fees: Default::default(),
521        };
522        let bci = fee_result.into_balance_change(id);
523        match bci.change() {
524            BalanceChange::RemoveFromBalance {
525                required_removed_balance,
526                desired_removed_balance,
527            } => {
528                // required = max(0, 100 - 50) = 50
529                assert_eq!(*required_removed_balance, 50);
530                // desired = 150 - 50 = 100
531                assert_eq!(*desired_removed_balance, 100);
532            }
533            other => panic!("Expected RemoveFromBalance, got {:?}", other),
534        }
535    }
536
537    #[test]
538    fn into_balance_change_refund_equals_fees() {
539        let id = make_id(1);
540        let refunds = fee_refunds_for_identity(id, 150);
541        let fee_result = FeeResult {
542            storage_fee: 100,
543            processing_fee: 50,
544            fee_refunds: refunds,
545            removed_bytes_from_system: 0,
546            lifetime_storage_fees: Default::default(),
547        };
548        let bci = fee_result.into_balance_change(id);
549        assert_eq!(bci.change(), &BalanceChange::NoBalanceChange);
550    }
551
552    #[test]
553    fn into_balance_change_refund_greater_than_fees() {
554        let id = make_id(1);
555        let refunds = fee_refunds_for_identity(id, 300);
556        let fee_result = FeeResult {
557            storage_fee: 100,
558            processing_fee: 50,
559            fee_refunds: refunds,
560            removed_bytes_from_system: 0,
561            lifetime_storage_fees: Default::default(),
562        };
563        let bci = fee_result.into_balance_change(id);
564        match bci.change() {
565            BalanceChange::AddToBalance(amount) => {
566                assert_eq!(*amount, 150); // 300 - 150
567            }
568            other => panic!("Expected AddToBalance, got {:?}", other),
569        }
570    }
571
572    #[test]
573    fn into_balance_change_no_refunds_no_fees() {
574        let id = make_id(1);
575        let fee_result = FeeResult::default();
576        let bci = fee_result.into_balance_change(id);
577        // 0 == 0, so NoBalanceChange? Actually 0.cmp(&0) is Equal
578        assert_eq!(bci.change(), &BalanceChange::NoBalanceChange);
579    }
580
581    #[test]
582    fn into_balance_change_no_refunds_with_fees() {
583        let id = make_id(1);
584        let fee_result = FeeResult::default_with_fees(200, 100);
585        let bci = fee_result.into_balance_change(id);
586        match bci.change() {
587            BalanceChange::RemoveFromBalance {
588                required_removed_balance,
589                desired_removed_balance,
590            } => {
591                assert_eq!(*required_removed_balance, 200);
592                assert_eq!(*desired_removed_balance, 300);
593            }
594            other => panic!("Expected RemoveFromBalance, got {:?}", other),
595        }
596    }
597
598    // --- apply_user_fee_increase ---
599
600    #[test]
601    fn apply_user_fee_increase_zero_percent() {
602        let mut fr = FeeResult::default_with_fees(100, 1000);
603        fr.apply_user_fee_increase(0);
604        assert_eq!(fr.processing_fee, 1000);
605    }
606
607    #[test]
608    fn apply_user_fee_increase_100_percent() {
609        let mut fr = FeeResult::default_with_fees(100, 1000);
610        fr.apply_user_fee_increase(100);
611        // 100% additional = doubles the processing fee
612        assert_eq!(fr.processing_fee, 2000);
613    }
614
615    #[test]
616    fn apply_user_fee_increase_50_percent() {
617        let mut fr = FeeResult::default_with_fees(100, 1000);
618        fr.apply_user_fee_increase(50);
619        // 50% additional = 1000 + 500
620        assert_eq!(fr.processing_fee, 1500);
621    }
622
623    #[test]
624    fn apply_user_fee_increase_does_not_affect_storage_fee() {
625        let mut fr = FeeResult::default_with_fees(500, 1000);
626        fr.apply_user_fee_increase(100);
627        assert_eq!(fr.storage_fee, 500);
628        assert_eq!(fr.processing_fee, 2000);
629    }
630
631    #[test]
632    fn apply_user_fee_increase_saturates_on_overflow() {
633        let mut fr = FeeResult::default_with_fees(0, u64::MAX);
634        fr.apply_user_fee_increase(100);
635        // Should saturate to u64::MAX rather than panicking
636        assert_eq!(fr.processing_fee, u64::MAX);
637    }
638
639    #[test]
640    fn apply_user_fee_increase_1_percent() {
641        let mut fr = FeeResult::default_with_fees(0, 10000);
642        fr.apply_user_fee_increase(1);
643        // 1% of 10000 = 100
644        assert_eq!(fr.processing_fee, 10100);
645    }
646}