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