dpp/balances/
credits.rs

1//! Credits
2//!
3//! Credits are Platform native token and used for micro payments
4//! between identities, state transitions fees and masternode rewards
5//!
6//! Credits are minted on Platform by locking Dash on payment chain and
7//! can be withdrawn back to the payment chain by burning them on Platform
8//! and unlocking dash on the payment chain.
9//!
10
11use crate::prelude::BlockHeight;
12use crate::ProtocolError;
13use integer_encoding::VarInt;
14use std::collections::BTreeMap;
15use std::convert::TryFrom;
16
17/// Duffs type
18pub type Duffs = u64;
19
20/// Credits type
21pub type Credits = u64;
22
23/// RemainingCredits type
24pub type RemainingCredits = Credits;
25
26/// Token Amount type
27pub type TokenAmount = u64;
28
29/// Signed Token Amount type
30pub type SignedTokenAmount = i64;
31
32/// Sum token amount
33pub type SumTokenAmount = i128;
34
35/// Signed Credits type is used for internal computations and total credits
36/// balance verification
37pub type SignedCredits = i64;
38
39/// Maximum value of credits
40pub const MAX_CREDITS: Credits = 9223372036854775807 as Credits; //i64 Max
41
42pub const CREDITS_PER_DUFF: Credits = 1000;
43
44/// An enum for credit operations
45#[derive(Debug, Clone, Copy, PartialEq, Eq, bincode::Encode, bincode::Decode)]
46pub enum CreditOperation {
47    /// We are setting credit amounts
48    SetCredits(Credits),
49    /// We are adding to credits
50    AddToCredits(Credits),
51}
52
53/// An enum for credit operations in compacted address blobs
54#[derive(Debug, Clone, PartialEq, Eq, bincode::Encode, bincode::Decode)]
55pub enum BlockAwareCreditOperation {
56    /// We are setting credit amounts - the final value after all operations
57    SetCredits(Credits),
58    /// We are adding to credits - individual additions by block height
59    AddToCreditsOperations(BTreeMap<BlockHeight, Credits>),
60}
61
62impl BlockAwareCreditOperation {
63    /// Merges a CreditOperation from a specific block height into this BlockAwareCreditOperation.
64    ///
65    /// The merge logic:
66    /// - Once a SetCredits is encountered, the result becomes SetCredits with the final computed value
67    /// - If only AddToCredits operations, they are preserved with their block heights
68    pub fn merge(&mut self, block_height: BlockHeight, operation: &CreditOperation) {
69        match (self, operation) {
70            // Current is SetCredits, new is SetCredits -> take new value
71            (
72                BlockAwareCreditOperation::SetCredits(current),
73                CreditOperation::SetCredits(new_val),
74            ) => {
75                *current = *new_val;
76            }
77            // Current is SetCredits, new is AddToCredits -> add to current value
78            (
79                BlockAwareCreditOperation::SetCredits(current),
80                CreditOperation::AddToCredits(add_val),
81            ) => {
82                *current = current.saturating_add(*add_val);
83            }
84            // Current is AddToCredits, new is SetCredits -> compute total of adds before this block + set value
85            (
86                this @ BlockAwareCreditOperation::AddToCreditsOperations(_),
87                CreditOperation::SetCredits(new_val),
88            ) => {
89                // When we see a SetCredits, all previous AddToCredits don't matter for the final value
90                // The SetCredits establishes the baseline
91                *this = BlockAwareCreditOperation::SetCredits(*new_val);
92            }
93            // Current is AddToCredits, new is AddToCredits -> add to map
94            (
95                BlockAwareCreditOperation::AddToCreditsOperations(map),
96                CreditOperation::AddToCredits(add_val),
97            ) => {
98                map.entry(block_height)
99                    .and_modify(|existing| *existing = existing.saturating_add(*add_val))
100                    .or_insert(*add_val);
101            }
102        }
103    }
104
105    /// Creates a new BlockAwareCreditOperation from a CreditOperation at a specific block height.
106    pub fn from_operation(block_height: BlockHeight, operation: &CreditOperation) -> Self {
107        match operation {
108            CreditOperation::SetCredits(value) => BlockAwareCreditOperation::SetCredits(*value),
109            CreditOperation::AddToCredits(value) => {
110                let mut map = BTreeMap::new();
111                map.insert(block_height, *value);
112                BlockAwareCreditOperation::AddToCreditsOperations(map)
113            }
114        }
115    }
116}
117
118impl CreditOperation {
119    /// Merges two credit operations, where `other` is applied after `self`.
120    ///
121    /// The merge logic:
122    /// - SetCredits + SetCredits = SetCredits (take the later value)
123    /// - SetCredits + AddToCredits = SetCredits (original set value + added amount)
124    /// - AddToCredits + SetCredits = SetCredits (take the later value)
125    /// - AddToCredits + AddToCredits = AddToCredits (sum of both)
126    pub fn merge(&self, other: &CreditOperation) -> CreditOperation {
127        match (self, other) {
128            // If other is SetCredits, it overrides (it's the most recent set)
129            (_, CreditOperation::SetCredits(value)) => CreditOperation::SetCredits(*value),
130            // If self is SetCredits and other adds, add to the set value
131            (CreditOperation::SetCredits(set_val), CreditOperation::AddToCredits(add_val)) => {
132                CreditOperation::SetCredits(set_val.saturating_add(*add_val))
133            }
134            // If both are AddToCredits, sum them
135            (CreditOperation::AddToCredits(val1), CreditOperation::AddToCredits(val2)) => {
136                CreditOperation::AddToCredits(val1.saturating_add(*val2))
137            }
138        }
139    }
140}
141
142/// Trait for signed and unsigned credits
143pub trait Creditable {
144    /// Convert unsigned credit to singed
145    fn to_signed(&self) -> Result<SignedCredits, ProtocolError>;
146    /// Convert singed credit to unsigned
147    fn to_unsigned(&self) -> Credits;
148
149    // TODO: Should we implement serialize / unserialize traits instead?
150
151    /// Decode bytes to credits
152    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError>
153    where
154        Self: Sized;
155    /// Encode credits to bytes
156    fn to_vec_bytes(&self) -> Vec<u8>;
157}
158
159impl Creditable for Credits {
160    fn to_signed(&self) -> Result<SignedCredits, ProtocolError> {
161        SignedCredits::try_from(*self)
162            .map_err(|_| ProtocolError::Overflow("credits are too big to convert to signed value"))
163    }
164
165    fn to_unsigned(&self) -> Credits {
166        *self
167    }
168
169    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError> {
170        Self::decode_var(vec.as_slice()).map(|(n, _)| n).ok_or(
171            ProtocolError::CorruptedSerialization(
172                "pending refunds epoch index for must be u16".to_string(),
173            ),
174        )
175    }
176
177    fn to_vec_bytes(&self) -> Vec<u8> {
178        self.encode_var_vec()
179    }
180}
181
182impl Creditable for SignedCredits {
183    fn to_signed(&self) -> Result<SignedCredits, ProtocolError> {
184        Ok(*self)
185    }
186
187    fn to_unsigned(&self) -> Credits {
188        self.unsigned_abs()
189    }
190
191    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError> {
192        Self::decode_var(vec.as_slice()).map(|(n, _)| n).ok_or(
193            ProtocolError::CorruptedSerialization(
194                "pending refunds epoch index for must be u16".to_string(),
195            ),
196        )
197    }
198
199    fn to_vec_bytes(&self) -> Vec<u8> {
200        self.encode_var_vec()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    mod block_aware_credit_operation {
209        use super::*;
210
211        #[test]
212        fn from_operation_set_credits() {
213            let op =
214                BlockAwareCreditOperation::from_operation(100, &CreditOperation::SetCredits(1000));
215            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1000));
216        }
217
218        #[test]
219        fn from_operation_add_to_credits() {
220            let op =
221                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
222            let expected: BTreeMap<BlockHeight, Credits> = [(100, 500)].into_iter().collect();
223            assert_eq!(
224                op,
225                BlockAwareCreditOperation::AddToCreditsOperations(expected)
226            );
227        }
228
229        #[test]
230        fn merge_set_then_set_takes_latest() {
231            let mut op = BlockAwareCreditOperation::SetCredits(1000);
232            op.merge(101, &CreditOperation::SetCredits(2000));
233            assert_eq!(op, BlockAwareCreditOperation::SetCredits(2000));
234        }
235
236        #[test]
237        fn merge_set_then_add_adds_to_set() {
238            let mut op = BlockAwareCreditOperation::SetCredits(1000);
239            op.merge(101, &CreditOperation::AddToCredits(500));
240            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1500));
241        }
242
243        #[test]
244        fn merge_set_then_multiple_adds() {
245            let mut op = BlockAwareCreditOperation::SetCredits(1000);
246            op.merge(101, &CreditOperation::AddToCredits(500));
247            op.merge(102, &CreditOperation::AddToCredits(300));
248            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1800));
249        }
250
251        #[test]
252        fn merge_add_then_set_becomes_set() {
253            let mut op =
254                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
255            op.merge(101, &CreditOperation::SetCredits(2000));
256            assert_eq!(op, BlockAwareCreditOperation::SetCredits(2000));
257        }
258
259        #[test]
260        fn merge_add_then_add_preserves_block_heights() {
261            let mut op =
262                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
263            op.merge(101, &CreditOperation::AddToCredits(300));
264            op.merge(102, &CreditOperation::AddToCredits(200));
265
266            let expected: BTreeMap<BlockHeight, Credits> =
267                [(100, 500), (101, 300), (102, 200)].into_iter().collect();
268            assert_eq!(
269                op,
270                BlockAwareCreditOperation::AddToCreditsOperations(expected)
271            );
272        }
273
274        #[test]
275        fn merge_multiple_adds_at_same_block_combines() {
276            let mut op =
277                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
278            op.merge(100, &CreditOperation::AddToCredits(300)); // Same block
279
280            let expected: BTreeMap<BlockHeight, Credits> = [(100, 800)].into_iter().collect();
281            assert_eq!(
282                op,
283                BlockAwareCreditOperation::AddToCreditsOperations(expected)
284            );
285        }
286
287        #[test]
288        fn merge_add_then_set_then_add() {
289            // AddToCredits(500) at block 100
290            let mut op =
291                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
292            // SetCredits(1000) at block 101 - wipes out the add
293            op.merge(101, &CreditOperation::SetCredits(1000));
294            // AddToCredits(200) at block 102 - adds to the set
295            op.merge(102, &CreditOperation::AddToCredits(200));
296
297            // Result: SetCredits(1200) because Set wiped previous Add, then new Add was applied
298            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1200));
299        }
300
301        #[test]
302        fn client_sync_scenario() {
303            // This tests the key use case: client synced at block 550,
304            // then receives a compacted range 400-600 with AddToCredits at various blocks.
305            // Client should be able to filter and only apply adds for blocks > 550.
306
307            let mut op =
308                BlockAwareCreditOperation::from_operation(400, &CreditOperation::AddToCredits(100));
309            op.merge(450, &CreditOperation::AddToCredits(200));
310            op.merge(500, &CreditOperation::AddToCredits(300));
311            op.merge(550, &CreditOperation::AddToCredits(400));
312            op.merge(600, &CreditOperation::AddToCredits(500));
313
314            // Verify we have all block heights preserved
315            if let BlockAwareCreditOperation::AddToCreditsOperations(map) = &op {
316                assert_eq!(map.len(), 5);
317
318                // Client synced at 550, so they need to apply blocks > 550
319                let to_apply: Credits = map
320                    .iter()
321                    .filter(|(block, _)| **block > 550)
322                    .map(|(_, credits)| *credits)
323                    .sum();
324
325                // Only block 600's AddToCredits(500) should be applied
326                assert_eq!(to_apply, 500);
327
328                // Client synced at 400, so they need to apply blocks > 400
329                let to_apply_from_400: Credits = map
330                    .iter()
331                    .filter(|(block, _)| **block > 400)
332                    .map(|(_, credits)| *credits)
333                    .sum();
334
335                // Blocks 450, 500, 550, 600: 200 + 300 + 400 + 500 = 1400
336                assert_eq!(to_apply_from_400, 1400);
337            } else {
338                panic!("Expected AddToCreditsOperations");
339            }
340        }
341
342        #[test]
343        fn set_credits_followed_by_adds_scenario() {
344            // SetCredits at block 400, then adds at 500, 600
345            // Client synced at 450, receives range 400-600
346            // Client knows balance was SET at 400, so they start from that value
347            // and only need to apply adds at blocks > 450
348
349            let mut op =
350                BlockAwareCreditOperation::from_operation(400, &CreditOperation::SetCredits(10000));
351            op.merge(500, &CreditOperation::AddToCredits(100));
352            op.merge(600, &CreditOperation::AddToCredits(200));
353
354            // Result is SetCredits(10300) - all operations merged into final value
355            assert_eq!(op, BlockAwareCreditOperation::SetCredits(10300));
356
357            // Note: Once SetCredits is encountered, we lose per-block granularity for adds
358            // This is by design - if the balance was SET, client must use the full compacted value
359        }
360    }
361}