diff --git a/graph/src/data/store/scalar/bigint.rs b/graph/src/data/store/scalar/bigint.rs index 3b31a3a623d..9ede6e08a4d 100644 --- a/graph/src/data/store/scalar/bigint.rs +++ b/graph/src/data/store/scalar/bigint.rs @@ -1,3 +1,4 @@ +use alloy::primitives::I256; use anyhow::bail; use num_bigint; use serde::{self, Deserialize, Serialize}; @@ -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 { 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 { @@ -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)) } @@ -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 { diff --git a/graph/src/data_source/common.rs b/graph/src/data_source/common.rs index c02a7577dd3..f23c8d3d849 100644 --- a/graph/src/data_source/common.rs +++ b/graph/src/data_source/common.rs @@ -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 => { diff --git a/runtime/test/src/test/abi.rs b/runtime/test/src/test/abi.rs index b93ed2d9cfa..bd1b2de04d9 100644 --- a/runtime/test/src/test/abi.rs +++ b/runtime/test/src/test/abi.rs @@ -525,8 +525,8 @@ async fn test_abi_big_int(api_version: Version) { let new_uint_obj: AscPtr = 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] diff --git a/runtime/wasm/src/to_from/external.rs b/runtime/wasm/src/to_from/external.rs index 39e23cf00d9..e917ffa2fa9 100644 --- a/runtime/wasm/src/to_from/external.rs +++ b/runtime/wasm/src/to_from/external.rs @@ -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::*; @@ -235,8 +232,7 @@ impl FromAscObj> for abi::DynSolValue { EthereumValueKind::Int => { let ptr: AscPtr = 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) }