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/// Fee Result
54#[derive(Debug, Clone, Eq, PartialEq, Default)]
55pub struct FeeResult {
56    /// Storage fee
57    pub storage_fee: Credits,
58    /// Processing fee
59    pub processing_fee: Credits,
60    /// Credits to refund to identities
61    pub fee_refunds: FeeRefunds,
62    /// Removed bytes not needing to be refunded to identities
63    pub removed_bytes_from_system: u32,
64}
65
66impl TryFrom<Vec<FeeResult>> for FeeResult {
67    type Error = ProtocolError;
68    fn try_from(value: Vec<FeeResult>) -> Result<Self, Self::Error> {
69        let mut aggregate_fee_result = FeeResult::default();
70        value
71            .into_iter()
72            .try_for_each(|fee_result| aggregate_fee_result.checked_add_assign(fee_result))?;
73        Ok(aggregate_fee_result)
74    }
75}
76
77impl TryFrom<Vec<Option<FeeResult>>> for FeeResult {
78    type Error = ProtocolError;
79    fn try_from(value: Vec<Option<FeeResult>>) -> Result<Self, Self::Error> {
80        let mut aggregate_fee_result = FeeResult::default();
81        value.into_iter().try_for_each(|fee_result| {
82            if let Some(fee_result) = fee_result {
83                aggregate_fee_result.checked_add_assign(fee_result)
84            } else {
85                Ok(())
86            }
87        })?;
88        Ok(aggregate_fee_result)
89    }
90}
91
92/// The balance change for an identity
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub enum BalanceChange {
95    /// Add Balance
96    AddToBalance(Credits),
97    /// Remove Balance
98    RemoveFromBalance {
99        /// the required removed balance
100        required_removed_balance: Credits,
101        /// the desired removed balance
102        desired_removed_balance: Credits,
103    },
104    /// There was no balance change
105    NoBalanceChange,
106}
107
108/// The fee expense for the identity from a fee result
109#[derive(Clone, Debug)]
110pub struct BalanceChangeForIdentity {
111    /// The identifier of the identity
112    pub identity_id: Identifier,
113
114    fee_result: FeeResult,
115    change: BalanceChange,
116}
117
118impl BalanceChangeForIdentity {
119    /// Balance change
120    pub fn change(&self) -> &BalanceChange {
121        &self.change
122    }
123
124    /// Returns refund amount of credits for other identities
125    pub fn other_refunds(&self) -> BTreeMap<Identifier, Credits> {
126        self.fee_result
127            .fee_refunds
128            .calculate_all_refunds_except_identity(self.identity_id)
129    }
130
131    /// Convert into a fee result
132    pub fn into_fee_result(self) -> FeeResult {
133        self.fee_result
134    }
135
136    /// Convert into a fee result minus some processing
137    fn into_fee_result_less_processing_debt(self, processing_debt: u64) -> FeeResult {
138        FeeResult {
139            processing_fee: self.fee_result.processing_fee - processing_debt,
140            ..self.fee_result
141        }
142    }
143
144    /// The fee result outcome based on user balance
145    pub fn fee_result_outcome<E>(self, user_balance: u64) -> Result<FeeResult, E>
146    where
147        E: From<FeeError>,
148    {
149        match self.change {
150            AddToBalance { .. } => {
151                // when we add balance we are sure that all the storage fee and processing fee has
152                // been paid
153                Ok(self.into_fee_result())
154            }
155            RemoveFromBalance {
156                required_removed_balance,
157                desired_removed_balance,
158            } => {
159                if user_balance >= desired_removed_balance {
160                    Ok(self.into_fee_result())
161                } else if user_balance >= required_removed_balance {
162                    // We do not take into account balance debt for total credits balance verification
163                    // so we shouldn't add them to pools
164                    Ok(self.into_fee_result_less_processing_debt(
165                        desired_removed_balance - user_balance,
166                    ))
167                } else {
168                    // The user could not pay for required storage space
169                    Err(
170                        FeeError::BalanceIsNotEnoughError(BalanceIsNotEnoughError::new(
171                            user_balance,
172                            required_removed_balance,
173                        ))
174                        .into(),
175                    )
176                }
177            }
178            NoBalanceChange => {
179                // while there might be no balance change we still need to deal with refunds
180                Ok(self.into_fee_result())
181            }
182        }
183    }
184}
185
186impl FeeResult {
187    /// Convenience method to create a fee result from processing credits
188    pub fn new_from_processing_fee(credits: Credits) -> Self {
189        Self {
190            storage_fee: 0,
191            processing_fee: credits,
192            fee_refunds: Default::default(),
193            removed_bytes_from_system: 0,
194        }
195    }
196
197    /// Apply a fee multiplier to a fee result
198    pub fn apply_user_fee_increase(&mut self, add_fee_percentage_multiplier: UserFeeIncrease) {
199        let additional_processing_fee = (self.processing_fee as u128)
200            .saturating_mul(add_fee_percentage_multiplier as u128)
201            .saturating_div(100);
202        if additional_processing_fee > u64::MAX as u128 {
203            self.processing_fee = u64::MAX;
204        } else {
205            self.processing_fee = self
206                .processing_fee
207                .saturating_add(additional_processing_fee as u64);
208        }
209    }
210
211    /// Convenience method to get total fee
212    pub fn total_base_fee(&self) -> Credits {
213        self.storage_fee.saturating_add(self.processing_fee)
214    }
215
216    /// Convenience method to get required removed balance
217    pub fn into_balance_change(self, identity_id: Identifier) -> BalanceChangeForIdentity {
218        let storage_credits_returned = self
219            .fee_refunds
220            .calculate_refunds_amount_for_identity(identity_id)
221            .unwrap_or_default();
222
223        let base_required_removed_balance = self.storage_fee;
224        let base_desired_removed_balance = self.storage_fee + self.processing_fee;
225
226        let balance_change = match storage_credits_returned.cmp(&base_desired_removed_balance) {
227            Ordering::Less => {
228                // If we refund more than require to pay we should nil the required
229                let required_removed_balance =
230                    base_required_removed_balance.saturating_sub(storage_credits_returned);
231
232                let desired_removed_balance =
233                    base_desired_removed_balance - storage_credits_returned;
234
235                RemoveFromBalance {
236                    required_removed_balance,
237                    desired_removed_balance,
238                }
239            }
240            Ordering::Equal => NoBalanceChange,
241            Ordering::Greater => {
242                // Credits returned are greater than our spend
243                AddToBalance(storage_credits_returned - base_desired_removed_balance)
244            }
245        };
246
247        BalanceChangeForIdentity {
248            identity_id,
249            fee_result: self,
250            change: balance_change,
251        }
252    }
253
254    /// Creates a FeeResult instance with specified storage and processing fees
255    pub fn default_with_fees(storage_fee: Credits, processing_fee: Credits) -> Self {
256        FeeResult {
257            storage_fee,
258            processing_fee,
259            ..Default::default()
260        }
261    }
262
263    /// Adds and self assigns result between two Fee Results
264    pub fn checked_add_assign(&mut self, rhs: Self) -> Result<(), ProtocolError> {
265        self.storage_fee = self
266            .storage_fee
267            .checked_add(rhs.storage_fee)
268            .ok_or(ProtocolError::Overflow("storage fee overflow error"))?;
269        self.processing_fee = self
270            .processing_fee
271            .checked_add(rhs.processing_fee)
272            .ok_or(ProtocolError::Overflow("processing fee overflow error"))?;
273        self.fee_refunds.checked_add_assign(rhs.fee_refunds)?;
274        self.removed_bytes_from_system = self
275            .removed_bytes_from_system
276            .checked_add(rhs.removed_bytes_from_system)
277            .ok_or(ProtocolError::Overflow(
278                "removed_bytes_from_system overflow error",
279            ))?;
280        Ok(())
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::consensus::fee::fee_error::FeeError;
288    use crate::fee::epoch::CreditsPerEpoch;
289    use crate::fee::fee_result::refunds::{CreditsPerEpochByIdentifier, FeeRefunds};
290
291    fn make_id(byte: u8) -> Identifier {
292        Identifier::from([byte; 32])
293    }
294
295    /// Build a FeeRefunds that gives `credits` to `identity_id` (all in epoch 0).
296    fn fee_refunds_for_identity(identity_id: Identifier, credits: Credits) -> FeeRefunds {
297        let mut credits_per_epoch = CreditsPerEpoch::default();
298        credits_per_epoch.insert(0, credits);
299        let mut map = CreditsPerEpochByIdentifier::new();
300        map.insert(*identity_id.as_bytes(), credits_per_epoch);
301        FeeRefunds(map)
302    }
303
304    // --- BalanceChangeForIdentity::change() ---
305
306    #[test]
307    fn balance_change_for_identity_change_returns_correct_ref() {
308        let id = make_id(1);
309        let fee_result = FeeResult::default_with_fees(100, 50);
310        let bci = fee_result.into_balance_change(id);
311        // No refunds, so it should be RemoveFromBalance
312        match bci.change() {
313            BalanceChange::RemoveFromBalance {
314                required_removed_balance,
315                desired_removed_balance,
316            } => {
317                assert_eq!(*required_removed_balance, 100);
318                assert_eq!(*desired_removed_balance, 150);
319            }
320            other => panic!("Expected RemoveFromBalance, got {:?}", other),
321        }
322    }
323
324    // --- BalanceChangeForIdentity::other_refunds() ---
325
326    #[test]
327    fn other_refunds_empty_when_no_refunds() {
328        let id = make_id(1);
329        let fee_result = FeeResult::default_with_fees(100, 50);
330        let bci = fee_result.into_balance_change(id);
331        let refunds = bci.other_refunds();
332        assert!(refunds.is_empty());
333    }
334
335    #[test]
336    fn other_refunds_excludes_own_identity() {
337        let id = make_id(1);
338        let other_id = make_id(2);
339        // Build refunds for both identities
340        let mut credits_per_epoch_self = CreditsPerEpoch::default();
341        credits_per_epoch_self.insert(0, 200);
342        let mut credits_per_epoch_other = CreditsPerEpoch::default();
343        credits_per_epoch_other.insert(0, 300);
344        let mut map = CreditsPerEpochByIdentifier::new();
345        map.insert(*id.as_bytes(), credits_per_epoch_self);
346        map.insert(*other_id.as_bytes(), credits_per_epoch_other);
347        let refunds = FeeRefunds(map);
348
349        let fee_result = FeeResult {
350            storage_fee: 100,
351            processing_fee: 50,
352            fee_refunds: refunds,
353            removed_bytes_from_system: 0,
354        };
355        let bci = fee_result.into_balance_change(id);
356        let other = bci.other_refunds();
357        assert_eq!(other.len(), 1);
358        assert_eq!(*other.get(&other_id).unwrap(), 300);
359    }
360
361    // --- BalanceChangeForIdentity::into_fee_result() ---
362
363    #[test]
364    fn into_fee_result_preserves_original() {
365        let fee_result = FeeResult {
366            storage_fee: 42,
367            processing_fee: 58,
368            fee_refunds: FeeRefunds::default(),
369            removed_bytes_from_system: 10,
370        };
371        let id = make_id(1);
372        let bci = fee_result.clone().into_balance_change(id);
373        let recovered = bci.into_fee_result();
374        assert_eq!(recovered.storage_fee, 42);
375        assert_eq!(recovered.processing_fee, 58);
376        assert_eq!(recovered.removed_bytes_from_system, 10);
377    }
378
379    // --- BalanceChangeForIdentity::fee_result_outcome() ---
380
381    #[test]
382    fn fee_result_outcome_add_to_balance_returns_fee_result() {
383        let id = make_id(1);
384        // Refund more than storage + processing so we get AddToBalance
385        let refunds = fee_refunds_for_identity(id, 500);
386        let fee_result = FeeResult {
387            storage_fee: 100,
388            processing_fee: 50,
389            fee_refunds: refunds,
390            removed_bytes_from_system: 0,
391        };
392        let bci = fee_result.into_balance_change(id);
393        match bci.change() {
394            BalanceChange::AddToBalance(amount) => assert_eq!(*amount, 350),
395            other => panic!("Expected AddToBalance, got {:?}", other),
396        }
397        // Cannot access change after move, re-create
398        let refunds2 = fee_refunds_for_identity(id, 500);
399        let fee_result2 = FeeResult {
400            storage_fee: 100,
401            processing_fee: 50,
402            fee_refunds: refunds2,
403            removed_bytes_from_system: 0,
404        };
405        let bci2 = fee_result2.into_balance_change(id);
406        let result: Result<FeeResult, FeeError> = bci2.fee_result_outcome(0);
407        assert!(result.is_ok());
408    }
409
410    #[test]
411    fn fee_result_outcome_remove_balance_sufficient_desired() {
412        let id = make_id(1);
413        let fee_result = FeeResult::default_with_fees(100, 50);
414        let bci = fee_result.into_balance_change(id);
415        // User has enough for desired_removed_balance (150)
416        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(200);
417        let fr = result.unwrap();
418        assert_eq!(fr.storage_fee, 100);
419        assert_eq!(fr.processing_fee, 50);
420    }
421
422    #[test]
423    fn fee_result_outcome_remove_balance_sufficient_required_but_not_desired() {
424        let id = make_id(1);
425        let fee_result = FeeResult::default_with_fees(100, 50);
426        let bci = fee_result.into_balance_change(id);
427        // User has 120: enough for required (100) but not desired (150)
428        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(120);
429        let fr = result.unwrap();
430        assert_eq!(fr.storage_fee, 100);
431        // processing_fee should be reduced by (desired - user_balance) = 150 - 120 = 30
432        assert_eq!(fr.processing_fee, 20);
433    }
434
435    #[test]
436    fn fee_result_outcome_remove_balance_insufficient_returns_error() {
437        let id = make_id(1);
438        let fee_result = FeeResult::default_with_fees(100, 50);
439        let bci = fee_result.into_balance_change(id);
440        // User has less than required (100)
441        let result: Result<FeeResult, FeeError> = bci.fee_result_outcome(50);
442        assert!(result.is_err());
443        match result.unwrap_err() {
444            FeeError::BalanceIsNotEnoughError(e) => {
445                assert_eq!(e.balance(), 50);
446                assert_eq!(e.fee(), 100);
447            }
448        }
449    }
450
451    #[test]
452    fn fee_result_outcome_no_balance_change_returns_fee_result() {
453        let id = make_id(1);
454        // Refund exactly storage + processing = 150
455        let refunds = fee_refunds_for_identity(id, 150);
456        let fee_result = FeeResult {
457            storage_fee: 100,
458            processing_fee: 50,
459            fee_refunds: refunds,
460            removed_bytes_from_system: 0,
461        };
462        let bci = fee_result.into_balance_change(id);
463        match bci.change() {
464            BalanceChange::NoBalanceChange => {}
465            other => panic!("Expected NoBalanceChange, got {:?}", other),
466        }
467        // Re-create for outcome check
468        let refunds2 = fee_refunds_for_identity(id, 150);
469        let fee_result2 = FeeResult {
470            storage_fee: 100,
471            processing_fee: 50,
472            fee_refunds: refunds2,
473            removed_bytes_from_system: 0,
474        };
475        let bci2 = fee_result2.into_balance_change(id);
476        let result: Result<FeeResult, FeeError> = bci2.fee_result_outcome(0);
477        assert!(result.is_ok());
478    }
479
480    // --- FeeResult::into_balance_change() with 3 ordering branches ---
481
482    #[test]
483    fn into_balance_change_less_refund_than_fees() {
484        let id = make_id(1);
485        // Refund 50, but storage=100 processing=50 total=150
486        let refunds = fee_refunds_for_identity(id, 50);
487        let fee_result = FeeResult {
488            storage_fee: 100,
489            processing_fee: 50,
490            fee_refunds: refunds,
491            removed_bytes_from_system: 0,
492        };
493        let bci = fee_result.into_balance_change(id);
494        match bci.change() {
495            BalanceChange::RemoveFromBalance {
496                required_removed_balance,
497                desired_removed_balance,
498            } => {
499                // required = max(0, 100 - 50) = 50
500                assert_eq!(*required_removed_balance, 50);
501                // desired = 150 - 50 = 100
502                assert_eq!(*desired_removed_balance, 100);
503            }
504            other => panic!("Expected RemoveFromBalance, got {:?}", other),
505        }
506    }
507
508    #[test]
509    fn into_balance_change_refund_equals_fees() {
510        let id = make_id(1);
511        let refunds = fee_refunds_for_identity(id, 150);
512        let fee_result = FeeResult {
513            storage_fee: 100,
514            processing_fee: 50,
515            fee_refunds: refunds,
516            removed_bytes_from_system: 0,
517        };
518        let bci = fee_result.into_balance_change(id);
519        assert_eq!(bci.change(), &BalanceChange::NoBalanceChange);
520    }
521
522    #[test]
523    fn into_balance_change_refund_greater_than_fees() {
524        let id = make_id(1);
525        let refunds = fee_refunds_for_identity(id, 300);
526        let fee_result = FeeResult {
527            storage_fee: 100,
528            processing_fee: 50,
529            fee_refunds: refunds,
530            removed_bytes_from_system: 0,
531        };
532        let bci = fee_result.into_balance_change(id);
533        match bci.change() {
534            BalanceChange::AddToBalance(amount) => {
535                assert_eq!(*amount, 150); // 300 - 150
536            }
537            other => panic!("Expected AddToBalance, got {:?}", other),
538        }
539    }
540
541    #[test]
542    fn into_balance_change_no_refunds_no_fees() {
543        let id = make_id(1);
544        let fee_result = FeeResult::default();
545        let bci = fee_result.into_balance_change(id);
546        // 0 == 0, so NoBalanceChange? Actually 0.cmp(&0) is Equal
547        assert_eq!(bci.change(), &BalanceChange::NoBalanceChange);
548    }
549
550    #[test]
551    fn into_balance_change_no_refunds_with_fees() {
552        let id = make_id(1);
553        let fee_result = FeeResult::default_with_fees(200, 100);
554        let bci = fee_result.into_balance_change(id);
555        match bci.change() {
556            BalanceChange::RemoveFromBalance {
557                required_removed_balance,
558                desired_removed_balance,
559            } => {
560                assert_eq!(*required_removed_balance, 200);
561                assert_eq!(*desired_removed_balance, 300);
562            }
563            other => panic!("Expected RemoveFromBalance, got {:?}", other),
564        }
565    }
566
567    // --- apply_user_fee_increase ---
568
569    #[test]
570    fn apply_user_fee_increase_zero_percent() {
571        let mut fr = FeeResult::default_with_fees(100, 1000);
572        fr.apply_user_fee_increase(0);
573        assert_eq!(fr.processing_fee, 1000);
574    }
575
576    #[test]
577    fn apply_user_fee_increase_100_percent() {
578        let mut fr = FeeResult::default_with_fees(100, 1000);
579        fr.apply_user_fee_increase(100);
580        // 100% additional = doubles the processing fee
581        assert_eq!(fr.processing_fee, 2000);
582    }
583
584    #[test]
585    fn apply_user_fee_increase_50_percent() {
586        let mut fr = FeeResult::default_with_fees(100, 1000);
587        fr.apply_user_fee_increase(50);
588        // 50% additional = 1000 + 500
589        assert_eq!(fr.processing_fee, 1500);
590    }
591
592    #[test]
593    fn apply_user_fee_increase_does_not_affect_storage_fee() {
594        let mut fr = FeeResult::default_with_fees(500, 1000);
595        fr.apply_user_fee_increase(100);
596        assert_eq!(fr.storage_fee, 500);
597        assert_eq!(fr.processing_fee, 2000);
598    }
599
600    #[test]
601    fn apply_user_fee_increase_saturates_on_overflow() {
602        let mut fr = FeeResult::default_with_fees(0, u64::MAX);
603        fr.apply_user_fee_increase(100);
604        // Should saturate to u64::MAX rather than panicking
605        assert_eq!(fr.processing_fee, u64::MAX);
606    }
607
608    #[test]
609    fn apply_user_fee_increase_1_percent() {
610        let mut fr = FeeResult::default_with_fees(0, 10000);
611        fr.apply_user_fee_increase(1);
612        // 1% of 10000 = 100
613        assert_eq!(fr.processing_fee, 10100);
614    }
615}