Skip to main content

amaru_uplc/
ledger_value.rs

1use bumpalo::collections::Vec as BumpVec;
2use num::{Signed, Zero};
3
4use crate::{
5    arena::Arena,
6    constant::{integer, Integer},
7    data::PlutusData,
8};
9
10#[derive(thiserror::Error, Debug)]
11pub enum UnValueDataError {
12    #[error("non-Map constructor")]
13    NonMapConstructor,
14    #[error("non-B constructor")]
15    NonByteStringConstructor,
16    #[error("non-I constructor")]
17    NonIntegerConstructor,
18    #[error("invalid key")]
19    InvalidKey,
20    #[error("empty inner map")]
21    EmptyInnerMap,
22    #[error("currency symbols not strictly ascending")]
23    CurrencyNotAscending,
24    #[error("token names not strictly ascending")]
25    TokenNotAscending,
26    #[error("invalid quantity")]
27    InvalidQuantity,
28}
29
30#[derive(thiserror::Error, Debug)]
31pub enum ValueError {
32    #[error("insertCoin: invalid currency")]
33    InsertCoinInvalidCurrency,
34    #[error("insertCoin: invalid token")]
35    InsertCoinInvalidToken,
36    #[error("unionValue: quantity is out of the signed 128-bit integer bounds")]
37    UnionValueQuantityOutOfBounds,
38    #[error("valueContains: first value contains negative amounts")]
39    ValueContainsFirstNegative,
40    #[error("valueContains: second value contains negative amounts")]
41    ValueContainsSecondNegative,
42    #[error("scaleValue: quantity out of bounds")]
43    ScaleValueQuantityOutOfBounds,
44    #[error("valueData: maximum input size ({0}) exceeded")]
45    ValueDataMaxSizeExceeded(usize),
46    #[error("unValueData: {0}")]
47    UnValueData(#[from] UnValueDataError),
48    #[error("Quantity out of signed 128-bit integer bounds")]
49    QuantityOutOfBounds,
50}
51
52#[derive(Debug, PartialEq)]
53pub struct LedgerValue<'a> {
54    pub entries: &'a [CurrencyEntry<'a>],
55    pub size: usize,
56    pub negative_count: usize,
57}
58
59#[derive(Debug, PartialEq, Clone)]
60pub struct CurrencyEntry<'a> {
61    pub currency: &'a [u8],
62    pub tokens: &'a [TokenEntry<'a>],
63}
64
65#[derive(Debug, PartialEq, Clone)]
66pub struct TokenEntry<'a> {
67    pub name: &'a [u8],
68    pub quantity: &'a Integer,
69}
70
71impl<'a> LedgerValue<'a> {
72    pub fn empty(arena: &'a Arena) -> &'a LedgerValue<'a> {
73        arena.alloc(LedgerValue {
74            entries: &[],
75            size: 0,
76            negative_count: 0,
77        })
78    }
79
80    pub fn lookup_coin(&self, arena: &'a Arena, ccy: &[u8], tok: &[u8]) -> &'a Integer {
81        for entry in self.entries {
82            match entry.currency.cmp(ccy) {
83                std::cmp::Ordering::Equal => {
84                    for token in entry.tokens {
85                        match token.name.cmp(tok) {
86                            std::cmp::Ordering::Equal => return token.quantity,
87                            std::cmp::Ordering::Greater => break,
88                            _ => {}
89                        }
90                    }
91                    return integer(arena);
92                }
93                std::cmp::Ordering::Greater => break,
94                _ => {}
95            }
96        }
97        integer(arena)
98    }
99
100    pub fn insert_coin(
101        arena: &'a Arena,
102        ccy: &'a [u8],
103        tok: &'a [u8],
104        qty: &'a Integer,
105        v: &'a LedgerValue<'a>,
106    ) -> &'a LedgerValue<'a> {
107        let mut currency_entries = BumpVec::new_in(arena.as_bump());
108        let mut found_ccy = false;
109
110        for entry in v.entries {
111            match entry.currency.cmp(ccy) {
112                std::cmp::Ordering::Less => {
113                    currency_entries.push(entry.clone());
114                }
115                std::cmp::Ordering::Equal => {
116                    found_ccy = true;
117                    let mut token_entries = BumpVec::new_in(arena.as_bump());
118                    let mut found_tok = false;
119
120                    for token in entry.tokens {
121                        match token.name.cmp(tok) {
122                            std::cmp::Ordering::Less => {
123                                token_entries.push(token.clone());
124                            }
125                            std::cmp::Ordering::Equal => {
126                                found_tok = true;
127                                if !qty.is_zero() {
128                                    token_entries.push(TokenEntry {
129                                        name: tok,
130                                        quantity: qty,
131                                    });
132                                }
133                            }
134                            std::cmp::Ordering::Greater => {
135                                if !found_tok {
136                                    found_tok = true;
137                                    if !qty.is_zero() {
138                                        token_entries.push(TokenEntry {
139                                            name: tok,
140                                            quantity: qty,
141                                        });
142                                    }
143                                }
144                                token_entries.push(token.clone());
145                            }
146                        }
147                    }
148                    if !found_tok && !qty.is_zero() {
149                        token_entries.push(TokenEntry {
150                            name: tok,
151                            quantity: qty,
152                        });
153                    }
154
155                    let tokens: &'a [TokenEntry<'a>] = arena.alloc(token_entries);
156                    if !tokens.is_empty() {
157                        currency_entries.push(CurrencyEntry {
158                            currency: entry.currency,
159                            tokens,
160                        });
161                    }
162                }
163                std::cmp::Ordering::Greater => {
164                    if !found_ccy {
165                        found_ccy = true;
166                        if !qty.is_zero() {
167                            let tokens: &'a [TokenEntry<'a>] = arena.alloc([TokenEntry {
168                                name: tok,
169                                quantity: qty,
170                            }]);
171                            currency_entries.push(CurrencyEntry {
172                                currency: ccy,
173                                tokens,
174                            });
175                        }
176                    }
177                    currency_entries.push(entry.clone());
178                }
179            }
180        }
181
182        if !found_ccy && !qty.is_zero() {
183            let tokens: &'a [TokenEntry<'a>] = arena.alloc([TokenEntry {
184                name: tok,
185                quantity: qty,
186            }]);
187            currency_entries.push(CurrencyEntry {
188                currency: ccy,
189                tokens,
190            });
191        }
192
193        let entries: &'a [CurrencyEntry<'a>] = arena.alloc(currency_entries);
194        let (size, negative_count) = count_stats(entries);
195        arena.alloc(LedgerValue {
196            entries,
197            size,
198            negative_count,
199        })
200    }
201
202    pub fn union_value(
203        arena: &'a Arena,
204        v1: &'a LedgerValue<'a>,
205        v2: &'a LedgerValue<'a>,
206    ) -> Result<&'a LedgerValue<'a>, ValueError> {
207        let mut currency_entries = BumpVec::new_in(arena.as_bump());
208        let mut i = 0usize;
209        let mut j = 0usize;
210
211        while i < v1.entries.len() && j < v2.entries.len() {
212            match v1.entries[i].currency.cmp(v2.entries[j].currency) {
213                std::cmp::Ordering::Less => {
214                    currency_entries.push(v1.entries[i].clone());
215                    i += 1;
216                }
217                std::cmp::Ordering::Greater => {
218                    currency_entries.push(v2.entries[j].clone());
219                    j += 1;
220                }
221                std::cmp::Ordering::Equal => {
222                    let merged = merge_tokens(arena, v1.entries[i].tokens, v2.entries[j].tokens)?;
223                    if !merged.is_empty() {
224                        currency_entries.push(CurrencyEntry {
225                            currency: v1.entries[i].currency,
226                            tokens: merged,
227                        });
228                    }
229                    i += 1;
230                    j += 1;
231                }
232            }
233        }
234
235        while i < v1.entries.len() {
236            currency_entries.push(v1.entries[i].clone());
237            i += 1;
238        }
239        while j < v2.entries.len() {
240            currency_entries.push(v2.entries[j].clone());
241            j += 1;
242        }
243
244        let entries: &'a [CurrencyEntry<'a>] = arena.alloc(currency_entries);
245        let (size, negative_count) = count_stats(entries);
246        Ok(arena.alloc(LedgerValue {
247            entries,
248            size,
249            negative_count,
250        }))
251    }
252
253    pub fn value_contains(v1: &LedgerValue<'a>, v2: &LedgerValue<'a>) -> Result<bool, ValueError> {
254        // 1. Check v1 for negatives
255        if v1.negative_count > 0 {
256            return Err(ValueError::ValueContainsFirstNegative);
257        }
258
259        // 2. Check v2 for negatives
260        if v2.negative_count > 0 {
261            return Err(ValueError::ValueContainsSecondNegative);
262        }
263
264        // 3. v2 has more tokens than v1, so v1 cannot contain v2
265        if v1.size < v2.size {
266            return Ok(false);
267        }
268
269        // 4. Sorted lockstep check that v2 is a submap of v1
270        let mut v1_iter = v1.entries.iter().flat_map(|entry| {
271            entry
272                .tokens
273                .iter()
274                .map(move |token| (entry.currency, token.name, token.quantity))
275        });
276        let mut v1_current = v1_iter.next();
277
278        for v2_entry in v2.entries {
279            for v2_token in v2_entry.tokens {
280                loop {
281                    match v1_current {
282                        Some((v1_ccy, v1_name, v1_qty)) => {
283                            match (v1_ccy, v1_name).cmp(&(v2_entry.currency, v2_token.name)) {
284                                std::cmp::Ordering::Less => {
285                                    v1_current = v1_iter.next();
286                                }
287                                std::cmp::Ordering::Equal => {
288                                    if v1_qty < v2_token.quantity {
289                                        return Ok(false);
290                                    }
291                                    v1_current = v1_iter.next();
292                                    break;
293                                }
294                                std::cmp::Ordering::Greater => {
295                                    return Ok(false);
296                                }
297                            }
298                        }
299                        None => return Ok(false),
300                    }
301                }
302            }
303        }
304
305        Ok(true)
306    }
307
308    pub fn scale_value(
309        arena: &'a Arena,
310        scalar: &'a Integer,
311        v: &'a LedgerValue<'a>,
312    ) -> Result<&'a LedgerValue<'a>, ValueError> {
313        if scalar.is_zero() {
314            return Ok(LedgerValue::empty(arena));
315        }
316
317        let mut currency_entries = BumpVec::new_in(arena.as_bump());
318
319        for entry in v.entries {
320            let mut token_entries = BumpVec::new_in(arena.as_bump());
321
322            for token in entry.tokens {
323                let result = token.quantity * scalar;
324                check_quantity_range(&result)
325                    .map_err(|_| ValueError::ScaleValueQuantityOutOfBounds)?;
326
327                if !result.is_zero() {
328                    let qty = arena.alloc_integer(result);
329                    token_entries.push(TokenEntry {
330                        name: token.name,
331                        quantity: qty,
332                    });
333                }
334            }
335
336            let tokens: &'a [TokenEntry<'a>] = arena.alloc(token_entries);
337            if !tokens.is_empty() {
338                currency_entries.push(CurrencyEntry {
339                    currency: entry.currency,
340                    tokens,
341                });
342            }
343        }
344
345        let entries: &'a [CurrencyEntry<'a>] = arena.alloc(currency_entries);
346        let (size, negative_count) = count_stats(entries);
347        Ok(arena.alloc(LedgerValue {
348            entries,
349            size,
350            negative_count,
351        }))
352    }
353
354    pub fn value_data(
355        arena: &'a Arena,
356        v: &'a LedgerValue<'a>,
357    ) -> Result<&'a PlutusData<'a>, ValueError> {
358        const VALUE_DATA_MAX_SIZE: usize = 40_000;
359
360        if v.size > VALUE_DATA_MAX_SIZE {
361            return Err(ValueError::ValueDataMaxSizeExceeded(VALUE_DATA_MAX_SIZE));
362        }
363
364        let mut outer_pairs = BumpVec::new_in(arena.as_bump());
365
366        for entry in v.entries {
367            let ccy_data = PlutusData::byte_string(arena, entry.currency);
368
369            let mut inner_pairs = BumpVec::new_in(arena.as_bump());
370
371            for token in entry.tokens {
372                let tok_data = PlutusData::byte_string(arena, token.name);
373                let qty_data = PlutusData::integer(arena, token.quantity);
374                inner_pairs.push((tok_data as &PlutusData, qty_data as &PlutusData));
375            }
376
377            let inner_pairs: &'a [_] = arena.alloc(inner_pairs);
378            let inner_map = PlutusData::map(arena, inner_pairs);
379            outer_pairs.push((ccy_data as &PlutusData, inner_map as &PlutusData));
380        }
381
382        let outer_pairs: &'a [_] = arena.alloc(outer_pairs);
383        Ok(PlutusData::map(arena, outer_pairs))
384    }
385
386    pub fn un_value_data(
387        arena: &'a Arena,
388        d: &'a PlutusData<'a>,
389    ) -> Result<&'a LedgerValue<'a>, ValueError> {
390        let outer_map = match d {
391            PlutusData::Map(m) => m,
392            _ => return Err(UnValueDataError::NonMapConstructor.into()),
393        };
394
395        let mut currency_entries = BumpVec::new_in(arena.as_bump());
396        let mut prev_ccy: Option<&[u8]> = None;
397
398        for (key, value) in outer_map.iter() {
399            let ccy = match key {
400                PlutusData::ByteString(bs) => *bs,
401                _ => return Err(UnValueDataError::NonByteStringConstructor.into()),
402            };
403
404            if ccy.len() > 32 {
405                return Err(UnValueDataError::InvalidKey.into());
406            }
407
408            // Check strictly ascending order
409            if let Some(prev) = prev_ccy {
410                if prev.cmp(ccy) != std::cmp::Ordering::Less {
411                    return Err(UnValueDataError::CurrencyNotAscending.into());
412                }
413            }
414            prev_ccy = Some(ccy);
415
416            let inner_map = match value {
417                PlutusData::Map(m) => m,
418                _ => return Err(UnValueDataError::NonMapConstructor.into()),
419            };
420
421            if inner_map.is_empty() {
422                return Err(UnValueDataError::EmptyInnerMap.into());
423            }
424
425            let mut token_entries = BumpVec::new_in(arena.as_bump());
426            let mut prev_tok: Option<&[u8]> = None;
427
428            for (inner_key, inner_value) in inner_map.iter() {
429                let tok = match inner_key {
430                    PlutusData::ByteString(bs) => *bs,
431                    _ => return Err(UnValueDataError::NonByteStringConstructor.into()),
432                };
433
434                if tok.len() > 32 {
435                    return Err(UnValueDataError::InvalidKey.into());
436                }
437
438                // Check strictly ascending order
439                if let Some(prev) = prev_tok {
440                    if prev.cmp(tok) != std::cmp::Ordering::Less {
441                        return Err(UnValueDataError::TokenNotAscending.into());
442                    }
443                }
444                prev_tok = Some(tok);
445
446                let qty = match inner_value {
447                    PlutusData::Integer(i) => *i,
448                    _ => return Err(UnValueDataError::NonIntegerConstructor.into()),
449                };
450
451                if qty.is_zero() {
452                    return Err(UnValueDataError::InvalidQuantity.into());
453                }
454
455                check_quantity_range(qty)
456                    .map_err(|_| ValueError::from(UnValueDataError::InvalidQuantity))?;
457
458                token_entries.push(TokenEntry {
459                    name: tok,
460                    quantity: qty,
461                });
462            }
463
464            let tokens: &'a [TokenEntry<'a>] = arena.alloc(token_entries);
465            currency_entries.push(CurrencyEntry {
466                currency: ccy,
467                tokens,
468            });
469        }
470
471        let entries: &'a [CurrencyEntry<'a>] = arena.alloc(currency_entries);
472        let (size, negative_count) = count_stats(entries);
473        Ok(arena.alloc(LedgerValue {
474            entries,
475            size,
476            negative_count,
477        }))
478    }
479}
480
481pub fn count_stats(entries: &[CurrencyEntry]) -> (usize, usize) {
482    let mut total_size = 0usize;
483    let mut negative_count = 0usize;
484    for e in entries {
485        for t in e.tokens {
486            total_size += 1;
487            if t.quantity.is_negative() {
488                negative_count += 1;
489            }
490        }
491    }
492    (total_size, negative_count)
493}
494
495fn merge_tokens<'a>(
496    arena: &'a Arena,
497    t1: &[TokenEntry<'a>],
498    t2: &[TokenEntry<'a>],
499) -> Result<&'a [TokenEntry<'a>], ValueError> {
500    let mut result = BumpVec::new_in(arena.as_bump());
501    let mut i = 0usize;
502    let mut j = 0usize;
503
504    while i < t1.len() && j < t2.len() {
505        match t1[i].name.cmp(t2[j].name) {
506            std::cmp::Ordering::Less => {
507                result.push(t1[i].clone());
508                i += 1;
509            }
510            std::cmp::Ordering::Greater => {
511                result.push(t2[j].clone());
512                j += 1;
513            }
514            std::cmp::Ordering::Equal => {
515                let sum = t1[i].quantity + t2[j].quantity;
516                check_quantity_range(&sum)
517                    .map_err(|_| ValueError::UnionValueQuantityOutOfBounds)?;
518                if !sum.is_zero() {
519                    let qty = arena.alloc_integer(sum);
520                    result.push(TokenEntry {
521                        name: t1[i].name,
522                        quantity: qty,
523                    });
524                }
525                i += 1;
526                j += 1;
527            }
528        }
529    }
530
531    while i < t1.len() {
532        result.push(t1[i].clone());
533        i += 1;
534    }
535    while j < t2.len() {
536        result.push(t2[j].clone());
537        j += 1;
538    }
539
540    Ok(arena.alloc(result) as &'a [TokenEntry<'a>])
541}
542
543/// Check that a quantity is within the 128-bit signed range: -(2^127) to (2^127 - 1).
544pub fn check_quantity_range(int: &Integer) -> Result<(), ValueError> {
545    let bits = int.bits();
546    if bits <= 127 {
547        return Ok(());
548    }
549    if bits > 128 {
550        return Err(ValueError::QuantityOutOfBounds);
551    }
552    // bits == 128: only valid if negative and exactly -(2^127)
553    if !int.is_negative() {
554        return Err(ValueError::QuantityOutOfBounds);
555    }
556    let magnitude = int.magnitude();
557    use num::One;
558    let two_pow_127 = num::BigUint::one() << 127;
559    if *magnitude == two_pow_127 {
560        Ok(())
561    } else {
562        Err(ValueError::QuantityOutOfBounds)
563    }
564}
565
566pub fn value_max_depth(v: &LedgerValue) -> i64 {
567    let outer_size = v.entries.len();
568    let mut max_inner = 0usize;
569    for entry in v.entries {
570        if entry.tokens.len() > max_inner {
571            max_inner = entry.tokens.len();
572        }
573    }
574    let log_outer: i64 = if outer_size > 0 {
575        (outer_size as f64).log2() as i64 + 1
576    } else {
577        0
578    };
579    let log_inner: i64 = if max_inner > 0 {
580        (max_inner as f64).log2() as i64 + 1
581    } else {
582        0
583    };
584    log_outer + log_inner
585}
586
587pub fn data_node_count(d: &PlutusData) -> i64 {
588    let mut total: i64 = 0;
589    let mut stack: Vec<&PlutusData> = vec![d];
590
591    while let Some(current) = stack.pop() {
592        total += 1;
593        match current {
594            PlutusData::Constr { fields, .. } => {
595                for field in fields.iter() {
596                    stack.push(field);
597                }
598            }
599            PlutusData::Map(pairs) => {
600                for (key, value) in pairs.iter() {
601                    stack.push(key);
602                    stack.push(value);
603                }
604            }
605            PlutusData::List(items) => {
606                for item in items.iter() {
607                    stack.push(item);
608                }
609            }
610            PlutusData::Integer(_) | PlutusData::ByteString(_) => {}
611        }
612    }
613
614    total
615}