Skip to main content

icydb_schema/
u256.rs

1//! Canonical fixed-width unsigned 256-bit scalar.
2
3use candid::{CandidType, Nat, types::Serializer, types::Type, types::TypeInner};
4use ethnum::U256 as EthU256;
5use num_bigint::BigUint;
6use serde::{Deserialize, Deserializer, Serialize, Serializer as SerdeSerializer};
7use std::{fmt, str::FromStr};
8
9use crate::{
10    Decimal, NumericValue,
11    integer_wire::{self, IntegerWire},
12};
13
14const MAX_DECIMAL_DIGITS: usize = 78;
15const DECIMAL_CHUNK_BASE: u64 = 100_000_000;
16const DECIMAL_CHUNK_WIDTH: usize = 8;
17const DECIMAL_BUFFER_LEN: usize = 80;
18const U128_LOW_U32_MASK: u128 = 0xffff_ffff;
19
20/// Error returned when text or a Candid natural is outside the unsigned
21/// 256-bit domain.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct ParseU256Error;
24
25impl fmt::Display for ParseU256Error {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        formatter.write_str("value is not an unsigned 256-bit integer")
28    }
29}
30
31impl std::error::Error for ParseU256Error {}
32
33/// IcyDB-owned fixed-width unsigned 256-bit scalar.
34///
35/// Runtime values are inline and allocation-free. Candid exposes this type as
36/// `nat`; ingress rejects values greater than [`U256::MAX`]. Persistence and
37/// index encodings are owned separately by IcyDB. Human-readable Serde uses
38/// decimal text; binary Serde uses u64 or tagged little-endian magnitude bytes.
39#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
40#[repr(transparent)]
41pub struct U256(EthU256);
42
43impl U256 {
44    /// Minimum unsigned 256-bit value.
45    pub const MIN: Self = Self::ZERO;
46
47    /// Zero.
48    pub const ZERO: Self = Self(EthU256::ZERO);
49
50    /// One.
51    pub const ONE: Self = Self(EthU256::ONE);
52
53    /// Maximum unsigned 256-bit value.
54    pub const MAX: Self = Self(EthU256::MAX);
55
56    /// Build from two 128-bit words in numeric high/low order.
57    #[must_use]
58    pub const fn from_words(high: u128, low: u128) -> Self {
59        Self(EthU256::from_words(high, low))
60    }
61
62    /// Split into two 128-bit words in numeric high/low order.
63    #[must_use]
64    pub const fn into_words(self) -> (u128, u128) {
65        self.0.into_words()
66    }
67
68    /// Build from exactly 32 unsigned big-endian bytes.
69    #[must_use]
70    pub fn from_be_bytes(bytes: [u8; 32]) -> Self {
71        Self(EthU256::from_be_bytes(bytes))
72    }
73
74    /// Return exactly 32 unsigned big-endian bytes.
75    #[must_use]
76    pub fn to_be_bytes(self) -> [u8; 32] {
77        self.0.to_be_bytes()
78    }
79
80    /// Convert to `u128` when the value is in range.
81    #[must_use]
82    pub const fn to_u128(self) -> Option<u128> {
83        let (high, low) = self.into_words();
84        if high == 0 { Some(low) } else { None }
85    }
86
87    /// Add two values, returning `None` on unsigned 256-bit overflow.
88    #[must_use]
89    pub fn checked_add(self, rhs: Self) -> Option<Self> {
90        self.0.checked_add(rhs.0).map(Self)
91    }
92
93    /// Subtract two values, returning `None` on unsigned 256-bit underflow.
94    #[must_use]
95    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
96        self.0.checked_sub(rhs.0).map(Self)
97    }
98
99    /// Multiply two values, returning `None` on unsigned 256-bit overflow.
100    #[must_use]
101    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
102        self.0.checked_mul(rhs.0).map(Self)
103    }
104
105    /// Divide two values, returning `None` when the divisor is zero.
106    #[must_use]
107    pub fn checked_div(self, rhs: Self) -> Option<Self> {
108        self.0.checked_div(rhs.0).map(Self)
109    }
110
111    /// Return the remainder, or `None` when the divisor is zero.
112    #[must_use]
113    pub fn checked_rem(self, rhs: Self) -> Option<Self> {
114        self.0.checked_rem(rhs.0).map(Self)
115    }
116
117    fn from_little_endian_magnitude(bytes: &[u8]) -> Result<Self, ParseU256Error> {
118        if bytes.len() > 32 {
119            return Err(ParseU256Error);
120        }
121        let mut fixed = [0_u8; 32];
122        for (destination, source) in fixed.iter_mut().rev().zip(bytes) {
123            *destination = *source;
124        }
125        Ok(Self::from_be_bytes(fixed))
126    }
127
128    fn to_candid_nat(self) -> Nat {
129        Nat(BigUint::from_bytes_be(&self.to_be_bytes()))
130    }
131
132    fn decimal_text(self) -> DecimalText {
133        let (high, low) = self.into_words();
134        let mut limbs = [
135            low_u32_from_u128(high >> 96),
136            low_u32_from_u128(high >> 64),
137            low_u32_from_u128(high >> 32),
138            low_u32_from_u128(high),
139            low_u32_from_u128(low >> 96),
140            low_u32_from_u128(low >> 64),
141            low_u32_from_u128(low >> 32),
142            low_u32_from_u128(low),
143        ];
144        let mut bytes = [0_u8; DECIMAL_BUFFER_LEN];
145        let mut start = bytes.len();
146
147        loop {
148            let mut remainder = 0_u64;
149            let mut quotient_is_zero = true;
150            for limb in &mut limbs {
151                let dividend = (remainder << 32) | u64::from(*limb);
152                let quotient = dividend / DECIMAL_CHUNK_BASE;
153                remainder = dividend % DECIMAL_CHUNK_BASE;
154                // Long division keeps the quotient within one base-2^32 limb.
155                *limb = u32::try_from(quotient).unwrap_or_default();
156                quotient_is_zero &= quotient == 0;
157            }
158
159            // The remainder is strictly below the 1e8 decimal chunk base.
160            let mut chunk = u32::try_from(remainder).unwrap_or_default();
161            for _ in 0..DECIMAL_CHUNK_WIDTH {
162                start -= 1;
163                bytes[start] = b'0' + u8::try_from(chunk % 10).unwrap_or_default();
164                chunk /= 10;
165            }
166            if quotient_is_zero {
167                break;
168            }
169        }
170        while start + 1 < bytes.len() && bytes[start] == b'0' {
171            start += 1;
172        }
173
174        DecimalText { bytes, start }
175    }
176}
177
178fn low_u32_from_u128(value: u128) -> u32 {
179    u32::try_from(value & U128_LOW_U32_MASK).unwrap_or_default()
180}
181
182struct DecimalText {
183    bytes: [u8; DECIMAL_BUFFER_LEN],
184    start: usize,
185}
186
187impl DecimalText {
188    fn as_str(&self) -> &str {
189        std::str::from_utf8(&self.bytes[self.start..]).unwrap_or_default()
190    }
191}
192
193impl CandidType for U256 {
194    fn ty() -> Type {
195        TypeInner::Nat.into()
196    }
197
198    fn _ty() -> Type {
199        TypeInner::Nat.into()
200    }
201
202    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
203    where
204        S: Serializer,
205    {
206        serializer.serialize_nat(&self.to_candid_nat())
207    }
208}
209
210impl fmt::Debug for U256 {
211    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212        fmt::Display::fmt(self, formatter)
213    }
214}
215
216impl fmt::Display for U256 {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        let text = self.decimal_text();
219        formatter.write_str(text.as_str())
220    }
221}
222
223impl From<u64> for U256 {
224    fn from(value: u64) -> Self {
225        Self(EthU256::from(value))
226    }
227}
228
229impl From<u128> for U256 {
230    fn from(value: u128) -> Self {
231        Self(EthU256::from(value))
232    }
233}
234
235impl FromStr for U256 {
236    type Err = ParseU256Error;
237
238    fn from_str(value: &str) -> Result<Self, Self::Err> {
239        if value.is_empty()
240            || value.len() > MAX_DECIMAL_DIGITS
241            || !value.bytes().all(|byte| byte.is_ascii_digit())
242        {
243            return Err(ParseU256Error);
244        }
245        value
246            .parse::<EthU256>()
247            .map(Self)
248            .map_err(|_| ParseU256Error)
249    }
250}
251
252impl NumericValue for U256 {
253    fn try_to_decimal(&self) -> Option<Decimal> {
254        self.to_u128().and_then(Decimal::from_u128)
255    }
256
257    fn try_from_decimal(value: Decimal) -> Option<Self> {
258        value.to_u128().map(Self::from)
259    }
260}
261
262impl<'de> Deserialize<'de> for U256 {
263    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264    where
265        D: Deserializer<'de>,
266    {
267        integer_wire::deserialize_integer(deserializer)
268    }
269}
270
271impl IntegerWire for U256 {
272    fn from_signed(value: i64) -> Option<Self> {
273        u64::try_from(value).ok().map(Self::from)
274    }
275
276    fn from_unsigned(value: u64) -> Self {
277        Self::from(value)
278    }
279
280    fn from_wire_bytes(value: &[u8]) -> Option<Self> {
281        Self::from_little_endian_magnitude(integer_wire::unsigned_body(value)?).ok()
282    }
283}
284
285impl Serialize for U256 {
286    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
287    where
288        S: SerdeSerializer,
289    {
290        if serializer.is_human_readable() {
291            return serializer.collect_str(self);
292        }
293        if let Some(value) = self.to_u128().and_then(|value| u64::try_from(value).ok()) {
294            return serializer.serialize_u64(value);
295        }
296        // Keep this fixed-width atom's binary serialization on the stack.
297        let magnitude = self.0.to_le_bytes();
298        let len = magnitude
299            .iter()
300            .rposition(|byte| *byte != 0)
301            .map_or(1, |index| index + 1);
302        let mut bytes = [0_u8; 33];
303        bytes[0] = 1;
304        bytes[1..=len].copy_from_slice(&magnitude[..len]);
305        serializer.serialize_bytes(&bytes[..=len])
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::U256;
312    use crate::{Decimal, NumericValue};
313    use candid::{CandidType, decode_one, encode_one};
314    use num_bigint::BigUint;
315
316    #[test]
317    fn candid_uses_nat_and_rejects_values_above_maximum() {
318        assert_eq!(U256::ty(), candid::Nat::ty());
319
320        let encoded = encode_one(U256::MAX).expect("U256 should encode");
321        assert_eq!(
322            decode_one::<U256>(&encoded).expect("U256 should decode"),
323            U256::MAX
324        );
325
326        let above = candid::Nat(BigUint::from(1_u8) << 256_usize);
327        let encoded = encode_one(above).expect("Nat should encode");
328        assert!(decode_one::<U256>(&encoded).is_err());
329    }
330
331    #[test]
332    fn fixed_bytes_and_decimal_are_exact() {
333        let value = "57896044618658097711785492504343953926634992332820282019728792003956564819968"
334            .parse::<U256>()
335            .expect("2^255 should parse");
336        assert_eq!(value.to_be_bytes()[0], 0x80);
337        assert_eq!(U256::from_be_bytes(value.to_be_bytes()), value);
338        assert_eq!(
339            value.to_string(),
340            "57896044618658097711785492504343953926634992332820282019728792003956564819968"
341        );
342        assert_eq!(U256::ZERO.to_string(), "0");
343        assert_eq!(U256::ONE.to_string(), "1");
344        assert_eq!(U256::MAX.to_string(), u256_max_decimal());
345    }
346
347    #[test]
348    fn checked_arithmetic_enforces_the_u256_domain() {
349        let two = U256::from(2_u64);
350        let three = U256::from(3_u64);
351
352        assert_eq!(two.checked_add(three), Some(U256::from(5_u64)));
353        assert_eq!(three.checked_sub(two), Some(U256::ONE));
354        assert_eq!(two.checked_mul(three), Some(U256::from(6_u64)));
355        assert_eq!(U256::from(7_u64).checked_div(two), Some(three));
356        assert_eq!(U256::from(7_u64).checked_rem(two), Some(U256::ONE));
357        assert_eq!(U256::MAX.checked_add(U256::ONE), None);
358        assert_eq!(U256::ZERO.checked_sub(U256::ONE), None);
359        assert_eq!(U256::MAX.checked_mul(two), None);
360        assert_eq!(U256::ONE.checked_div(U256::ZERO), None);
361        assert_eq!(U256::ONE.checked_rem(U256::ZERO), None);
362    }
363
364    #[test]
365    fn generic_numeric_conversion_is_fallible_without_widening_the_u256_domain() {
366        let value = U256::from(u128::try_from(i128::MAX).expect("i128::MAX should fit u128"));
367        assert_eq!(
368            value.try_to_decimal().and_then(U256::try_from_decimal),
369            Some(value),
370        );
371        assert_eq!(U256::MAX.try_to_decimal(), None);
372        let negative_one = Decimal::from_i128(-1).expect("-1 should be a valid Decimal");
373        assert_eq!(U256::try_from_decimal(negative_one), None);
374    }
375
376    fn u256_max_decimal() -> &'static str {
377        "115792089237316195423570985008687907853269984665640564039457584007913129639935"
378    }
379}