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 if v1.negative_count > 0 {
256 return Err(ValueError::ValueContainsFirstNegative);
257 }
258
259 if v2.negative_count > 0 {
261 return Err(ValueError::ValueContainsSecondNegative);
262 }
263
264 if v1.size < v2.size {
266 return Ok(false);
267 }
268
269 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 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 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
543pub 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 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}