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, DecodeUntrusted, 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(
33 Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize, Encode, Decode, DecodeUntrusted,
34)]
35pub struct FeeRefunds(pub CreditsPerEpochByIdentifier);
36
37impl FeeRefunds {
38 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 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 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 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 self.0.insert(identifier, to_insert_int_map);
105 }
106 Ok(())
107 }
108
109 pub fn get(&self, key: &[u8; 32]) -> Option<&CreditsPerEpoch> {
111 self.0.get(key)
112 }
113
114 pub fn iter(&self) -> Iter<'_, [u8; 32], CreditsPerEpoch> {
116 self.0.iter()
117 }
118
119 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 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 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}