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
362    // -----------------------------------------------------------------------
363    // Creditable::to_signed() on Credits (u64)
364    // -----------------------------------------------------------------------
365
366    #[test]
367    fn credits_to_signed_within_range() {
368        let credits: Credits = 1000;
369        let result = credits.to_signed();
370        assert!(result.is_ok());
371        assert_eq!(result.unwrap(), 1000i64);
372    }
373
374    #[test]
375    fn credits_to_signed_zero() {
376        let credits: Credits = 0;
377        let result = credits.to_signed();
378        assert!(result.is_ok());
379        assert_eq!(result.unwrap(), 0i64);
380    }
381
382    #[test]
383    fn credits_to_signed_max_i64() {
384        let credits: Credits = i64::MAX as u64;
385        let result = credits.to_signed();
386        assert!(result.is_ok());
387        assert_eq!(result.unwrap(), i64::MAX);
388    }
389
390    #[test]
391    fn credits_to_signed_overflow() {
392        // u64::MAX cannot be represented as i64
393        let credits: Credits = u64::MAX;
394        let result = credits.to_signed();
395        assert!(result.is_err());
396        match result.unwrap_err() {
397            ProtocolError::Overflow(msg) => {
398                assert!(msg.contains("too big"));
399            }
400            other => panic!("Expected Overflow error, got: {:?}", other),
401        }
402    }
403
404    #[test]
405    fn credits_to_signed_just_over_i64_max() {
406        // i64::MAX + 1 should overflow
407        let credits: Credits = (i64::MAX as u64) + 1;
408        let result = credits.to_signed();
409        assert!(result.is_err());
410    }
411
412    // -----------------------------------------------------------------------
413    // Creditable::to_unsigned() on Credits (u64)
414    // -----------------------------------------------------------------------
415
416    #[test]
417    fn credits_to_unsigned_returns_self() {
418        let credits: Credits = 42;
419        assert_eq!(credits.to_unsigned(), 42);
420    }
421
422    #[test]
423    fn credits_to_unsigned_zero() {
424        let credits: Credits = 0;
425        assert_eq!(credits.to_unsigned(), 0);
426    }
427
428    #[test]
429    fn credits_to_unsigned_max() {
430        let credits: Credits = u64::MAX;
431        assert_eq!(credits.to_unsigned(), u64::MAX);
432    }
433
434    // -----------------------------------------------------------------------
435    // Creditable on SignedCredits (i64)
436    // -----------------------------------------------------------------------
437
438    #[test]
439    fn signed_credits_to_signed_returns_self() {
440        let sc: SignedCredits = -500;
441        assert_eq!(sc.to_signed().unwrap(), -500);
442    }
443
444    #[test]
445    fn signed_credits_to_unsigned_returns_abs() {
446        let sc: SignedCredits = -500;
447        assert_eq!(sc.to_unsigned(), 500);
448
449        let sc_pos: SignedCredits = 500;
450        assert_eq!(sc_pos.to_unsigned(), 500);
451    }
452
453    #[test]
454    fn signed_credits_to_unsigned_zero() {
455        let sc: SignedCredits = 0;
456        assert_eq!(sc.to_unsigned(), 0);
457    }
458
459    // -----------------------------------------------------------------------
460    // from_vec_bytes / to_vec_bytes round-trip for Credits (u64)
461    // -----------------------------------------------------------------------
462
463    #[test]
464    fn credits_roundtrip_zero() {
465        let original: Credits = 0;
466        let bytes = original.to_vec_bytes();
467        let decoded = Credits::from_vec_bytes(bytes).unwrap();
468        assert_eq!(decoded, original);
469    }
470
471    #[test]
472    fn credits_roundtrip_one() {
473        let original: Credits = 1;
474        let bytes = original.to_vec_bytes();
475        let decoded = Credits::from_vec_bytes(bytes).unwrap();
476        assert_eq!(decoded, original);
477    }
478
479    #[test]
480    fn credits_roundtrip_max() {
481        let original: Credits = u64::MAX;
482        let bytes = original.to_vec_bytes();
483        let decoded = Credits::from_vec_bytes(bytes).unwrap();
484        assert_eq!(decoded, original);
485    }
486
487    #[test]
488    fn credits_roundtrip_large_value() {
489        let original: Credits = 1_000_000_000_000;
490        let bytes = original.to_vec_bytes();
491        let decoded = Credits::from_vec_bytes(bytes).unwrap();
492        assert_eq!(decoded, original);
493    }
494
495    #[test]
496    fn credits_roundtrip_max_credits_constant() {
497        let original: Credits = MAX_CREDITS;
498        let bytes = original.to_vec_bytes();
499        let decoded = Credits::from_vec_bytes(bytes).unwrap();
500        assert_eq!(decoded, original);
501    }
502
503    #[test]
504    fn credits_from_vec_bytes_empty_vec_error() {
505        let result = Credits::from_vec_bytes(vec![]);
506        assert!(result.is_err());
507    }
508
509    // -----------------------------------------------------------------------
510    // from_vec_bytes / to_vec_bytes round-trip for SignedCredits (i64)
511    // -----------------------------------------------------------------------
512
513    #[test]
514    fn signed_credits_roundtrip_zero() {
515        let original: SignedCredits = 0;
516        let bytes = original.to_vec_bytes();
517        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
518        assert_eq!(decoded, original);
519    }
520
521    #[test]
522    fn signed_credits_roundtrip_positive() {
523        let original: SignedCredits = 123456789;
524        let bytes = original.to_vec_bytes();
525        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
526        assert_eq!(decoded, original);
527    }
528
529    #[test]
530    fn signed_credits_roundtrip_negative() {
531        let original: SignedCredits = -987654321;
532        let bytes = original.to_vec_bytes();
533        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
534        assert_eq!(decoded, original);
535    }
536
537    #[test]
538    fn signed_credits_roundtrip_max() {
539        let original: SignedCredits = i64::MAX;
540        let bytes = original.to_vec_bytes();
541        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
542        assert_eq!(decoded, original);
543    }
544
545    #[test]
546    fn signed_credits_roundtrip_min() {
547        let original: SignedCredits = i64::MIN;
548        let bytes = original.to_vec_bytes();
549        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
550        assert_eq!(decoded, original);
551    }
552
553    #[test]
554    fn signed_credits_from_vec_bytes_empty_vec_error() {
555        let result = SignedCredits::from_vec_bytes(vec![]);
556        assert!(result.is_err());
557    }
558
559    // -----------------------------------------------------------------------
560    // MAX_CREDITS constant
561    // -----------------------------------------------------------------------
562
563    #[test]
564    fn max_credits_equals_i64_max() {
565        assert_eq!(MAX_CREDITS, i64::MAX as u64);
566    }
567
568    // -----------------------------------------------------------------------
569    // CreditOperation::merge
570    // -----------------------------------------------------------------------
571
572    #[test]
573    fn credit_operation_merge_set_set() {
574        let a = CreditOperation::SetCredits(100);
575        let b = CreditOperation::SetCredits(200);
576        assert_eq!(a.merge(&b), CreditOperation::SetCredits(200));
577    }
578
579    #[test]
580    fn credit_operation_merge_set_add() {
581        let a = CreditOperation::SetCredits(100);
582        let b = CreditOperation::AddToCredits(50);
583        assert_eq!(a.merge(&b), CreditOperation::SetCredits(150));
584    }
585
586    #[test]
587    fn credit_operation_merge_add_set() {
588        let a = CreditOperation::AddToCredits(100);
589        let b = CreditOperation::SetCredits(200);
590        assert_eq!(a.merge(&b), CreditOperation::SetCredits(200));
591    }
592
593    #[test]
594    fn credit_operation_merge_add_add() {
595        let a = CreditOperation::AddToCredits(100);
596        let b = CreditOperation::AddToCredits(50);
597        assert_eq!(a.merge(&b), CreditOperation::AddToCredits(150));
598    }
599
600    #[test]
601    fn credit_operation_merge_set_add_saturating() {
602        let a = CreditOperation::SetCredits(u64::MAX);
603        let b = CreditOperation::AddToCredits(1);
604        // Should saturate, not overflow
605        assert_eq!(a.merge(&b), CreditOperation::SetCredits(u64::MAX));
606    }
607
608    #[test]
609    fn credit_operation_merge_add_add_saturating() {
610        let a = CreditOperation::AddToCredits(u64::MAX);
611        let b = CreditOperation::AddToCredits(1);
612        assert_eq!(a.merge(&b), CreditOperation::AddToCredits(u64::MAX));
613    }
614
615    // -----------------------------------------------------------------------
616    // CREDITS_PER_DUFF constant
617    // -----------------------------------------------------------------------
618
619    #[test]
620    fn credits_per_duff_is_1000() {
621        assert_eq!(CREDITS_PER_DUFF, 1000);
622    }
623}