diff --git a/Cargo.lock b/Cargo.lock index 6ea048eea..394572eb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,6 +649,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "clvm-traits-fuzz" +version = "0.0.0" +dependencies = [ + "clvm-traits", + "clvmr", + "libfuzzer-sys", +] + [[package]] name = "clvm-utils" version = "0.10.0" diff --git a/crates/clvm-traits/fuzz/.gitignore b/crates/clvm-traits/fuzz/.gitignore new file mode 100644 index 000000000..1a45eee77 --- /dev/null +++ b/crates/clvm-traits/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/crates/clvm-traits/fuzz/Cargo.toml b/crates/clvm-traits/fuzz/Cargo.toml new file mode 100644 index 000000000..a94af67c1 --- /dev/null +++ b/crates/clvm-traits/fuzz/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "clvm-traits-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = { workspace = true } +clvmr = { workspace = true } +clvm-traits = { workspace = true } + +[[bin]] +name = "int_encoding" +path = "fuzz_targets/int_encoding.rs" +test = false +doc = false +bench = false diff --git a/crates/clvm-traits/fuzz/fuzz_targets/int_encoding.rs b/crates/clvm-traits/fuzz/fuzz_targets/int_encoding.rs new file mode 100644 index 000000000..5e8e981ef --- /dev/null +++ b/crates/clvm-traits/fuzz/fuzz_targets/int_encoding.rs @@ -0,0 +1,40 @@ +#![no_main] + +use clvm_traits::{decode_number, encode_number}; +use clvmr::Allocator; +use libfuzzer_sys::{arbitrary::Unstructured, fuzz_target}; + +fuzz_target!(|data: &[u8]| { + let mut unstructured = Unstructured::new(data); + + macro_rules! impl_num { + ( $num_type:ty, $signed:expr ) => { + let num: $num_type = unstructured.arbitrary().unwrap(); + let mut allocator = Allocator::new(); + let ptr = allocator.new_number(num.into()).unwrap(); + let atom = allocator.atom(ptr); + let expected = atom.as_ref(); + + #[allow(unused_comparisons)] + let encoded = encode_number(&num.to_be_bytes(), num < 0); + assert_eq!(expected, encoded); + + let expected = num.to_be_bytes(); + let decoded = decode_number(&encoded, $signed).unwrap(); + assert_eq!(expected, decoded); + }; + } + + impl_num!(u8, false); + impl_num!(i8, true); + impl_num!(u16, false); + impl_num!(i16, true); + impl_num!(u32, false); + impl_num!(i32, true); + impl_num!(u64, false); + impl_num!(i64, true); + impl_num!(u128, false); + impl_num!(i128, true); + impl_num!(usize, false); + impl_num!(isize, true); +}); diff --git a/crates/clvm-traits/src/clvm_decoder.rs b/crates/clvm-traits/src/clvm_decoder.rs index e66ce176a..490fe1a5a 100644 --- a/crates/clvm-traits/src/clvm_decoder.rs +++ b/crates/clvm-traits/src/clvm_decoder.rs @@ -1,4 +1,5 @@ use clvmr::{allocator::SExp, Allocator, Atom, NodePtr}; +use num_bigint::BigInt; use crate::{ destructure_list, destructure_quote, match_list, match_quote, FromClvm, FromClvmError, @@ -11,6 +12,11 @@ pub trait ClvmDecoder: Sized { fn decode_atom(&self, node: &Self::Node) -> Result, FromClvmError>; fn decode_pair(&self, node: &Self::Node) -> Result<(Self::Node, Self::Node), FromClvmError>; + fn decode_bigint(&self, node: &Self::Node) -> Result { + let atom = self.decode_atom(node)?; + Ok(BigInt::from_signed_bytes_be(atom.as_ref())) + } + fn decode_curried_arg( &self, node: &Self::Node, @@ -49,6 +55,14 @@ impl ClvmDecoder for Allocator { Err(FromClvmError::ExpectedPair) } } + + fn decode_bigint(&self, node: &Self::Node) -> Result { + if let SExp::Atom = self.sexp(*node) { + Ok(self.number(*node)) + } else { + Err(FromClvmError::ExpectedAtom) + } + } } impl FromClvm for NodePtr { diff --git a/crates/clvm-traits/src/from_clvm.rs b/crates/clvm-traits/src/from_clvm.rs index d5a8d2a3e..897ffe8e9 100644 --- a/crates/clvm-traits/src/from_clvm.rs +++ b/crates/clvm-traits/src/from_clvm.rs @@ -1,8 +1,8 @@ use std::{rc::Rc, sync::Arc}; -use num_bigint::{BigInt, Sign}; +use num_bigint::BigInt; -use crate::{ClvmDecoder, FromClvmError}; +use crate::{decode_number, ClvmDecoder, FromClvmError}; pub trait FromClvm: Sized where @@ -12,50 +12,45 @@ where } macro_rules! clvm_primitive { - ($primitive:ty) => { + ($primitive:ty, $signed:expr) => { impl> FromClvm for $primitive { fn from_clvm(decoder: &D, node: N) -> Result { const LEN: usize = std::mem::size_of::<$primitive>(); - let bytes = decoder.decode_atom(&node)?; - let number = BigInt::from_signed_bytes_be(bytes.as_ref()); - let (sign, mut vec) = number.to_bytes_be(); - - if vec.len() < std::mem::size_of::<$primitive>() { - let mut zeros = vec![0; LEN - vec.len()]; - zeros.extend(vec); - vec = zeros; - } + let atom = decoder.decode_atom(&node)?; + let slice = atom.as_ref(); - let value = <$primitive>::from_be_bytes(vec.as_slice().try_into().or(Err( - FromClvmError::WrongAtomLength { + let Some(bytes) = decode_number(slice, $signed) else { + return Err(FromClvmError::WrongAtomLength { expected: LEN, - found: bytes.as_ref().len(), - }, - ))?); - - Ok(if sign == Sign::Minus { - value.wrapping_neg() - } else { - value - }) + found: slice.len(), + }); + }; + + Ok(<$primitive>::from_be_bytes(bytes)) } } }; } -clvm_primitive!(u8); -clvm_primitive!(i8); -clvm_primitive!(u16); -clvm_primitive!(i16); -clvm_primitive!(u32); -clvm_primitive!(i32); -clvm_primitive!(u64); -clvm_primitive!(i64); -clvm_primitive!(u128); -clvm_primitive!(i128); -clvm_primitive!(usize); -clvm_primitive!(isize); +clvm_primitive!(u8, false); +clvm_primitive!(i8, true); +clvm_primitive!(u16, false); +clvm_primitive!(i16, true); +clvm_primitive!(u32, false); +clvm_primitive!(i32, true); +clvm_primitive!(u64, false); +clvm_primitive!(i64, true); +clvm_primitive!(u128, false); +clvm_primitive!(i128, true); +clvm_primitive!(usize, false); +clvm_primitive!(isize, true); + +impl> FromClvm for BigInt { + fn from_clvm(decoder: &D, node: N) -> Result { + decoder.decode_bigint(&node) + } +} impl> FromClvm for bool { fn from_clvm(decoder: &D, node: N) -> Result { diff --git a/crates/clvm-traits/src/int_encoding.rs b/crates/clvm-traits/src/int_encoding.rs new file mode 100644 index 000000000..03fb3d8aa --- /dev/null +++ b/crates/clvm-traits/src/int_encoding.rs @@ -0,0 +1,98 @@ +pub fn encode_number(slice: &[u8], negative: bool) -> Vec { + let mut start = 0; + let pad_byte = if negative { 0xFF } else { 0x00 }; + + // Skip leading pad bytes + while start < slice.len() && slice[start] == pad_byte { + start += 1; + } + + let needs_padding = if negative { + start == slice.len() || (slice[start] & 0x80) == 0 + } else { + start < slice.len() && (slice[start] & 0x80) != 0 + }; + + let mut result = Vec::with_capacity(if needs_padding { + slice.len() - start + 1 + } else { + slice.len() - start + }); + + if needs_padding { + result.push(pad_byte); + } + + result.extend_from_slice(&slice[start..]); + result +} + +pub fn decode_number(mut slice: &[u8], signed: bool) -> Option<[u8; LEN]> { + let negative = signed && !slice.is_empty() && slice[0] & 0x80 != 0; + let padding_byte = if negative { 0xFF } else { 0x00 }; + + if slice.len() > LEN && slice[0] == padding_byte { + slice = &slice[slice.len() - LEN..]; + } + + if slice.len() > LEN { + return None; + } + + assert!(slice.len() <= LEN); + + let mut result = [padding_byte; LEN]; + let start = LEN - slice.len(); + + result[start..].copy_from_slice(slice); + + Some(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + use clvmr::Allocator; + + macro_rules! test_roundtrip { + ( $num:expr, $signed:expr ) => { + let mut allocator = Allocator::new(); + let ptr = allocator.new_number($num.into()).unwrap(); + let atom = allocator.atom(ptr); + let expected = atom.as_ref(); + + #[allow(unused_comparisons)] + let encoded = encode_number(&$num.to_be_bytes(), $num < 0); + assert_eq!(expected, encoded); + + let expected = $num.to_be_bytes(); + let decoded = decode_number(&encoded, $signed).unwrap(); + assert_eq!(expected, decoded); + }; + } + + #[test] + fn test_signed_encoding() { + test_roundtrip!(0i32, true); + test_roundtrip!(1i32, true); + test_roundtrip!(2i32, true); + test_roundtrip!(3i32, true); + test_roundtrip!(255i32, true); + test_roundtrip!(4716i32, true); + test_roundtrip!(-255i32, true); + test_roundtrip!(-10i32, true); + test_roundtrip!(i32::MIN, true); + test_roundtrip!(i32::MAX, true); + } + + #[test] + fn test_unsigned_encoding() { + test_roundtrip!(0u32, false); + test_roundtrip!(1u32, false); + test_roundtrip!(2u32, false); + test_roundtrip!(3u32, false); + test_roundtrip!(255u32, false); + test_roundtrip!(u32::MAX, false); + } +} diff --git a/crates/clvm-traits/src/lib.rs b/crates/clvm-traits/src/lib.rs index 408cd0267..823a3762b 100644 --- a/crates/clvm-traits/src/lib.rs +++ b/crates/clvm-traits/src/lib.rs @@ -13,6 +13,7 @@ mod clvm_decoder; mod clvm_encoder; mod error; mod from_clvm; +mod int_encoding; mod macros; mod match_byte; mod to_clvm; @@ -22,6 +23,7 @@ pub use clvm_decoder::*; pub use clvm_encoder::*; pub use error::*; pub use from_clvm::*; +pub use int_encoding::*; pub use match_byte::*; pub use to_clvm::*; pub use wrappers::*; diff --git a/crates/clvm-traits/src/to_clvm.rs b/crates/clvm-traits/src/to_clvm.rs index 963d20f2c..c2672b0f0 100644 --- a/crates/clvm-traits/src/to_clvm.rs +++ b/crates/clvm-traits/src/to_clvm.rs @@ -3,7 +3,7 @@ use std::{rc::Rc, sync::Arc}; use clvmr::Atom; use num_bigint::BigInt; -use crate::{ClvmEncoder, ToClvmError}; +use crate::{encode_number, ClvmEncoder, ToClvmError}; pub trait ToClvm where @@ -16,7 +16,9 @@ macro_rules! clvm_primitive { ($primitive:ty) => { impl> ToClvm for $primitive { fn to_clvm(&self, encoder: &mut E) -> Result { - encoder.encode_bigint(BigInt::from(*self)) + let bytes = self.to_be_bytes(); + #[allow(unused_comparisons)] + encoder.encode_atom(Atom::Borrowed(&encode_number(&bytes, *self < 0))) } } }; @@ -35,6 +37,12 @@ clvm_primitive!(i128); clvm_primitive!(usize); clvm_primitive!(isize); +impl> ToClvm for BigInt { + fn to_clvm(&self, encoder: &mut E) -> Result<::Node, ToClvmError> { + encoder.encode_bigint(self.clone()) + } +} + impl> ToClvm for bool { fn to_clvm(&self, encoder: &mut E) -> Result { i32::from(*self).to_clvm(encoder)