Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions graph/src/data/store/scalar/bigint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloy::primitives::I256;
use anyhow::bail;
use num_bigint;
use serde::{self, Deserialize, Serialize};
Expand Down Expand Up @@ -185,24 +186,27 @@ impl BigInt {
BigInt::from_unsigned_bytes_le(&bytes).unwrap()
}

pub fn from_signed_u256(n: &U256) -> Self {
let bytes: [u8; U256::BYTES] = n.to_le_bytes();
pub fn from_i256(n: &I256) -> Self {
let bytes: [u8; I256::BYTES] = n.to_le_bytes();
// Unwrap: 256 bits is much less than BigInt::MAX_BITS
BigInt::from_signed_bytes_le(&bytes).unwrap()
}

pub fn to_signed_u256(&self) -> U256 {
pub fn to_i256(&self) -> Result<I256, anyhow::Error> {
let bytes = self.to_signed_bytes_le();
if self < &BigInt::from(0) {
assert!(
bytes.len() <= 32,
"BigInt value does not fit into signed U256"
);
let mut i_bytes: [u8; 32] = [255; 32];
i_bytes[..bytes.len()].copy_from_slice(&bytes);
U256::from_le_slice(&i_bytes)
anyhow::ensure!(
bytes.len() <= I256::BYTES,
"BigInt value `{}` does not fit into int256",
self
);
let fill: u8 = if self.sign() == BigIntSign::Minus {
0xFF
} else {
U256::from_le_slice(&bytes)
}
0x00
};
let mut buf = [fill; I256::BYTES];
buf[..bytes.len()].copy_from_slice(&bytes);
Ok(I256::from_le_bytes(buf))
}

pub fn to_unsigned_u256(&self) -> Result<U256, anyhow::Error> {
Expand All @@ -213,6 +217,11 @@ impl BigInt {
self
);
}
anyhow::ensure!(
bytes.len() <= U256::BYTES,
"BigInt value `{}` does not fit into uint256",
self
);
Ok(U256::from_le_slice(&bytes))
}

Expand Down Expand Up @@ -410,6 +419,71 @@ mod test {

use super::{super::test::same_stable_hash, BigInt};

/// Compute 2^n via repeated doubling so we can build values larger than
/// `BigInt::pow`'s u8 exponent limit.
fn pow2(n: u32) -> BigInt {
let mut acc = BigInt::from(1u64);
for _ in 0..n {
acc = acc * BigInt::from(2u64);
}
acc
}

#[test]
fn to_i256_succeeds_at_boundaries() {
let one = BigInt::from(1u64);
let i256_max = pow2(255) - one.clone();
let i256_min = BigInt::from(0) - pow2(255);

for v in &[
BigInt::from(0),
BigInt::from(1),
BigInt::from(-1),
i256_max.clone(),
i256_min.clone(),
] {
let i = v.to_i256().expect("in-range value should convert");
let back = BigInt::from_i256(&i);
assert_eq!(&back, v, "round-trip failed for {}", v);
}
}

#[test]
fn to_i256_errors_outside_range() {
let one = BigInt::from(1u64);
let just_above_max = pow2(255);
let just_below_min = BigInt::from(0) - pow2(255) - one;
let way_above = pow2(300);
let way_below = BigInt::from(0) - pow2(300);

for v in &[just_above_max, just_below_min, way_above, way_below] {
assert!(
v.to_i256().is_err(),
"out-of-range value {} should error, not panic",
v
);
}
}

#[test]
fn to_unsigned_u256_errors_outside_range() {
let just_above_max = pow2(256);
let way_above = pow2(300);

for v in &[just_above_max, way_above] {
assert!(
v.to_unsigned_u256().is_err(),
"value {} above u256::MAX should error, not panic",
v
);
}

assert!(
BigInt::from(-1).to_unsigned_u256().is_err(),
"negative value should error"
);
}

#[test]
fn bigint_to_from_u64() {
for n in 0..100 {
Expand Down
3 changes: 1 addition & 2 deletions graph/src/data_source/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,7 @@ impl CallDecl {
Ok(DynSolValue::Int(x, x.bits() as usize))
}
(DynSolType::Int(_), Value::BigInt(i)) => {
let x =
abi::I256::from_le_bytes(i.to_signed_u256().to_le_bytes::<{ U256::BYTES }>());
let x = i.to_i256()?;
Ok(DynSolValue::Int(x, x.bits() as usize))
}
(DynSolType::Uint(_), Value::Int(i)) if *i >= 0 => {
Expand Down
4 changes: 2 additions & 2 deletions runtime/test/src/test/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,8 +525,8 @@ async fn test_abi_big_int(api_version: Version) {
let new_uint_obj: AscPtr<AscBigInt> = module.invoke_export1("test_uint", &old_uint).await;
let new_uint: BigInt = module.asc_get(new_uint_obj).unwrap();
assert_eq!(new_uint, BigInt::from(-49_i32));
let new_uint_from_u256 = BigInt::from_signed_u256(&new_uint.to_signed_u256());
assert_eq!(new_uint, new_uint_from_u256);
let new_uint_from_i256 = BigInt::from_i256(&new_uint.to_i256().unwrap());
assert_eq!(new_uint, new_uint_from_i256);
}

#[graph::test]
Expand Down
8 changes: 2 additions & 6 deletions runtime/wasm/src/to_from/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ use graph::runtime::{
AscIndexId, AscPtr, AscType, AscValue, HostExportError, ToAscObj, asc_get, asc_new,
};
use graph::{data::store, runtime::DeterministicHostError};
use graph::{
prelude::{alloy::primitives::U256, serde_json},
runtime::FromAscObj,
};
use graph::{prelude::serde_json, runtime::FromAscObj};

use crate::asc_abi::class::*;

Expand Down Expand Up @@ -235,8 +232,7 @@ impl FromAscObj<AscEnum<EthereumValueKind>> for abi::DynSolValue {
EthereumValueKind::Int => {
let ptr: AscPtr<AscBigInt> = AscPtr::from(payload);
let n: BigInt = asc_get(heap, ptr, gas, depth)?;
let x =
abi::I256::from_le_bytes(n.to_signed_u256().to_le_bytes::<{ U256::BYTES }>());
let x = n.to_i256().map_err(DeterministicHostError::Other)?;

Self::Int(x, x.bits() as usize)
}
Expand Down
Loading