Skip to content

Commit 156de10

Browse files
authored
feat: add a u256 type (#141)
1 parent dde2695 commit 156de10

File tree

9 files changed

+1164
-1
lines changed

9 files changed

+1164
-1
lines changed

crates/starknet-types-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ arbitrary = { version = "1.3", optional = true }
2121
blake2 = { version = "0.10.6", default-features = false, optional = true }
2222
digest = { version = "0.10.7", optional = true }
2323
serde = { version = "1", optional = true, default-features = false, features = [
24-
"alloc",
24+
"alloc", "derive"
2525
] }
2626
lambdaworks-crypto = { version = "0.10.0", default-features = false, optional = true }
2727
parity-scale-codec = { version = "3.6", default-features = false, optional = true }

crates/starknet-types-core/src/felt/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use core::str::FromStr;
3131
use num_bigint::{BigInt, BigUint, Sign};
3232
use num_integer::Integer;
3333
use num_traits::{One, Zero};
34+
pub use primitive_conversions::PrimitiveFromFeltError;
3435

3536
#[cfg(feature = "alloc")]
3637
pub extern crate alloc;

crates/starknet-types-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ pub mod felt;
99

1010
#[cfg(any(feature = "std", feature = "alloc"))]
1111
pub mod short_string;
12+
pub mod u256;
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//! A Cairo-like u256 type.
2+
//!
3+
//! This `U256` type purpose is not to be used to perfomr arithmetic operations,
4+
//! but rather to offer a handy interface to convert from and to Cairo's u256 values.
5+
//! Indeed, the Cairo language represent u256 values as a two felts struct,
6+
//! representing the `low` and `high` 128 bits of the value.
7+
//! We mirror this representation, allowing for efficient serialization/deserializatin.
8+
//!
9+
//! We recommand you create From/Into implementation to bridge the gap between your favourite u256 type,
10+
//! and the one provided by this crate.
11+
12+
#[cfg(feature = "num-traits")]
13+
mod num_traits_impl;
14+
mod primitive_conversions;
15+
#[cfg(test)]
16+
mod tests;
17+
18+
use core::{fmt::Debug, str::FromStr};
19+
20+
use crate::felt::{Felt, PrimitiveFromFeltError};
21+
22+
/// Error types that can occur when parsing a string into a U256.
23+
#[derive(Debug)]
24+
pub enum FromStrError {
25+
/// The string contain too many characters to be the representation of a valid u256 value.
26+
StringTooLong,
27+
/// The parsed value exceeds the maximum representable value for U256.
28+
ValueTooBig,
29+
/// The string contains invalid characters for the expected format.
30+
Invalid,
31+
/// Underlying u128 parsing failed.
32+
Parse(<u128 as FromStr>::Err),
33+
}
34+
35+
impl core::fmt::Display for FromStrError {
36+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37+
match self {
38+
FromStrError::ValueTooBig => core::fmt::Display::fmt("value too big for u256", f),
39+
FromStrError::Invalid => core::fmt::Display::fmt("invalid characters", f),
40+
FromStrError::Parse(e) => {
41+
// Avoid using format as it requires `alloc`
42+
core::fmt::Display::fmt("invalid string: ", f)?;
43+
core::fmt::Display::fmt(e, f)
44+
}
45+
FromStrError::StringTooLong => {
46+
core::fmt::Display::fmt("too many characters to be a valid u256 represenation", f)
47+
}
48+
}
49+
}
50+
}
51+
52+
#[cfg(feature = "std")]
53+
impl std::error::Error for FromStrError {}
54+
55+
/// A 256-bit unsigned integer represented as two 128-bit components.
56+
///
57+
/// The internal representation uses big-endian ordering where `high` contains
58+
/// the most significant 128 bits and `low` contains the least significant 128 bits.
59+
/// This reflects the way u256 are represented in the Cairo language.
60+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
62+
pub struct U256 {
63+
high: u128,
64+
low: u128,
65+
}
66+
67+
impl U256 {
68+
/// Returns the high 128 bits of the U256 value.
69+
pub fn high(&self) -> u128 {
70+
self.high
71+
}
72+
73+
/// Returns the low 128 bits of the U256 value.
74+
pub fn low(&self) -> u128 {
75+
self.low
76+
}
77+
78+
/// Constructs a U256 from explicit high and low 128-bit components.
79+
///
80+
/// This is the most direct way to create a U256 when you already have
81+
/// the component values separated.
82+
pub fn from_parts(high: u128, low: u128) -> Self {
83+
Self { low, high }
84+
}
85+
86+
/// Attempts to construct a U256 from two Felt values representing high and low parts.
87+
///
88+
/// This conversion can fail if either Felt value cannot be represented as a u128,
89+
/// which would indicate the Felt contains a value outside the valid range.
90+
pub fn try_from_felt_parts(high: Felt, low: Felt) -> Result<Self, PrimitiveFromFeltError> {
91+
Ok(Self {
92+
high: high.try_into()?,
93+
low: low.try_into()?,
94+
})
95+
}
96+
97+
/// Attempts to construct a U256 from decimal string representations of high and low parts.
98+
///
99+
/// Both strings must be valid decimal representations that fit within u128 range.
100+
pub fn try_from_dec_str_parts(high: &str, low: &str) -> Result<Self, <u128 as FromStr>::Err> {
101+
Ok(Self {
102+
high: high.parse()?,
103+
low: low.parse()?,
104+
})
105+
}
106+
107+
/// Attempts to construct a U256 from hexadecimal string representations of high and low parts.
108+
///
109+
/// Both strings must be valid hexadecimal (prefixed or not) representations that fit within u128 range.
110+
pub fn try_from_hex_str_parts(high: &str, low: &str) -> Result<Self, <u128 as FromStr>::Err> {
111+
let high = if high.starts_with("0x") || high.starts_with("0X") {
112+
&high[2..]
113+
} else {
114+
high
115+
};
116+
let low = if low.starts_with("0x") || low.starts_with("0X") {
117+
&low[2..]
118+
} else {
119+
low
120+
};
121+
122+
Ok(Self {
123+
high: u128::from_str_radix(high, 16)?,
124+
low: u128::from_str_radix(low, 16)?,
125+
})
126+
}
127+
128+
/// Parses a hexadecimal string into a U256 value.
129+
///
130+
/// Accepts strings with or without "0x"/"0X" prefixes and handles leading zero removal.
131+
/// The implementation automatically determines the split between high and low components
132+
/// based on string length, with values over 32 hex digits requiring high component usage.
133+
pub fn from_hex_str(hex_str: &str) -> Result<Self, FromStrError> {
134+
// Remove prefix
135+
let string_without_prefix = if hex_str.starts_with("0x") || hex_str.starts_with("0X") {
136+
&hex_str[2..]
137+
} else {
138+
hex_str
139+
};
140+
141+
if string_without_prefix.is_empty() {
142+
return Err(FromStrError::Invalid);
143+
}
144+
145+
// Remove leading zero
146+
let string_without_zero_padding = string_without_prefix.trim_start_matches('0');
147+
148+
let (high, low) = if string_without_zero_padding.is_empty() {
149+
// The string was uniquely made out of of `0`
150+
(0, 0)
151+
} else if string_without_zero_padding.len() > 64 {
152+
return Err(FromStrError::StringTooLong);
153+
} else if string_without_zero_padding.len() > 32 {
154+
// The 32 last characters are the `low` u128 bytes,
155+
// all the other ones are the `high` u128 bytes.
156+
let delimiter_index = string_without_zero_padding.len() - 32;
157+
(
158+
u128::from_str_radix(&string_without_zero_padding[0..delimiter_index], 16)
159+
.map_err(FromStrError::Parse)?,
160+
u128::from_str_radix(&string_without_zero_padding[delimiter_index..], 16)
161+
.map_err(FromStrError::Parse)?,
162+
)
163+
} else {
164+
// There is no `high` bytes.
165+
(
166+
0,
167+
u128::from_str_radix(string_without_zero_padding, 16)
168+
.map_err(FromStrError::Parse)?,
169+
)
170+
};
171+
172+
Ok(U256 { high, low })
173+
}
174+
175+
/// Parses a decimal string into a `u256`.
176+
///
177+
/// Custom arithmetic is executed in order to efficiently parse the input as two `u128` values.
178+
///
179+
/// This implementation performs digit-by-digit multiplication to handle values
180+
/// that exceed u128 range. The algorithm uses overflow detection to prevent
181+
/// silent wraparound and ensures accurate representation of large decimal numbers.
182+
/// Values with more than 78 decimal digits are rejected as they exceed U256 capacity.
183+
pub fn from_dec_str(dec_str: &str) -> Result<Self, FromStrError> {
184+
if dec_str.is_empty() {
185+
return Err(FromStrError::Invalid);
186+
}
187+
188+
// Ignore leading zeros
189+
let string_without_zero_padding = dec_str.trim_start_matches('0');
190+
191+
let (high, low) = if string_without_zero_padding.is_empty() {
192+
// The string was uniquely made out of of `0`
193+
(0, 0)
194+
} else if string_without_zero_padding.len() > 78 {
195+
return Err(FromStrError::StringTooLong);
196+
} else {
197+
let mut low = 0u128;
198+
let mut high = 0u128;
199+
200+
// b is ascii value of the char less the ascii value of the char '0'
201+
// which happen to be equal to the number represented by the char.
202+
// b = ascii(char) - ascii('0')
203+
for b in string_without_zero_padding
204+
.bytes()
205+
.map(|b| b.wrapping_sub(b'0'))
206+
{
207+
// Using `wrapping_sub` all non 0-9 characters will yield a value greater than 9.
208+
if b > 9 {
209+
return Err(FromStrError::Invalid);
210+
}
211+
212+
// We use a [long multiplication](https://en.wikipedia.org/wiki/Multiplication_algorithm#Long_multiplication)
213+
// algorithm to perform the computation.
214+
// The idea is that if
215+
// `v = (high << 128) + low`
216+
// then
217+
// `v * 10 = ((high * 10) << 128) + low * 10`
218+
219+
// Compute `high * 10`, return error on overflow.
220+
let (new_high, did_overflow) = high.overflowing_mul(10);
221+
if did_overflow {
222+
return Err(FromStrError::ValueTooBig);
223+
}
224+
// Now we want to compute `low * 10`, but in case it overflows, we want to carry rather than error.
225+
// To do so, we perform another long multiplication to get both the result and carry values,
226+
// this time breaking the u128 (low) value into two u64 (low_low and low_high),
227+
// perform multiplication on each part individually, extracting an eventual carry, and finally
228+
// combining them back.
229+
//
230+
// Any overflow on the high part will result in an error.
231+
// Any overflow on the low part should be handled by carrying the extra amount to the high part.
232+
let (new_low, carry) = {
233+
let low_low = low as u64;
234+
let low_high = (low >> 64) as u64;
235+
236+
// Both of those values cannot overflow, as they are u64 stored into a u128.
237+
// Intead they will just start using the highest half part of their bytes.
238+
let low_low = (low_low as u128) * 10;
239+
let low_high = (low_high as u128) * 10;
240+
241+
// The carry of the multiplication per 10 is in the highest 64 bytes of the `low_high` part.
242+
let carry_mul_10 = low_high >> 64;
243+
// We shift back the bytes, erasing any carry we may have.
244+
let low_high_without_carry = low_high << 64;
245+
246+
// By adding back the two low parts together we get its new value.
247+
let (new_low, did_overflow) = low_low.overflowing_add(low_high_without_carry);
248+
// I couldn't come up with a value where `did_overflow` is true,
249+
// but better safe than sorry
250+
(new_low, carry_mul_10 + if did_overflow { 1 } else { 0 })
251+
};
252+
253+
// Add carry to high if it exists.
254+
let new_high = if carry != 0 {
255+
let (new_high, did_overflow) = new_high.overflowing_add(carry);
256+
// Error if it overflows.
257+
if did_overflow {
258+
return Err(FromStrError::ValueTooBig);
259+
}
260+
new_high
261+
} else {
262+
new_high
263+
};
264+
265+
// Add the new digit to low.
266+
let (new_low, did_overflow) = new_low.overflowing_add(b.into());
267+
268+
// Add one to high if the previous operation overflowed.
269+
if did_overflow {
270+
let (new_high, did_overflow) = new_high.overflowing_add(1);
271+
// Error if it overflows.
272+
if did_overflow {
273+
return Err(FromStrError::ValueTooBig);
274+
}
275+
high = new_high;
276+
} else {
277+
high = new_high;
278+
}
279+
280+
low = new_low
281+
}
282+
283+
(high, low)
284+
};
285+
286+
Ok(U256 { high, low })
287+
}
288+
}
289+
290+
impl FromStr for U256 {
291+
type Err = FromStrError;
292+
293+
/// Parses a string into a U256 by detecting the format automatically.
294+
///
295+
/// Strings beginning with "0x" or "0X" are treated as hexadecimal,
296+
/// while all other strings are interpreted as decimal.
297+
fn from_str(s: &str) -> Result<Self, Self::Err> {
298+
if s.starts_with("0x") || s.starts_with("0X") {
299+
Self::from_hex_str(s)
300+
} else {
301+
Self::from_dec_str(s)
302+
}
303+
}
304+
}

0 commit comments

Comments
 (0)