Skip to content

Commit c8a88c2

Browse files
Add fuzz tests comparing Solidity DecimalFloat ops against f64
Proptest fuzz tests for add, sub, mul, div, neg, abs, inv, and comparisons. Each test generates random Float values in f64-compatible range, computes the operation via both Solidity (REVM) and Rust f64, and asserts results match within relative tolerance of 1e-10. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 615b2c3 commit c8a88c2

2 files changed

Lines changed: 199 additions & 0 deletions

File tree

crates/float/src/fuzz_ops.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use crate::Float;
4+
use alloy::primitives::{aliases::I224, I256};
5+
use proptest::prelude::*;
6+
7+
/// Convert a Solidity Float to f64 via unpack.
8+
fn sol_to_f64(f: Float) -> Option<f64> {
9+
let (coeff, exp) = f.unpack().ok()?;
10+
let c: f64 = i256_to_f64(coeff);
11+
let e: i32 = exp.as_i32();
12+
Some(c * 10.0_f64.powi(e))
13+
}
14+
15+
fn i256_to_f64(v: I256) -> f64 {
16+
if v.is_negative() {
17+
let abs = (!v).wrapping_add(I256::ONE);
18+
-(u256_to_f64(abs.into_raw()))
19+
} else {
20+
u256_to_f64(v.into_raw())
21+
}
22+
}
23+
24+
fn u256_to_f64(v: alloy::primitives::U256) -> f64 {
25+
// Convert U256 to f64 via string parsing for accuracy.
26+
v.to_string().parse::<f64>().unwrap_or(f64::INFINITY)
27+
}
28+
29+
/// Generate floats in a range where f64 can represent them without
30+
/// overflow/underflow. Coefficients up to ~1e15 and exponents -15..15
31+
/// keep values in f64's comfortable range.
32+
prop_compose! {
33+
fn f64_compatible_float()(
34+
coefficient in -10i64.pow(15)..10i64.pow(15),
35+
exponent in -15i32..15i32,
36+
) -> Float {
37+
Float::pack_lossless(
38+
I224::try_from(coefficient).unwrap(),
39+
exponent,
40+
).unwrap()
41+
}
42+
}
43+
44+
/// Check that two f64 values are approximately equal, allowing for
45+
/// f64 rounding errors. Returns true if they're within a relative
46+
/// tolerance of 1e-10 or both are effectively zero.
47+
fn approx_eq(a: f64, b: f64) -> bool {
48+
if a == b {
49+
return true;
50+
}
51+
if a.is_nan() || b.is_nan() {
52+
return false;
53+
}
54+
let max_abs = a.abs().max(b.abs());
55+
if max_abs < 1e-30 {
56+
return true;
57+
}
58+
((a - b).abs() / max_abs) < 1e-10
59+
}
60+
61+
proptest! {
62+
#[test]
63+
fn fuzz_add(
64+
a in f64_compatible_float(),
65+
b in f64_compatible_float(),
66+
) {
67+
let sol_result = (a + b).unwrap();
68+
let a_f64 = sol_to_f64(a).unwrap();
69+
let b_f64 = sol_to_f64(b).unwrap();
70+
let expected = a_f64 + b_f64;
71+
let actual = sol_to_f64(sol_result).unwrap();
72+
prop_assert!(
73+
approx_eq(expected, actual),
74+
"add: {a_f64} + {b_f64} = {expected}, sol = {actual}",
75+
);
76+
}
77+
78+
#[test]
79+
fn fuzz_sub(
80+
a in f64_compatible_float(),
81+
b in f64_compatible_float(),
82+
) {
83+
let sol_result = (a - b).unwrap();
84+
let a_f64 = sol_to_f64(a).unwrap();
85+
let b_f64 = sol_to_f64(b).unwrap();
86+
let expected = a_f64 - b_f64;
87+
let actual = sol_to_f64(sol_result).unwrap();
88+
prop_assert!(
89+
approx_eq(expected, actual),
90+
"sub: {a_f64} - {b_f64} = {expected}, sol = {actual}",
91+
);
92+
}
93+
94+
#[test]
95+
fn fuzz_mul(
96+
a in f64_compatible_float(),
97+
b in f64_compatible_float(),
98+
) {
99+
let sol_result = (a * b).unwrap();
100+
let a_f64 = sol_to_f64(a).unwrap();
101+
let b_f64 = sol_to_f64(b).unwrap();
102+
let expected = a_f64 * b_f64;
103+
let actual = sol_to_f64(sol_result).unwrap();
104+
prop_assert!(
105+
approx_eq(expected, actual),
106+
"mul: {a_f64} * {b_f64} = {expected}, sol = {actual}",
107+
);
108+
}
109+
110+
#[test]
111+
fn fuzz_div(
112+
a in f64_compatible_float(),
113+
b in f64_compatible_float(),
114+
) {
115+
let b_f64 = sol_to_f64(b).unwrap();
116+
// Skip division by zero.
117+
prop_assume!(b_f64.abs() > 1e-30);
118+
119+
let sol_result = (a / b).unwrap();
120+
let a_f64 = sol_to_f64(a).unwrap();
121+
let expected = a_f64 / b_f64;
122+
let actual = sol_to_f64(sol_result).unwrap();
123+
prop_assert!(
124+
approx_eq(expected, actual),
125+
"div: {a_f64} / {b_f64} = {expected}, sol = {actual}",
126+
);
127+
}
128+
129+
#[test]
130+
fn fuzz_neg(a in f64_compatible_float()) {
131+
let sol_result = (-a).unwrap();
132+
let a_f64 = sol_to_f64(a).unwrap();
133+
let expected = -a_f64;
134+
let actual = sol_to_f64(sol_result).unwrap();
135+
prop_assert!(
136+
approx_eq(expected, actual),
137+
"neg: -{a_f64} = {expected}, sol = {actual}",
138+
);
139+
}
140+
141+
#[test]
142+
fn fuzz_abs(a in f64_compatible_float()) {
143+
let sol_result = a.abs().unwrap();
144+
let a_f64 = sol_to_f64(a).unwrap();
145+
let expected = a_f64.abs();
146+
let actual = sol_to_f64(sol_result).unwrap();
147+
prop_assert!(
148+
approx_eq(expected, actual),
149+
"abs: |{a_f64}| = {expected}, sol = {actual}",
150+
);
151+
}
152+
153+
#[test]
154+
fn fuzz_inv(a in f64_compatible_float()) {
155+
let a_f64 = sol_to_f64(a).unwrap();
156+
// Skip values too close to zero.
157+
prop_assume!(a_f64.abs() > 1e-10);
158+
159+
let sol_result = a.inv().unwrap();
160+
let expected = 1.0 / a_f64;
161+
let actual = sol_to_f64(sol_result).unwrap();
162+
prop_assert!(
163+
approx_eq(expected, actual),
164+
"inv: 1/{a_f64} = {expected}, sol = {actual}",
165+
);
166+
}
167+
168+
#[test]
169+
fn fuzz_comparisons(
170+
a in f64_compatible_float(),
171+
b in f64_compatible_float(),
172+
) {
173+
let a_f64 = sol_to_f64(a).unwrap();
174+
let b_f64 = sol_to_f64(b).unwrap();
175+
176+
// Only test comparisons when values are far enough apart
177+
// that f64 precision issues don't cause false failures.
178+
let diff = (a_f64 - b_f64).abs();
179+
let max_abs = a_f64.abs().max(b_f64.abs());
180+
prop_assume!(diff > max_abs * 1e-10 || diff < 1e-30);
181+
182+
let sol_lt = a.lt(b).unwrap();
183+
let sol_gt = a.gt(b).unwrap();
184+
let sol_eq = a.eq(b).unwrap();
185+
186+
if diff < 1e-30 {
187+
// Both effectively zero.
188+
prop_assert!(sol_eq, "eq: {a_f64} == {b_f64} should be true");
189+
} else if a_f64 < b_f64 {
190+
prop_assert!(sol_lt, "lt: {a_f64} < {b_f64} should be true");
191+
prop_assert!(!sol_gt, "gt: {a_f64} > {b_f64} should be false");
192+
} else {
193+
prop_assert!(sol_gt, "gt: {a_f64} > {b_f64} should be true");
194+
prop_assert!(!sol_lt, "lt: {a_f64} < {b_f64} should be false");
195+
}
196+
}
197+
}
198+
}

crates/float/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use alloy::primitives::aliases::I224;
1111

1212
pub mod error;
1313
mod evm;
14+
mod fuzz_ops;
1415
pub mod js_api;
1516
pub mod tables;
1617

0 commit comments

Comments
 (0)