Skip to main content

dpp/fee/fee_result/
refunds.rs

1//! Fee Refunds
2//!
3//! Fee refunds are calculated based on removed bytes per epoch.
4//!
5
6use crate::block::epoch::{Epoch, EpochIndex};
7use crate::fee::default_costs::KnownCostItem::StorageDiskUsageCreditPerByte;
8use crate::fee::default_costs::{CachedEpochIndexFeeVersions, EpochCosts};
9use crate::fee::epoch::distribution::calculate_storage_fee_refund_amount_and_leftovers;
10use crate::fee::epoch::{BytesPerEpoch, CreditsPerEpoch};
11use crate::fee::Credits;
12use crate::ProtocolError;
13use bincode::{Decode, DecodeUntrusted, Encode};
14
15use platform_value::Identifier;
16use serde::{Deserialize, Serialize};
17use std::collections::btree_map::Iter;
18use std::collections::BTreeMap;
19
20/// There are additional work and storage required to process refunds
21/// To protect system from the spam and unnecessary work
22/// a dust refund limit is used
23const MIN_REFUND_LIMIT_BYTES: u32 = 32;
24
25/// Credits per Epoch by Identifier
26pub type CreditsPerEpochByIdentifier = BTreeMap<[u8; 32], CreditsPerEpoch>;
27
28/// Bytes per Epoch by Identifier
29pub type BytesPerEpochByIdentifier = BTreeMap<[u8; 32], BytesPerEpoch>;
30
31/// Fee refunds to identities based on removed data from specific epochs
32#[derive(
33    Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize, Encode, Decode, DecodeUntrusted,
34)]
35pub struct FeeRefunds(pub CreditsPerEpochByIdentifier);
36
37impl FeeRefunds {
38    /// Create fee refunds from GroveDB's StorageRemovalPerEpochByIdentifier
39    pub fn from_storage_removal<I, C, E>(
40        storage_removal: I,
41        current_epoch_index: EpochIndex,
42        epochs_per_era: u16,
43        previous_fee_versions: &CachedEpochIndexFeeVersions,
44    ) -> Result<Self, ProtocolError>
45    where
46        I: IntoIterator<Item = ([u8; 32], C)>,
47        C: IntoIterator<Item = (E, u32)>,
48        E: TryInto<u16>,
49    {
50        let refunds_per_epoch_by_identifier = storage_removal
51            .into_iter()
52            .map(|(identifier, bytes_per_epochs)| {
53                bytes_per_epochs
54                    .into_iter()
55                    .filter(|(_, bytes)| bytes >= &MIN_REFUND_LIMIT_BYTES)
56                    .map(|(encoded_epoch_index, bytes)| {
57                        let epoch_index : u16 = encoded_epoch_index.try_into().map_err(|_| ProtocolError::Overflow("can't fit u64 epoch index from StorageRemovalPerEpochByIdentifier to u16 EpochIndex"))?;
58
59                        // TODO Add in multipliers once they have been made
60
61                        let credits: Credits = (bytes as Credits)
62                            .checked_mul(Epoch::new(current_epoch_index)?.cost_for_known_cost_item(previous_fee_versions, StorageDiskUsageCreditPerByte))
63                            .ok_or(ProtocolError::Overflow("storage written bytes cost overflow"))?;
64
65                        let (amount, _) = calculate_storage_fee_refund_amount_and_leftovers(
66                            credits,
67                            epoch_index,
68                            current_epoch_index,
69                            epochs_per_era,
70                        )?;
71
72                        Ok((epoch_index, amount))
73                    })
74                    .collect::<Result<CreditsPerEpoch, ProtocolError>>()
75                    .map(|credits_per_epochs| (identifier, credits_per_epochs))
76            })
77            .collect::<Result<CreditsPerEpochByIdentifier, ProtocolError>>()?;
78
79        Ok(Self(refunds_per_epoch_by_identifier))
80    }
81
82    /// Adds and self assigns result between two Fee Results
83    pub fn checked_add_assign(&mut self, rhs: Self) -> Result<(), ProtocolError> {
84        for (identifier, mut int_map_b) in rhs.0.into_iter() {
85            let to_insert_int_map = if let Some(sint_map_a) = self.0.remove(&identifier) {
86                // other has an int_map with the same identifier
87                let intersection = sint_map_a
88                    .into_iter()
89                    .map(|(k, v)| {
90                        let combined = if let Some(value_b) = int_map_b.remove(&k) {
91                            v.checked_add(value_b)
92                                .ok_or(ProtocolError::Overflow("storage fee overflow error"))
93                        } else {
94                            Ok(v)
95                        };
96                        combined.map(|c| (k, c))
97                    })
98                    .collect::<Result<CreditsPerEpoch, ProtocolError>>()?;
99                intersection.into_iter().chain(int_map_b).collect()
100            } else {
101                int_map_b
102            };
103            // reinsert the now combined IntMap
104            self.0.insert(identifier, to_insert_int_map);
105        }
106        Ok(())
107    }
108
109    /// Passthrough method for get
110    pub fn get(&self, key: &[u8; 32]) -> Option<&CreditsPerEpoch> {
111        self.0.get(key)
112    }
113
114    /// Passthrough method for iteration
115    pub fn iter(&self) -> Iter<'_, [u8; 32], CreditsPerEpoch> {
116        self.0.iter()
117    }
118
119    /// Sums the fee result among all identities
120    pub fn sum_per_epoch(self) -> CreditsPerEpoch {
121        let mut summed_credits = CreditsPerEpoch::default();
122
123        self.into_iter().for_each(|(_, credits_per_epoch)| {
124            credits_per_epoch
125                .into_iter()
126                .for_each(|(epoch_index, credits)| {
127                    summed_credits
128                        .entry(epoch_index)
129                        .and_modify(|base_credits| *base_credits += credits)
130                        .or_insert(credits);
131                });
132        });
133        summed_credits
134    }
135
136    /// Calculates a refund amount of credits per identity excluding specified identity id
137    pub fn calculate_all_refunds_except_identity(
138        &self,
139        identity_id: Identifier,
140    ) -> BTreeMap<Identifier, Credits> {
141        self.iter()
142            .filter_map(|(&identifier, _)| {
143                if identifier == identity_id {
144                    return None;
145                }
146
147                let credits = self
148                    .calculate_refunds_amount_for_identity(identifier.into())
149                    .unwrap();
150
151                Some((identifier.into(), credits))
152            })
153            .collect()
154    }
155
156    /// Calculates a refund amount of credits for specified identity id
157    pub fn calculate_refunds_amount_for_identity(
158        &self,
159        identity_id: Identifier,
160    ) -> Option<Credits> {
161        let credits_per_epoch = self.get(identity_id.as_bytes())?;
162
163        let credits = credits_per_epoch.values().sum();
164
165        Some(credits)
166    }
167}
168
169impl IntoIterator for FeeRefunds {
170    type Item = ([u8; 32], CreditsPerEpoch);
171    type IntoIter = std::collections::btree_map::IntoIter<[u8; 32], CreditsPerEpoch>;
172
173    fn into_iter(self) -> Self::IntoIter {
174        self.0.into_iter()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use once_cell::sync::Lazy;
182    use platform_version::version::fee::FeeVersion;
183
184    static EPOCH_CHANGE_FEE_VERSION_TEST: Lazy<CachedEpochIndexFeeVersions> =
185        Lazy::new(|| BTreeMap::from([(0, FeeVersion::first())]));
186
187    mod from_storage_removal {
188        use super::*;
189        use nohash_hasher::IntMap;
190        use std::iter::FromIterator;
191
192        #[test]
193        fn should_filter_out_refunds_under_the_limit() {
194            let identity_id = [0; 32];
195
196            let bytes_per_epoch = IntMap::from_iter([(0, 31), (1, 100)]);
197            let storage_removal =
198                BytesPerEpochByIdentifier::from_iter([(identity_id, bytes_per_epoch)]);
199
200            let fee_refunds = FeeRefunds::from_storage_removal(
201                storage_removal,
202                3,
203                20,
204                &EPOCH_CHANGE_FEE_VERSION_TEST,
205            )
206            .expect("should create fee refunds");
207
208            let credits_per_epoch = fee_refunds.get(&identity_id).expect("should exists");
209
210            assert!(credits_per_epoch.get(&0).is_none());
211            assert!(credits_per_epoch.get(&1).is_some());
212        }
213    }
214}