diff --git a/Cargo.lock b/Cargo.lock index eece587fc..3d3163c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1428,6 +1428,7 @@ version = "3.5.0" dependencies = [ "assert_matches", "borsh", + "bounded-vec", "derive_more 2.1.1", "hex", "rstest", @@ -1437,6 +1438,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "bounded-vec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dc0086e469182132244e9b8d313a0742e1132da43a08c24b9dd3c18e0faf3a" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "bs58" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 94270bd7e..5caa7c24b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ backon = { version = "1.6.0", features = ["tokio-sleep"] } base64 = "0.22.1" blstrs = "0.7.1" borsh = { version = "1.6.0", features = ["derive"] } +bounded-vec = "0.9" bs58 = { version = "0.5.1" } built = { version = "0.8", features = ["git2", "chrono"] } bytes = { version = "1.11.1" } diff --git a/crates/bounded-collections/Cargo.toml b/crates/bounded-collections/Cargo.toml index 7278f05c6..1854553cb 100644 --- a/crates/bounded-collections/Cargo.toml +++ b/crates/bounded-collections/Cargo.toml @@ -9,6 +9,7 @@ abi = ["borsh/unstable__schema", "schemars"] [dependencies] borsh = { workspace = true } +bounded-vec = { workspace = true } derive_more = { workspace = true } hex = { workspace = true } serde = { workspace = true } diff --git a/crates/bounded-collections/src/bounded_vec.rs b/crates/bounded-collections/src/bounded_vec.rs index f269ef0a5..4c150a6ca 100644 --- a/crates/bounded-collections/src/bounded_vec.rs +++ b/crates/bounded-collections/src/bounded_vec.rs @@ -1,25 +1,8 @@ -use std::{ - convert::{TryFrom, TryInto}, - slice::{Iter, IterMut}, - vec, -}; +use std::{convert::TryFrom, vec}; +use bounded_vec::BoundedVec as Upstream; use thiserror::Error; -/// Vec bounded with minimal (L - lower bound) and maximal (U - upper bound) items quantity. -/// -/// By default the witness type is [`witnesses::NonEmpty`], which requires `L > 0`. -/// For a possibly-empty bounded vector (where `L = 0`), use [`EmptyBoundedVec`] instead. -/// -/// # Type Parameters -/// -/// * `W` - witness type to prove vector ranges and shape of interface accordingly -#[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)] -pub struct BoundedVec> { - inner: Vec, - witness: W, -} - /// BoundedVec errors #[derive(Error, PartialEq, Eq, Debug, Clone)] pub enum BoundedVecOutOfBounds { @@ -41,433 +24,153 @@ pub enum BoundedVecOutOfBounds { }, } -/// Module for type witnesses used to prove vector bounds at compile time -pub mod witnesses { - /// Compile-time proof of valid bounds. Must be constructed with same bounds to instantiate `BoundedVec`. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] - pub struct NonEmpty( - (), // private field to prevent direct construction. - ); - - /// Possibly empty vector with upper bound. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] - pub struct PossiblyEmpty( - (), // private field to prevent direct construction. - ); - - /// Type a compile-time proof of valid bounds - pub const fn non_empty() -> NonEmpty { - const { - if L == 0 { - panic!("L must be greater than 0") +impl From for BoundedVecOutOfBounds { + fn from(e: bounded_vec::BoundedVecOutOfBounds) -> Self { + match e { + bounded_vec::BoundedVecOutOfBounds::LowerBoundError { lower_bound, got } => { + Self::LowerBoundError { lower_bound, got } } - if L > U { - panic!("L must be less than or equal to U") + bounded_vec::BoundedVecOutOfBounds::UpperBoundError { upper_bound, got } => { + Self::UpperBoundError { upper_bound, got } } - - NonEmpty::(()) } } +} - /// Type a compile-time proof for possibly empty vector with upper bound - pub const fn possibly_empty() -> PossiblyEmpty { - const { PossiblyEmpty::(()) } - } +pub mod witnesses { + pub use bounded_vec::witnesses::Empty as PossiblyEmpty; + pub use bounded_vec::witnesses::NonEmpty; } -impl BoundedVec> { - /// Creates new [`BoundedVec`] or returns error if items count is out of bounds - /// - /// # Parameters - /// - /// * `items` - vector of items within bounds - /// - /// # Errors - /// - /// * `UpperBoundError` - if `items` len is more than U (upper bound) - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::witnesses; - /// let data: BoundedVec<_, 0, 8, witnesses::PossiblyEmpty<8>> = - /// BoundedVec::<_, 0, 8, witnesses::PossiblyEmpty<8>>::from_vec(vec![1u8, 2]).unwrap(); - /// ``` - pub fn from_vec(items: Vec) -> Result { - let witness = witnesses::possibly_empty::(); - let len = items.len(); - if len > U { - Err(BoundedVecOutOfBounds::UpperBoundError { - upper_bound: U, - got: len, - }) - } else { - Ok(BoundedVec { - inner: items, - witness, - }) - } - } +/// Vec bounded with minimal (L - lower bound) and maximal (U - upper bound) items quantity. +/// +/// Wraps [`bounded_vec::BoundedVec`] and adds Borsh serialization support. +/// +/// By default the witness type is [`witnesses::NonEmpty`], which requires `L > 0`. +/// For a possibly-empty bounded vector (where `L = 0`), use [`EmptyBoundedVec`] instead. +/// +/// Read-only access to the inner vec is available via `Deref` to the upstream type. +#[derive(Debug, Clone, derive_more::Deref)] +pub struct BoundedVec>( + Upstream, +); - /// Returns the first element of the vector, or [`None`] if it is empty - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::witnesses; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.first(), Some(&1u8)); - /// ``` - pub fn first(&self) -> Option<&T> { - self.inner.first() - } +// Manual trait impls that compare only the inner Vec, avoiding trait bounds on +// the witness type W (the upstream witnesses lack Ord/PartialOrd). - /// Returns `true` if the vector contains no elements - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::witnesses; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.is_empty(), false); - /// ``` - pub fn is_empty(&self) -> bool { - self.inner.is_empty() +impl PartialEq for BoundedVec { + fn eq(&self, other: &Self) -> bool { + self.as_vec() == other.as_vec() } +} - /// Returns the last element of the vector, or [`None`] if it is empty - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::witnesses; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.last(), Some(&2u8)); - /// ``` - pub fn last(&self) -> Option<&T> { - self.inner.last() +impl Eq for BoundedVec {} + +impl std::hash::Hash + for BoundedVec +{ + fn hash(&self, state: &mut H) { + self.as_vec().hash(state); } } -/// Methods which works for all witnesses -impl BoundedVec { - /// Returns a reference to underlying [`Vec`] - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.as_vec(), &vec![1u8,2]); - /// ``` - pub fn as_vec(&self) -> &Vec { - &self.inner +impl PartialOrd for BoundedVec { + fn partial_cmp(&self, other: &Self) -> Option { + self.as_vec().partial_cmp(other.as_vec()) } +} - /// Returns an underlying [`Vec`] - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.to_vec(), vec![1u8,2]); - /// ``` - pub fn to_vec(self) -> Vec { - self.inner +impl Ord for BoundedVec { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_vec().cmp(other.as_vec()) } +} - /// Extracts a slice containing the entire vector. - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(data.as_slice(), &[1u8,2]); - /// ``` - pub fn as_slice(&self) -> &[T] { - self.inner.as_slice() - } +// Only methods that consume self or aren't available generically on the upstream type. +// All &self read methods (as_vec, as_slice, get, iter, first, last, split_last, is_empty) +// are available via Deref to the upstream type. - /// Returns a reference for an element at index or `None` if out of bounds - /// - /// # Example - /// - /// ``` - /// use bounded_collections::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let elem = *data.get(1).unwrap(); - /// assert_eq!(elem, 2); - /// ``` - pub fn get(&self, index: usize) -> Option<&T> { - self.inner.get(index) +impl BoundedVec { + pub fn to_vec(self) -> Vec { + self.0.to_vec() } - /// Returns the number of elements in the vector - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec = vec![1u8,2].try_into().unwrap(); - /// assert_eq!(data.len(), 2); - /// ``` + /// Upstream only defines `len()` on NonEmpty; we provide it for all witnesses. pub fn len(&self) -> usize { - self.inner.len() + self.0.as_vec().len() } - /// Returns an iterator - pub fn iter(&self) -> Iter { - self.inner.iter() + pub fn is_empty(&self) -> bool { + self.0.as_vec().is_empty() } - /// Returns an iterator that allows to modify each value - pub fn iter_mut(&mut self) -> IterMut { - self.inner.iter_mut() + /// Deref only provides `&self` access; `iter_mut` needs explicit forwarding. + pub fn iter_mut(&mut self) -> std::slice::IterMut { + self.0.iter_mut() } } +// Methods that return our BoundedVec (must shadow the Deref'd upstream versions). + impl BoundedVec> { - /// Creates new BoundedVec or returns error if items count is out of bounds - /// - /// # Parameters - /// - /// * `items` - vector of items within bounds - /// - /// # Errors - /// - /// * `LowerBoundError` - if `items` len is less than L (lower bound) - /// * `UpperBoundError` - if `items` len is more than U (upper bound) - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::witnesses; - /// let data: BoundedVec<_, 2, 8, witnesses::NonEmpty<2, 8>> = - /// BoundedVec::<_, 2, 8, witnesses::NonEmpty<2, 8>>::from_vec(vec![1u8, 2]).unwrap(); - /// ``` pub fn from_vec(items: Vec) -> Result { - let witness = witnesses::non_empty::(); - let len = items.len(); - if len < L { - Err(BoundedVecOutOfBounds::LowerBoundError { - lower_bound: L, - got: len, - }) - } else if len > U { - Err(BoundedVecOutOfBounds::UpperBoundError { - upper_bound: U, - got: len, - }) - } else { - Ok(BoundedVec { - inner: items, - witness, - }) - } - } - - /// Returns the first element of non-empty Vec - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(*data.first(), 1); - /// ``` - pub fn first(&self) -> &T { - self.inner.first().unwrap() + >>::from_vec(items) + .map(Self) + .map_err(Into::into) } - /// Returns the last element of non-empty Vec - /// - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use std::convert::TryInto; - /// - /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); - /// assert_eq!(*data.last(), 2); - /// ``` - pub fn last(&self) -> &T { - self.inner.last().unwrap() - } - - /// Create a new `BoundedVec` by consuming `self` and mapping each element. - /// - /// This is useful as it keeps the knowledge that the length is >= L, <= U, - /// even through the old `BoundedVec` is consumed and turned into an iterator. - /// - /// # Example - /// - /// ``` - /// use bounded_collections::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let data = data.mapped(|x|x*2); - /// assert_eq!(data, [2u8,4].into()); - /// ``` pub fn mapped(self, map_fn: F) -> BoundedVec> where F: FnMut(T) -> N, { - BoundedVec { - inner: self.inner.into_iter().map(map_fn).collect::>(), - witness: self.witness, - } + BoundedVec(self.0.mapped(map_fn)) } - /// Create a new `BoundedVec` by mapping references to the elements of self - /// - /// This is useful as it keeps the knowledge that the length is >= L, <= U, - /// will still hold for new `BoundedVec` - /// - /// # Example - /// - /// ``` - /// use bounded_collections::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let data = data.mapped_ref(|x|x*2); - /// assert_eq!(data, [2u8,4].into()); - /// ``` pub fn mapped_ref(&self, map_fn: F) -> BoundedVec> where F: FnMut(&T) -> N, { - BoundedVec { - inner: self.inner.iter().map(map_fn).collect::>(), - witness: self.witness, - } + BoundedVec(self.0.mapped_ref(map_fn)) } - /// Create a new `BoundedVec` by consuming `self` and mapping each element - /// to a `Result`. - /// - /// This is useful as it keeps the knowledge that the length is preserved - /// even through the old `BoundedVec` is consumed and turned into an iterator. - /// - /// As this method consumes self, returning an error means that this - /// vec is dropped. I.e. this method behaves roughly like using a - /// chain of `into_iter()`, `map`, `collect::,E>>` and - /// then converting the `Vec` back to a `Vec1`. - /// - /// - /// # Errors - /// - /// Once any call to `map_fn` returns a error that error is directly - /// returned by this method. - /// - /// # Example - /// - /// ``` - /// use bounded_collections::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let data: Result, _> = data.try_mapped(|x| Err("failed")); - /// assert_eq!(data, Err("failed")); - /// ``` pub fn try_mapped( self, - mut map_fn: F, + map_fn: F, ) -> Result>, E> where F: FnMut(T) -> Result, { - let out = self - .inner - .into_iter() - .map(&mut map_fn) - .collect::, E>>()?; - - Ok(BoundedVec { - inner: out, - witness: self.witness, - }) + self.0.try_mapped(map_fn).map(BoundedVec) } - /// Create a new `BoundedVec` by mapping references of `self` elements - /// to a `Result`. - /// - /// This is useful as it keeps the knowledge that the length is preserved - /// even through the old `BoundedVec` is consumed and turned into an iterator. - /// - /// # Errors - /// - /// Once any call to `map_fn` returns a error that error is directly - /// returned by this method. - /// - /// # Example - /// - /// ``` - /// use bounded_collections::BoundedVec; - /// let data: BoundedVec = [1u8,2].into(); - /// let data: Result, _> = data.try_mapped_ref(|x| Err("failed")); - /// assert_eq!(data, Err("failed")); - /// ``` pub fn try_mapped_ref( &self, - mut map_fn: F, + map_fn: F, ) -> Result>, E> where F: FnMut(&T) -> Result, { - let out = self - .inner - .iter() - .map(&mut map_fn) - .collect::, E>>()?; - - Ok(BoundedVec { - inner: out, - witness: self.witness, - }) + self.0.try_mapped_ref(map_fn).map(BoundedVec) } - /// Returns the last and all the rest of the elements - pub fn split_last(&self) -> (&T, &[T]) { - self.inner.split_last().unwrap() - } - - /// Return a new BoundedVec with indices included pub fn enumerated(self) -> BoundedVec<(usize, T), L, U, witnesses::NonEmpty> { - BoundedVec { - inner: self.inner.into_iter().enumerate().collect(), - witness: self.witness, - } + BoundedVec(self.0.enumerated()) } - /// Return a Some(BoundedVec) or None if `v` is empty - /// # Example - /// ``` - /// use bounded_collections::BoundedVec; - /// use bounded_collections::OptBoundedVecToVec; - /// - /// let opt_bv_none = BoundedVec::::opt_empty_vec(vec![]).unwrap(); - /// assert!(opt_bv_none.is_none()); - /// assert_eq!(opt_bv_none.to_vec(), Vec::::new()); - /// let opt_bv_some = BoundedVec::::opt_empty_vec(vec![0u8, 2]).unwrap(); - /// assert!(opt_bv_some.is_some()); - /// assert_eq!(opt_bv_some.to_vec(), vec![0u8, 2]); - /// ``` pub fn opt_empty_vec( v: Vec, ) -> Result>>, BoundedVecOutOfBounds> { - if v.is_empty() { - Ok(None) - } else { - Ok(Some(Self::from_vec(v)?)) - } + Upstream::opt_empty_vec(v) + .map(|opt| opt.map(BoundedVec)) + .map_err(Into::into) + } +} + +impl BoundedVec> { + pub fn from_vec(items: Vec) -> Result { + >>::from_vec(items) + .map(Self) + .map_err(Into::into) } } @@ -499,15 +202,11 @@ impl TryFrom> for BoundedVec From<[T; L]> for BoundedVec> { fn from(arr: [T; L]) -> Self { - BoundedVec { - inner: arr.into(), - witness: witnesses::non_empty(), - } + Self(Upstream::from(arr)) } } @@ -515,13 +214,13 @@ impl From { fn from(v: BoundedVec>) -> Self { - v.inner + v.0.into() } } impl From>> for Vec { fn from(v: BoundedVec>) -> Self { - v.inner + v.0.to_vec() } } @@ -530,7 +229,7 @@ impl IntoIterator for BoundedVec; fn into_iter(self) -> Self::IntoIter { - self.inner.into_iter() + self.0.into_iter() } } @@ -539,7 +238,7 @@ impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a BoundedVec; fn into_iter(self) -> Self::IntoIter { - self.inner.iter() + self.0.iter() } } @@ -548,56 +247,36 @@ impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a mut BoundedV type IntoIter = core::slice::IterMut<'a, T>; fn into_iter(self) -> Self::IntoIter { - self.inner.iter_mut() + self.0.iter_mut() } } impl AsRef> for BoundedVec { fn as_ref(&self) -> &Vec { - &self.inner + self.0.as_ref() } } impl AsRef<[T]> for BoundedVec { fn as_ref(&self) -> &[T] { - self.inner.as_slice() + self.0.as_ref() } } /// `AsRef<[T; N]>` is only available when `L == U == N`, i.e. the vector has /// a fixed length known at compile time. -/// -/// ``` -/// use bounded_collections::BoundedVec; -/// let data: BoundedVec = [1u8, 2, 3].into(); -/// let arr: &[u8; 3] = data.as_ref(); -/// assert_eq!(arr, &[1, 2, 3]); -/// ``` -/// -/// Does not compile when L != U (variable-length vec): -/// ```compile_fail,E0277 -/// use bounded_collections::BoundedVec; -/// let data: BoundedVec = vec![1u8, 2].try_into().unwrap(); -/// let _: &[u8; 2] = data.as_ref(); -/// ``` -/// -/// Does not compile when N differs from L and U: -/// ```compile_fail,E0277 -/// use bounded_collections::BoundedVec; -/// let data: BoundedVec = [1u8, 2, 3].into(); -/// let _: &[u8; 4] = data.as_ref(); -/// ``` impl AsRef<[T; N]> for BoundedVec> { fn as_ref(&self) -> &[T; N] { - self.inner.as_slice().try_into().expect( - "When L == U == N, the length is guaranteed to be exactly N, so the conversion to a fixed-size array is infallible", - ) + self.0 + .as_slice() + .try_into() + .expect("When L == U == N, the length is guaranteed to be exactly N") } } -/// [`Option>`] to [`Vec`] +/// `Option>` to `Vec` pub trait OptBoundedVecToVec { - /// [`Option>`] to [`Vec`] + /// `Option>` to `Vec` fn to_vec(self) -> Vec; } @@ -617,7 +296,7 @@ mod borsh_impl { for BoundedVec { fn serialize(&self, writer: &mut Writer) -> std::io::Result<()> { - self.inner.serialize(writer) + self.0.as_vec().serialize(writer) } } @@ -676,7 +355,7 @@ mod serde_impl { where S: serde::Serializer, { - self.inner.serialize(serializer) + self.0.as_vec().serialize(serializer) } } @@ -887,15 +566,10 @@ mod tests { // Given let items: Vec = vec![]; // When - let result = EmptyBoundedVec::::from_vec(items); + let result = EmptyBoundedVec::::from_vec(items).unwrap(); // Then - assert_eq!( - result, - Ok(BoundedVec { - inner: vec![], - witness: witnesses::possibly_empty() - }) - ); + assert!(result.is_empty()); + assert_eq!(result.as_vec(), &Vec::::new()); } #[test]