dpp/fee/fee_result/
refunds.rs1use 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
20const MIN_REFUND_LIMIT_BYTES: u32 = 32;
24
25pub type CreditsPerEpochByIdentifier = BTreeMap<[u8; 32], CreditsPerEpoch>;
27
28pub type BytesPerEpochByIdentifier = BTreeMap<[u8; 32], BytesPerEpoch>;
30
31#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize, Encode, Decode)]
33pub struct FeeRefunds(pub CreditsPerEpochByIdentifier);
34
35impl FeeRefunds {
36 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 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 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 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 self.0.insert(identifier, to_insert_int_map);
103 }
104 Ok(())
105 }
106
107 pub fn get(&self, key: &[u8; 32]) -> Option<&CreditsPerEpoch> {
109 self.0.get(key)
110 }
111
112 pub fn iter(&self) -> Iter<'_, [u8; 32], CreditsPerEpoch> {
114 self.0.iter()
115 }
116
117 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 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 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}