Skip to main content

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 bincode::{Decode, DecodeUntrusted, Encode};
14use integer_encoding::VarInt;
15use std::collections::BTreeMap;
16use std::convert::TryFrom;
17
18/// Duffs type
19pub type Duffs = u64;
20
21/// Credits type
22pub type Credits = u64;
23
24/// RemainingCredits type
25pub type RemainingCredits = Credits;
26
27/// Token Amount type
28pub type TokenAmount = u64;
29
30/// Signed Token Amount type
31pub type SignedTokenAmount = i64;
32
33/// Sum token amount
34pub type SumTokenAmount = i128;
35
36/// Signed Credits type is used for internal computations and total credits
37/// balance verification
38pub type SignedCredits = i64;
39
40/// Maximum value of credits
41pub const MAX_CREDITS: Credits = 9223372036854775807 as Credits; //i64 Max
42
43pub const CREDITS_PER_DUFF: Credits = 1000;
44
45/// An enum for credit operations
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
47pub enum CreditOperation {
48    /// We are setting credit amounts
49    SetCredits(Credits),
50    /// We are adding to credits
51    AddToCredits(Credits),
52}
53
54/// An enum for credit operations in compacted address blobs
55#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
56pub enum BlockAwareCreditOperation {
57    /// We are setting credit amounts - the final value after all operations
58    SetCredits(Credits),
59    /// We are adding to credits - individual additions by block height
60    AddToCreditsOperations(BTreeMap<BlockHeight, Credits>),
61}
62
63impl BlockAwareCreditOperation {
64    /// Merges a CreditOperation from a specific block height into this BlockAwareCreditOperation.
65    ///
66    /// The merge logic:
67    /// - Once a SetCredits is encountered, the result becomes SetCredits with the final computed value
68    /// - If only AddToCredits operations, they are preserved with their block heights
69    pub fn merge(&mut self, block_height: BlockHeight, operation: &CreditOperation) {
70        match (self, operation) {
71            // Current is SetCredits, new is SetCredits -> take new value
72            (
73                BlockAwareCreditOperation::SetCredits(current),
74                CreditOperation::SetCredits(new_val),
75            ) => {
76                *current = *new_val;
77            }
78            // Current is SetCredits, new is AddToCredits -> add to current value
79            (
80                BlockAwareCreditOperation::SetCredits(current),
81                CreditOperation::AddToCredits(add_val),
82            ) => {
83                *current = current.saturating_add(*add_val);
84            }
85            // Current is AddToCredits, new is SetCredits -> compute total of adds before this block + set value
86            (
87                this @ BlockAwareCreditOperation::AddToCreditsOperations(_),
88                CreditOperation::SetCredits(new_val),
89            ) => {
90                // When we see a SetCredits, all previous AddToCredits don't matter for the final value
91                // The SetCredits establishes the baseline
92                *this = BlockAwareCreditOperation::SetCredits(*new_val);
93            }
94            // Current is AddToCredits, new is AddToCredits -> add to map
95            (
96                BlockAwareCreditOperation::AddToCreditsOperations(map),
97                CreditOperation::AddToCredits(add_val),
98            ) => {
99                map.entry(block_height)
100                    .and_modify(|existing| *existing = existing.saturating_add(*add_val))
101                    .or_insert(*add_val);
102            }
103        }
104    }
105
106    /// Creates a new BlockAwareCreditOperation from a CreditOperation at a specific block height.
107    pub fn from_operation(block_height: BlockHeight, operation: &CreditOperation) -> Self {
108        match operation {
109            CreditOperation::SetCredits(value) => BlockAwareCreditOperation::SetCredits(*value),
110            CreditOperation::AddToCredits(value) => {
111                let mut map = BTreeMap::new();
112                map.insert(block_height, *value);
113                BlockAwareCreditOperation::AddToCreditsOperations(map)
114            }
115        }
116    }
117}
118
119impl CreditOperation {
120    /// Merges two credit operations, where `other` is applied after `self`.
121    ///
122    /// The merge logic:
123    /// - SetCredits + SetCredits = SetCredits (take the later value)
124    /// - SetCredits + AddToCredits = SetCredits (original set value + added amount)
125    /// - AddToCredits + SetCredits = SetCredits (take the later value)
126    /// - AddToCredits + AddToCredits = AddToCredits (sum of both)
127    pub fn merge(&self, other: &CreditOperation) -> CreditOperation {
128        match (self, other) {
129            // If other is SetCredits, it overrides (it's the most recent set)
130            (_, CreditOperation::SetCredits(value)) => CreditOperation::SetCredits(*value),
131            // If self is SetCredits and other adds, add to the set value
132            (CreditOperation::SetCredits(set_val), CreditOperation::AddToCredits(add_val)) => {
133                CreditOperation::SetCredits(set_val.saturating_add(*add_val))
134            }
135            // If both are AddToCredits, sum them
136            (CreditOperation::AddToCredits(val1), CreditOperation::AddToCredits(val2)) => {
137                CreditOperation::AddToCredits(val1.saturating_add(*val2))
138            }
139        }
140    }
141}
142
143/// Trait for signed and unsigned credits
144pub trait Creditable {
145    /// Convert unsigned credit to singed
146    fn to_signed(&self) -> Result<SignedCredits, ProtocolError>;
147    /// Convert singed credit to unsigned
148    fn to_unsigned(&self) -> Credits;
149
150    // TODO: Should we implement serialize / unserialize traits instead?
151
152    /// Decode bytes to credits
153    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError>
154    where
155        Self: Sized;
156    /// Encode credits to bytes
157    fn to_vec_bytes(&self) -> Vec<u8>;
158}
159
160impl Creditable for Credits {
161    fn to_signed(&self) -> Result<SignedCredits, ProtocolError> {
162        SignedCredits::try_from(*self)
163            .map_err(|_| ProtocolError::Overflow("credits are too big to convert to signed value"))
164    }
165
166    fn to_unsigned(&self) -> Credits {
167        *self
168    }
169
170    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError> {
171        Self::decode_var(vec.as_slice()).map(|(n, _)| n).ok_or(
172            ProtocolError::CorruptedSerialization(
173                "pending refunds epoch index for must be u16".to_string(),
174            ),
175        )
176    }
177
178    fn to_vec_bytes(&self) -> Vec<u8> {
179        self.encode_var_vec()
180    }
181}
182
183impl Creditable for SignedCredits {
184    fn to_signed(&self) -> Result<SignedCredits, ProtocolError> {
185        Ok(*self)
186    }
187
188    fn to_unsigned(&self) -> Credits {
189        self.unsigned_abs()
190    }
191
192    fn from_vec_bytes(vec: Vec<u8>) -> Result<Self, ProtocolError> {
193        Self::decode_var(vec.as_slice()).map(|(n, _)| n).ok_or(
194            ProtocolError::CorruptedSerialization(
195                "pending refunds epoch index for must be u16".to_string(),
196            ),
197        )
198    }
199
200    fn to_vec_bytes(&self) -> Vec<u8> {
201        self.encode_var_vec()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    mod block_aware_credit_operation {
210        use super::*;
211
212        #[test]
213        fn from_operation_set_credits() {
214            let op =
215                BlockAwareCreditOperation::from_operation(100, &CreditOperation::SetCredits(1000));
216            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1000));
217        }
218
219        #[test]
220        fn from_operation_add_to_credits() {
221            let op =
222                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
223            let expected: BTreeMap<BlockHeight, Credits> = [(100, 500)].into_iter().collect();
224            assert_eq!(
225                op,
226                BlockAwareCreditOperation::AddToCreditsOperations(expected)
227            );
228        }
229
230        #[test]
231        fn merge_set_then_set_takes_latest() {
232            let mut op = BlockAwareCreditOperation::SetCredits(1000);
233            op.merge(101, &CreditOperation::SetCredits(2000));
234            assert_eq!(op, BlockAwareCreditOperation::SetCredits(2000));
235        }
236
237        #[test]
238        fn merge_set_then_add_adds_to_set() {
239            let mut op = BlockAwareCreditOperation::SetCredits(1000);
240            op.merge(101, &CreditOperation::AddToCredits(500));
241            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1500));
242        }
243
244        #[test]
245        fn merge_set_then_multiple_adds() {
246            let mut op = BlockAwareCreditOperation::SetCredits(1000);
247            op.merge(101, &CreditOperation::AddToCredits(500));
248            op.merge(102, &CreditOperation::AddToCredits(300));
249            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1800));
250        }
251
252        #[test]
253        fn merge_add_then_set_becomes_set() {
254            let mut op =
255                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
256            op.merge(101, &CreditOperation::SetCredits(2000));
257            assert_eq!(op, BlockAwareCreditOperation::SetCredits(2000));
258        }
259
260        #[test]
261        fn merge_add_then_add_preserves_block_heights() {
262            let mut op =
263                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
264            op.merge(101, &CreditOperation::AddToCredits(300));
265            op.merge(102, &CreditOperation::AddToCredits(200));
266
267            let expected: BTreeMap<BlockHeight, Credits> =
268                [(100, 500), (101, 300), (102, 200)].into_iter().collect();
269            assert_eq!(
270                op,
271                BlockAwareCreditOperation::AddToCreditsOperations(expected)
272            );
273        }
274
275        #[test]
276        fn merge_multiple_adds_at_same_block_combines() {
277            let mut op =
278                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
279            op.merge(100, &CreditOperation::AddToCredits(300)); // Same block
280
281            let expected: BTreeMap<BlockHeight, Credits> = [(100, 800)].into_iter().collect();
282            assert_eq!(
283                op,
284                BlockAwareCreditOperation::AddToCreditsOperations(expected)
285            );
286        }
287
288        #[test]
289        fn merge_add_then_set_then_add() {
290            // AddToCredits(500) at block 100
291            let mut op =
292                BlockAwareCreditOperation::from_operation(100, &CreditOperation::AddToCredits(500));
293            // SetCredits(1000) at block 101 - wipes out the add
294            op.merge(101, &CreditOperation::SetCredits(1000));
295            // AddToCredits(200) at block 102 - adds to the set
296            op.merge(102, &CreditOperation::AddToCredits(200));
297
298            // Result: SetCredits(1200) because Set wiped previous Add, then new Add was applied
299            assert_eq!(op, BlockAwareCreditOperation::SetCredits(1200));
300        }
301
302        #[test]
303        fn client_sync_scenario() {
304            // This tests the key use case: client synced at block 550,
305            // then receives a compacted range 400-600 with AddToCredits at various blocks.
306            // Client should be able to filter and only apply adds for blocks > 550.
307
308            let mut op =
309                BlockAwareCreditOperation::from_operation(400, &CreditOperation::AddToCredits(100));
310            op.merge(450, &CreditOperation::AddToCredits(200));
311            op.merge(500, &CreditOperation::AddToCredits(300));
312            op.merge(550, &CreditOperation::AddToCredits(400));
313            op.merge(600, &CreditOperation::AddToCredits(500));
314
315            // Verify we have all block heights preserved
316            if let BlockAwareCreditOperation::AddToCreditsOperations(map) = &op {
317                assert_eq!(map.len(), 5);
318
319                // Client synced at 550, so they need to apply blocks > 550
320                let to_apply: Credits = map
321                    .iter()
322                    .filter(|(block, _)| **block > 550)
323                    .map(|(_, credits)| *credits)
324                    .sum();
325
326                // Only block 600's AddToCredits(500) should be applied
327                assert_eq!(to_apply, 500);
328
329                // Client synced at 400, so they need to apply blocks > 400
330                let to_apply_from_400: Credits = map
331                    .iter()
332                    .filter(|(block, _)| **block > 400)
333                    .map(|(_, credits)| *credits)
334                    .sum();
335
336                // Blocks 450, 500, 550, 600: 200 + 300 + 400 + 500 = 1400
337                assert_eq!(to_apply_from_400, 1400);
338            } else {
339                panic!("Expected AddToCreditsOperations");
340            }
341        }
342
343        #[test]
344        fn set_credits_followed_by_adds_scenario() {
345            // SetCredits at block 400, then adds at 500, 600
346            // Client synced at 450, receives range 400-600
347            // Client knows balance was SET at 400, so they start from that value
348            // and only need to apply adds at blocks > 450
349
350            let mut op =
351                BlockAwareCreditOperation::from_operation(400, &CreditOperation::SetCredits(10000));
352            op.merge(500, &CreditOperation::AddToCredits(100));
353            op.merge(600, &CreditOperation::AddToCredits(200));
354
355            // Result is SetCredits(10300) - all operations merged into final value
356            assert_eq!(op, BlockAwareCreditOperation::SetCredits(10300));
357
358            // Note: Once SetCredits is encountered, we lose per-block granularity for adds
359            // This is by design - if the balance was SET, client must use the full compacted value
360        }
361    }
362
363    // -----------------------------------------------------------------------
364    // Creditable::to_signed() on Credits (u64)
365    // -----------------------------------------------------------------------
366
367    #[test]
368    fn credits_to_signed_within_range() {
369        let credits: Credits = 1000;
370        let result = credits.to_signed();
371        assert!(result.is_ok());
372        assert_eq!(result.unwrap(), 1000i64);
373    }
374
375    #[test]
376    fn credits_to_signed_zero() {
377        let credits: Credits = 0;
378        let result = credits.to_signed();
379        assert!(result.is_ok());
380        assert_eq!(result.unwrap(), 0i64);
381    }
382
383    #[test]
384    fn credits_to_signed_max_i64() {
385        let credits: Credits = i64::MAX as u64;
386        let result = credits.to_signed();
387        assert!(result.is_ok());
388        assert_eq!(result.unwrap(), i64::MAX);
389    }
390
391    #[test]
392    fn credits_to_signed_overflow() {
393        // u64::MAX cannot be represented as i64
394        let credits: Credits = u64::MAX;
395        let result = credits.to_signed();
396        assert!(result.is_err());
397        match result.unwrap_err() {
398            ProtocolError::Overflow(msg) => {
399                assert!(msg.contains("too big"));
400            }
401            other => panic!("Expected Overflow error, got: {:?}", other),
402        }
403    }
404
405    #[test]
406    fn credits_to_signed_just_over_i64_max() {
407        // i64::MAX + 1 should overflow
408        let credits: Credits = (i64::MAX as u64) + 1;
409        let result = credits.to_signed();
410        assert!(result.is_err());
411    }
412
413    // -----------------------------------------------------------------------
414    // Creditable::to_unsigned() on Credits (u64)
415    // -----------------------------------------------------------------------
416
417    #[test]
418    fn credits_to_unsigned_returns_self() {
419        let credits: Credits = 42;
420        assert_eq!(credits.to_unsigned(), 42);
421    }
422
423    #[test]
424    fn credits_to_unsigned_zero() {
425        let credits: Credits = 0;
426        assert_eq!(credits.to_unsigned(), 0);
427    }
428
429    #[test]
430    fn credits_to_unsigned_max() {
431        let credits: Credits = u64::MAX;
432        assert_eq!(credits.to_unsigned(), u64::MAX);
433    }
434
435    // -----------------------------------------------------------------------
436    // Creditable on SignedCredits (i64)
437    // -----------------------------------------------------------------------
438
439    #[test]
440    fn signed_credits_to_signed_returns_self() {
441        let sc: SignedCredits = -500;
442        assert_eq!(sc.to_signed().unwrap(), -500);
443    }
444
445    #[test]
446    fn signed_credits_to_unsigned_returns_abs() {
447        let sc: SignedCredits = -500;
448        assert_eq!(sc.to_unsigned(), 500);
449
450        let sc_pos: SignedCredits = 500;
451        assert_eq!(sc_pos.to_unsigned(), 500);
452    }
453
454    #[test]
455    fn signed_credits_to_unsigned_zero() {
456        let sc: SignedCredits = 0;
457        assert_eq!(sc.to_unsigned(), 0);
458    }
459
460    // -----------------------------------------------------------------------
461    // from_vec_bytes / to_vec_bytes round-trip for Credits (u64)
462    // -----------------------------------------------------------------------
463
464    #[test]
465    fn credits_roundtrip_zero() {
466        let original: Credits = 0;
467        let bytes = original.to_vec_bytes();
468        let decoded = Credits::from_vec_bytes(bytes).unwrap();
469        assert_eq!(decoded, original);
470    }
471
472    #[test]
473    fn credits_roundtrip_one() {
474        let original: Credits = 1;
475        let bytes = original.to_vec_bytes();
476        let decoded = Credits::from_vec_bytes(bytes).unwrap();
477        assert_eq!(decoded, original);
478    }
479
480    #[test]
481    fn credits_roundtrip_max() {
482        let original: Credits = u64::MAX;
483        let bytes = original.to_vec_bytes();
484        let decoded = Credits::from_vec_bytes(bytes).unwrap();
485        assert_eq!(decoded, original);
486    }
487
488    #[test]
489    fn credits_roundtrip_large_value() {
490        let original: Credits = 1_000_000_000_000;
491        let bytes = original.to_vec_bytes();
492        let decoded = Credits::from_vec_bytes(bytes).unwrap();
493        assert_eq!(decoded, original);
494    }
495
496    #[test]
497    fn credits_roundtrip_max_credits_constant() {
498        let original: Credits = MAX_CREDITS;
499        let bytes = original.to_vec_bytes();
500        let decoded = Credits::from_vec_bytes(bytes).unwrap();
501        assert_eq!(decoded, original);
502    }
503
504    #[test]
505    fn credits_from_vec_bytes_empty_vec_error() {
506        let result = Credits::from_vec_bytes(vec![]);
507        assert!(result.is_err());
508    }
509
510    // -----------------------------------------------------------------------
511    // from_vec_bytes / to_vec_bytes round-trip for SignedCredits (i64)
512    // -----------------------------------------------------------------------
513
514    #[test]
515    fn signed_credits_roundtrip_zero() {
516        let original: SignedCredits = 0;
517        let bytes = original.to_vec_bytes();
518        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
519        assert_eq!(decoded, original);
520    }
521
522    #[test]
523    fn signed_credits_roundtrip_positive() {
524        let original: SignedCredits = 123456789;
525        let bytes = original.to_vec_bytes();
526        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
527        assert_eq!(decoded, original);
528    }
529
530    #[test]
531    fn signed_credits_roundtrip_negative() {
532        let original: SignedCredits = -987654321;
533        let bytes = original.to_vec_bytes();
534        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
535        assert_eq!(decoded, original);
536    }
537
538    #[test]
539    fn signed_credits_roundtrip_max() {
540        let original: SignedCredits = i64::MAX;
541        let bytes = original.to_vec_bytes();
542        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
543        assert_eq!(decoded, original);
544    }
545
546    #[test]
547    fn signed_credits_roundtrip_min() {
548        let original: SignedCredits = i64::MIN;
549        let bytes = original.to_vec_bytes();
550        let decoded = SignedCredits::from_vec_bytes(bytes).unwrap();
551        assert_eq!(decoded, original);
552    }
553
554    #[test]
555    fn signed_credits_from_vec_bytes_empty_vec_error() {
556        let result = SignedCredits::from_vec_bytes(vec![]);
557        assert!(result.is_err());
558    }
559
560    // -----------------------------------------------------------------------
561    // MAX_CREDITS constant
562    // -----------------------------------------------------------------------
563
564    #[test]
565    fn max_credits_equals_i64_max() {
566        assert_eq!(MAX_CREDITS, i64::MAX as u64);
567    }
568
569    // -----------------------------------------------------------------------
570    // CreditOperation::merge
571    // -----------------------------------------------------------------------
572
573    #[test]
574    fn credit_operation_merge_set_set() {
575        let a = CreditOperation::SetCredits(100);
576        let b = CreditOperation::SetCredits(200);
577        assert_eq!(a.merge(&b), CreditOperation::SetCredits(200));
578    }
579
580    #[test]
581    fn credit_operation_merge_set_add() {
582        let a = CreditOperation::SetCredits(100);
583        let b = CreditOperation::AddToCredits(50);
584        assert_eq!(a.merge(&b), CreditOperation::SetCredits(150));
585    }
586
587    #[test]
588    fn credit_operation_merge_add_set() {
589        let a = CreditOperation::AddToCredits(100);
590        let b = CreditOperation::SetCredits(200);
591        assert_eq!(a.merge(&b), CreditOperation::SetCredits(200));
592    }
593
594    #[test]
595    fn credit_operation_merge_add_add() {
596        let a = CreditOperation::AddToCredits(100);
597        let b = CreditOperation::AddToCredits(50);
598        assert_eq!(a.merge(&b), CreditOperation::AddToCredits(150));
599    }
600
601    #[test]
602    fn credit_operation_merge_set_add_saturating() {
603        let a = CreditOperation::SetCredits(u64::MAX);
604        let b = CreditOperation::AddToCredits(1);
605        // Should saturate, not overflow
606        assert_eq!(a.merge(&b), CreditOperation::SetCredits(u64::MAX));
607    }
608
609    #[test]
610    fn credit_operation_merge_add_add_saturating() {
611        let a = CreditOperation::AddToCredits(u64::MAX);
612        let b = CreditOperation::AddToCredits(1);
613        assert_eq!(a.merge(&b), CreditOperation::AddToCredits(u64::MAX));
614    }
615
616    // -----------------------------------------------------------------------
617    // CREDITS_PER_DUFF constant
618    // -----------------------------------------------------------------------
619
620    #[test]
621    fn credits_per_duff_is_1000() {
622        assert_eq!(CREDITS_PER_DUFF, 1000);
623    }
624}