Repository: commonwarexyz/monorepo
Pull request #770: [Codec] Add `BTreeMap`, `BTreeSet`, `HashSet`
Author: login: BrendanChou
Created: 2025-04-18T15:26:04Z | Merged: 2025-05-28T20:16:21Z

Fixes #768 

Linked issue #768: [codec] Add built-ins for `BTreeMap`, `BTreeSet`, and `HashSet`
The sets can go in a new `types/sets.rs` file
The map can go in the existing `types/maps.rs` file

I wonder if there is a way to get the implementations to generalize over a `Map` and a `Set`? Doesn't seem like it but maybe I can do it with `IntoIterator` or `FromIterator` (?)

Diff:
diff --git a/codec/src/types/map.rs b/codec/src/types/map.rs
index 8a4fb854b3..3ee9293028 100644
--- a/codec/src/types/map.rs
+++ b/codec/src/types/map.rs
@@ -1,4 +1,4 @@
-//! Codec implementations for HashMap.
+//! Codec implementations for various map types.
 //!
 //! For portability and consistency between architectures,
 //! the size of the map must fit within a [`u32`].
@@ -9,24 +9,127 @@ use crate::{
     RangeCfg,
 };
 use bytes::{Buf, BufMut};
-use std::{collections::HashMap, hash::Hash};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeMap, HashMap},
+    hash::Hash,
+};
+
+const BTREEMAP_TYPE: &str = "BTreeMap";
+const HASHMAP_TYPE: &str = "HashMap";
+
+/// Read keyed items from [Buf] in ascending order.
+fn read_ordered_map<K, V, F>(
+    buf: &mut impl Buf,
+    len: usize,
+    k_cfg: &K::Cfg,
+    v_cfg: &V::Cfg,
+    mut insert: F,
+    map_type: &'static str,
+) -> Result<(), Error>
+where
+    K: Read + Ord,
+    V: Read,
+    F: FnMut(K, V) -> Option<V>,
+{
+    let mut last: Option<(K, V)> = None;
+    for _ in 0..len {
+        // Read key
+        let key = K::read_cfg(buf, k_cfg)?;
+
+        // Check if keys are in ascending order relative to the previous key
+        if let Some((ref last_key, _)) = last {
+            match key.cmp(last_key) {
+                Ordering::Equal => return Err(Error::Invalid(map_type, "Duplicate key")),
+                Ordering::Less => return Err(Error::Invalid(map_type, "Keys must ascend")),
+                _ => {}
+            }
+        }
+
+        // Read value
+        let value = V::read_cfg(buf, v_cfg)?;
+
+        // Add previous item, if exists
+        if let Some((last_key, last_value)) = last.take() {
+            insert(last_key, last_value);
+        }
+        last = Some((key, value));
+    }
+
+    // Add last item, if exists
+    if let Some((last_key, last_value)) = last {
+        insert(last_key, last_value);
+    }
+
+    Ok(())
+}
+
+// ---------- BTreeMap ----------
+
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for BTreeMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Keys are already sorted in BTreeMap, so we can iterate directly
+        for (k, v) in self {
+            k.write(buf);
+            v.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize, V: EncodeSize> EncodeSize for BTreeMap<K, V> {
+    fn encode_size(&self) -> usize {
+        // Start with the size of the length prefix
+        let mut size = self.len().encode_size();
+
+        // Add the encoded size of each key and value
+        for (k, v) in self {
+            size += k.encode_size();
+            size += v.encode_size();
+        }
+        size
+    }
+}
+
+impl<K: Read + Clone + Ord + Hash + Eq, V: Read + Clone> Read for BTreeMap<K, V> {
+    type Cfg = (RangeCfg, (K::Cfg, V::Cfg));
+
+    fn read_cfg(buf: &mut impl Buf, (range, (k_cfg, v_cfg)): &Self::Cfg) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut map = BTreeMap::new();
+
+        // Read items in ascending order
+        read_ordered_map(
+            buf,
+            len,
+            k_cfg,
+            v_cfg,
+            |k, v| map.insert(k, v),
+            BTREEMAP_TYPE,
+        )?;
+
+        Ok(map)
+    }
+}
+
+// ---------- HashMap ----------
 
-// Write implementation for HashMap
 impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
     fn write(&self, buf: &mut impl BufMut) {
         self.len().write(buf);
 
         // Sort the keys to ensure deterministic encoding
-        let mut keys: Vec<_> = self.keys().collect();
-        keys.sort();
-        for key in keys {
-            key.write(buf);
-            self.get(key).unwrap().write(buf);
+        let mut entries: Vec<_> = self.iter().collect();
+        entries.sort_by(|a, b| a.0.cmp(b.0));
+        for (k, v) in entries {
+            k.write(buf);
+            v.write(buf);
         }
     }
 }
 
-// EncodeSize implementation for HashMap
 impl<K: Ord + Hash + Eq + EncodeSize, V: EncodeSize> EncodeSize for HashMap<K, V> {
     fn encode_size(&self) -> usize {
         // Start with the size of the length prefix
@@ -34,9 +137,9 @@ impl<K: Ord + Hash + Eq + EncodeSize, V: EncodeSize> EncodeSize for HashMap<K, V
 
         // Add the encoded size of each key and value
         // Note: Iteration order doesn't matter for size calculation.
-        for (key, value) in self {
-            size += key.encode_size();
-            size += value.encode_size();
+        for (k, v) in self {
+            size += k.encode_size();
+            size += v.encode_size();
         }
         size
     }
@@ -51,27 +154,15 @@ impl<K: Read + Clone + Ord + Hash + Eq, V: Read + Clone> Read for HashMap<K, V>
         let len = usize::read_cfg(buf, range)?;
         let mut map = HashMap::with_capacity(len);
 
-        // Keep track of the last key read
-        let mut last_key: Option<K> = None;
-
-        // Read each key-value pair
-        for _ in 0..len {
-            let key = K::read_cfg(buf, k_cfg)?;
-
-            // Check if keys are in ascending order relative to the previous key
-            if let Some(ref last) = last_key {
-                use std::cmp::Ordering;
-                match key.cmp(last) {
-                    Ordering::Equal => return Err(Error::Invalid("HashMap", "Duplicate key")),
-                    Ordering::Less => return Err(Error::Invalid("HashMap", "Keys must ascend")),
-                    _ => {}
-                }
-            }
-            last_key = Some(key.clone());
-
-            let value = V::read_cfg(buf, v_cfg)?;
-            map.insert(key, value);
-        }
+        // Read items in ascending order
+        read_ordered_map(
+            buf,
+            len,
+            k_cfg,
+            v_cfg,
+            |k, v| map.insert(k, v),
+            HASHMAP_TYPE,
+        )?;
 
         Ok(map)
     }
@@ -79,40 +170,374 @@ impl<K: Read + Clone + Ord + Hash + Eq, V: Read + Clone> Read for HashMap<K, V>
 
 #[cfg(test)]
 mod tests {
-    use crate::{
-        codec::{Decode, Encode, EncodeSize, FixedSize, Read, Write},
-        error::Error,
-        RangeCfg,
-    };
-    use bytes::{BufMut, Bytes, BytesMut};
-    use std::{collections::HashMap, fmt::Debug, hash::Hash};
+    use super::*;
+    use crate::{Decode, Encode, FixedSize};
+    use bytes::{Bytes, BytesMut};
+    use std::fmt::Debug;
+
+    // Manual round trip test function for BTreeMap with non-default configs
+    fn round_trip_btree<K, V, KCfg, VCfg>(
+        map: &BTreeMap<K, V>,
+        range_cfg: RangeCfg,
+        k_cfg: KCfg,
+        v_cfg: VCfg,
+    ) where
+        K: Write + EncodeSize + Read<Cfg = KCfg> + Clone + Ord + Hash + Eq + PartialEq + Debug,
+        V: Write + EncodeSize + Read<Cfg = VCfg> + Clone + PartialEq + Debug,
+        BTreeMap<K, V>: Read<Cfg = (RangeCfg, (K::Cfg, V::Cfg))>
+            + Decode<Cfg = (RangeCfg, (K::Cfg, V::Cfg))>
+            + PartialEq
+            + Write
+            + EncodeSize,
+    {
+        let encoded = map.encode();
+        assert_eq!(encoded.len(), map.encode_size());
+        let config_tuple = (range_cfg, (k_cfg, v_cfg));
+        let decoded = BTreeMap::<K, V>::decode_cfg(encoded, &config_tuple)
+            .expect("decode_cfg failed for BTreeMap");
+        assert_eq!(map, &decoded);
+    }
+
+    fn round_trip_hash<K, V, KCfg, VCfg>(
+        map: &HashMap<K, V>,
+        range_cfg: RangeCfg,
+        k_cfg: KCfg,
+        v_cfg: VCfg,
+    ) where
+        K: Write + EncodeSize + Read<Cfg = KCfg> + Clone + Ord + Hash + Eq + PartialEq + Debug,
+        V: Write + EncodeSize + Read<Cfg = VCfg> + Clone + PartialEq + Debug,
+        HashMap<K, V>: Read<Cfg = (RangeCfg, (K::Cfg, V::Cfg))>
+            + Decode<Cfg = (RangeCfg, (K::Cfg, V::Cfg))>
+            + PartialEq
+            + Write
+            + EncodeSize,
+    {
+        let encoded = map.encode();
+        assert_eq!(encoded.len(), map.encode_size());
+        let config_tuple = (range_cfg, (k_cfg, v_cfg));
+        let decoded = HashMap::<K, V>::decode_cfg(encoded, &config_tuple)
+            .expect("decode_cfg failed for HashMap");
+        assert_eq!(map, &decoded);
+    }
 
     // Manual round trip test function for non-default configs
     fn round_trip<K, V>(map: &HashMap<K, V>, range_cfg: RangeCfg, k_cfg: K::Cfg, v_cfg: V::Cfg)
     where
-        K: Write + EncodeSize + Read + Clone + Ord + Hash + Eq + Debug + PartialEq,
-        V: Write + EncodeSize + Read + Clone + Debug + PartialEq,
-        HashMap<K, V>:
-            Read<Cfg = (RangeCfg, (K::Cfg, V::Cfg))> + Debug + PartialEq + Write + EncodeSize,
+        K: Write + EncodeSize + Read + Clone + Ord + Hash + Eq + PartialEq + Debug,
+        V: Write + EncodeSize + Read + Clone + PartialEq + Debug,
+        HashMap<K, V>: Read<Cfg = (RangeCfg, (K::Cfg, V::Cfg))> + PartialEq + Write + EncodeSize,
     {
         let encoded = map.encode();
         let config_tuple = (range_cfg, (k_cfg, v_cfg));
-        let decoded =
-            HashMap::<K, V>::decode_cfg(encoded, &config_tuple).expect("decode_cfg failed");
+        let decoded = HashMap::<K, V>::decode_cfg(encoded, &config_tuple)
+            .expect("decode_cfg failed for HashMap");
         assert_eq!(map, &decoded);
     }
 
+    // --- BTreeMap Tests ---
+
+    #[test]
+    fn test_empty_btreemap() {
+        let map = BTreeMap::<u32, u64>::new();
+        round_trip_btree(&map, (..).into(), (), ());
+        assert_eq!(map.encode_size(), 1); // varint 0
+        let encoded = map.encode();
+        assert_eq!(encoded, Bytes::from_static(&[0]));
+    }
+
+    #[test]
+    fn test_simple_btreemap_u32_u64() {
+        let mut map = BTreeMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+        map.insert(2u32, 200u64);
+        round_trip_btree(&map, (..).into(), (), ());
+        assert_eq!(map.encode_size(), 1 + 3 * (u32::SIZE + u64::SIZE));
+        // Check encoding order (BTreeMap guarantees sorted keys: 1, 2, 5)
+        let mut expected = BytesMut::new();
+        3usize.write(&mut expected); // Map length = 3
+        1u32.write(&mut expected);
+        100u64.write(&mut expected);
+        2u32.write(&mut expected);
+        200u64.write(&mut expected);
+        5u32.write(&mut expected);
+        500u64.write(&mut expected);
+        assert_eq!(map.encode(), expected.freeze());
+    }
+
+    #[test]
+    fn test_large_btreemap() {
+        // Fixed-size items
+        let mut map = BTreeMap::new();
+        for i in 0..1000 {
+            map.insert(i as u16, i as u64 * 2);
+        }
+        round_trip_btree(&map, (0..=1000).into(), (), ());
+
+        // Variable-size items
+        let mut map = BTreeMap::new();
+        for i in 0..1000usize {
+            map.insert(i, 1000usize + i);
+        }
+        round_trip_btree(
+            &map,
+            (0..=1000).into(),
+            (..=1000).into(),
+            (1000..=2000).into(),
+        );
+    }
+
+    #[test]
+    fn test_btreemap_with_variable_values() {
+        let mut map = BTreeMap::new();
+        map.insert(Bytes::from_static(b"apple"), vec![1, 2]);
+        map.insert(Bytes::from_static(b"banana"), vec![3, 4, 5]);
+        map.insert(Bytes::from_static(b"cherry"), vec![]);
+
+        let map_range = 0..=10;
+        let key_range = ..=10;
+        let val_range = 0..=100;
+
+        round_trip_btree(
+            &map,
+            map_range.into(),
+            key_range.into(),
+            (val_range.into(), ()),
+        );
+    }
+
+    #[test]
+    fn test_btreemap_decode_length_limit_exceeded() {
+        let mut map = BTreeMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+
+        let encoded = map.encode();
+        let config_tuple = ((0..=1).into(), ((), ()));
+
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::InvalidLength(2))));
+    }
+
+    #[test]
+    fn test_btreemap_decode_value_length_limit_exceeded() {
+        let mut map = BTreeMap::new();
+        map.insert(Bytes::from_static(b"key1"), vec![1, 2, 3, 4, 5]);
+
+        let key_range = ..=10;
+        let map_range = 0..=10;
+        let restrictive_val_range = 0..=3;
+
+        let encoded = map.encode();
+        let config_tuple = (
+            map_range.into(),
+            (key_range.into(), (restrictive_val_range.into(), ())),
+        );
+        let result = BTreeMap::<Bytes, Vec<u8>>::decode_cfg(encoded, &config_tuple);
+
+        assert!(matches!(result, Err(Error::InvalidLength(5))));
+    }
+
+    #[test]
+    fn test_btreemap_decode_invalid_key_order() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Map length = 2
+        5u32.write(&mut encoded); // Key 5
+        500u64.write(&mut encoded); // Value 500
+        2u32.write(&mut encoded); // Key 2 (out of order)
+        200u64.write(&mut encoded); // Value 200
+
+        let range = (..).into();
+        let config_tuple = (range, ((), ()));
+
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        // Note: Error message uses HashMap currently, should ideally be BTreeMap
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("BTreeMap", "Keys must ascend"))
+        ));
+    }
+
+    #[test]
+    fn test_btreemap_decode_duplicate_key() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Map length = 2
+        1u32.write(&mut encoded); // Key 1
+        100u64.write(&mut encoded); // Value 100
+        1u32.write(&mut encoded); // Duplicate Key 1
+        200u64.write(&mut encoded); // Value 200
+
+        let range = (..).into();
+        let config_tuple = (range, ((), ()));
+
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        // Note: Error message uses HashMap currently, should ideally be BTreeMap
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("BTreeMap", "Duplicate key"))
+        ));
+    }
+
+    #[test]
+    fn test_btreemap_decode_end_of_buffer_key() {
+        let mut map = BTreeMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+
+        let mut encoded = map.encode();
+        encoded.truncate(map.encode_size() - 10); // Truncate during last key/value pair (key 5)
+
+        let range = (..).into();
+        let config_tuple = (range, ((), ()));
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_btreemap_decode_end_of_buffer_value() {
+        let mut map = BTreeMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+
+        let mut encoded = map.encode();
+        encoded.truncate(map.encode_size() - 4); // Truncate during last value (for key 5)
+
+        let range = (..).into();
+        let config_tuple = (range, ((), ()));
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_btreemap_decode_extra_data() {
+        let mut map = BTreeMap::new();
+        map.insert(1u32, 100u64);
+
+        let mut encoded = map.encode();
+        encoded.put_u8(0xFF); // Add extra byte
+
+        // Use decode_cfg which enforces buffer is fully consumed
+        let config_tuple = ((..).into(), ((), ()));
+        let result = BTreeMap::<u32, u64>::decode_cfg(encoded.clone(), &config_tuple);
+        assert!(matches!(result, Err(Error::ExtraData(1))));
+
+        // Verify that read_cfg would succeed (doesn't check for extra data)
+        let read_result = BTreeMap::<u32, u64>::read_cfg(&mut encoded.clone(), &config_tuple);
+        assert!(read_result.is_ok());
+        let decoded_map = read_result.unwrap();
+        assert_eq!(decoded_map.len(), 1);
+        assert_eq!(decoded_map.get(&1u32), Some(&100u64));
+    }
+
+    #[test]
+    fn test_btreemap_deterministic_encoding() {
+        // In-order
+        let mut map2 = BTreeMap::new();
+        (0..=1000u32).for_each(|i| {
+            map2.insert(i, i * 2);
+        });
+
+        // Reverse order
+        let mut map1 = BTreeMap::new();
+        (0..=1000u32).rev().for_each(|i| {
+            map1.insert(i, i * 2);
+        });
+
+        assert_eq!(map1.encode(), map2.encode());
+    }
+
+    #[test]
+    fn test_btreemap_conformity() {
+        // Case 1: Empty BTreeMap<u8, u16>
+        let map1 = BTreeMap::<u8, u16>::new();
+        let mut expected1 = BytesMut::new();
+        0usize.write(&mut expected1); // Length 0
+        assert_eq!(map1.encode(), expected1.freeze());
+        assert_eq!(map1.encode_size(), 1);
+
+        // Case 2: Simple BTreeMap<u8, u16>
+        // BTreeMap will store and encode keys in sorted order: 1, 2
+        let mut map2 = BTreeMap::<u8, u16>::new();
+        map2.insert(2u8, 0xBBBBu16); // Inserted out of order
+        map2.insert(1u8, 0xAAAAu16);
+
+        let mut expected2 = BytesMut::new();
+        2usize.write(&mut expected2); // Length 2
+        1u8.write(&mut expected2); // Key 1
+        0xAAAAu16.write(&mut expected2); // Value for key 1
+        2u8.write(&mut expected2); // Key 2
+        0xBBBBu16.write(&mut expected2); // Value for key 2
+        assert_eq!(map2.encode(), expected2.freeze());
+        // Expected size: 1 (len) + (1 (u8) + 2 (u16)) * 2 entries = 1 + 3*2 = 7
+        assert_eq!(map2.encode_size(), 1 + (u8::SIZE + u16::SIZE) * 2);
+
+        // Case 3: BTreeMap<u16, bool>
+        // BTreeMap will store and encode keys in sorted order: 0x0101, 0x0202, 0x0303
+        let mut map3 = BTreeMap::<u16, bool>::new();
+        map3.insert(0x0303u16, true);
+        map3.insert(0x0101u16, false);
+        map3.insert(0x0202u16, true);
+
+        let mut expected3 = BytesMut::new();
+        3usize.write(&mut expected3); // Length 3
+        0x0101u16.write(&mut expected3); // Key 0x0101
+        false.write(&mut expected3); // Value false (0x00)
+        0x0202u16.write(&mut expected3); // Key 0x0202
+        true.write(&mut expected3); // Value true (0x01)
+        0x0303u16.write(&mut expected3); // Key 0x0303
+        true.write(&mut expected3); // Value true (0x01)
+        assert_eq!(map3.encode(), expected3.freeze());
+        // Expected size: 1 (len) + (2 (u16) + 1 (bool)) * 3 entries = 1 + 3*3 = 10
+        assert_eq!(map3.encode_size(), 1 + (u16::SIZE + bool::SIZE) * 3);
+
+        // Case 4: BTreeMap with Bytes as key and Vec<u8> as value
+        // BTreeMap sorts keys: "a", "b"
+        let mut map4 = BTreeMap::<Bytes, Vec<u8>>::new();
+        map4.insert(Bytes::from_static(b"b"), vec![20u8, 21u8]);
+        map4.insert(Bytes::from_static(b"a"), vec![10u8]);
+
+        let mut expected4 = BytesMut::new();
+        2usize.write(&mut expected4); // Map length = 2
+
+        // Key "a" (length 1, 'a')
+        Bytes::from_static(b"a").write(&mut expected4);
+        // Value vec![10u8] (length 1, 10u8)
+        vec![10u8].write(&mut expected4);
+
+        // Key "b" (length 1, 'b')
+        Bytes::from_static(b"b").write(&mut expected4);
+        // Value vec![20u8, 21u8] (length 2, 20u8, 21u8)
+        vec![20u8, 21u8].write(&mut expected4);
+
+        assert_eq!(map4.encode(), expected4.freeze());
+        // Expected size:
+        // map_len_size = 1
+        // key_a_size = 1 (len) + 1 (char) = 2
+        // val_a_size = 1 (len) + 1 (byte) = 2
+        // key_b_size = 1 (len) + 1 (char) = 2
+        // val_b_size = 1 (len) + 2 (bytes) = 3
+        // total = 1 + 2 + 2 + 2 + 3 = 10
+        let expected_size = 1usize.encode_size()
+            + Bytes::from_static(b"a").encode_size()
+            + vec![10u8].encode_size()
+            + Bytes::from_static(b"b").encode_size()
+            + vec![20u8, 21u8].encode_size();
+        assert_eq!(map4.encode_size(), expected_size);
+    }
+
+    // --- HashMap Tests ---
+
     #[test]
-    fn test_empty_map() {
+    fn test_empty_hashmap() {
         let map = HashMap::<u32, u64>::new();
-        round_trip(&map, (..).into(), (), ());
+        round_trip_hash(&map, (..).into(), (), ());
         assert_eq!(map.encode_size(), 1);
         let encoded = map.encode();
         assert_eq!(encoded, 0usize.encode());
     }
 
     #[test]
-    fn test_simple_map_u32_u64() {
+    fn test_simple_hashmap_u32_u64() {
         let mut map = HashMap::new();
         map.insert(1u32, 100u64);
         map.insert(5u32, 500u64);
@@ -122,16 +547,29 @@ mod tests {
     }
 
     #[test]
-    fn test_large_map() {
+    fn test_large_hashmap() {
+        // Fixed-size items
         let mut map = HashMap::new();
         for i in 0..1000 {
-            map.insert(i, i as u64 * 2);
+            map.insert(i as u16, i as u64 * 2);
         }
-        round_trip(&map, (..=1000).into(), (), ());
+        round_trip_hash(&map, (0..=1000).into(), (), ());
+
+        // Variable-size items
+        let mut map = HashMap::new();
+        for i in 0..1000usize {
+            map.insert(i, 1000usize + i);
+        }
+        round_trip_hash(
+            &map,
+            (0..=1000).into(),
+            (..=1000).into(),
+            (1000..=2000).into(),
+        );
     }
 
     #[test]
-    fn test_map_with_variable_values() {
+    fn test_hashmap_with_variable_values() {
         let mut map = HashMap::new();
         map.insert(Bytes::from_static(b"apple"), vec![1, 2]);
         map.insert(Bytes::from_static(b"banana"), vec![3, 4, 5]);
@@ -141,25 +579,24 @@ mod tests {
         let key_range = RangeCfg::from(..=10);
         let val_range = RangeCfg::from(0..=100);
 
-        round_trip(&map, map_range, key_range, (val_range, ()));
+        round_trip_hash(&map, map_range, key_range, (val_range, ()));
     }
 
     #[test]
-    fn test_decode_length_limit_exceeded() {
+    fn test_hashmap_decode_length_limit_exceeded() {
         let mut map = HashMap::new();
         map.insert(1u32, 100u64);
         map.insert(5u32, 500u64);
 
         let encoded = map.encode();
-        let restrictive_range = (0..=1).into();
-        let config_tuple = (restrictive_range, ((), ()));
+        let config_tuple = ((0..=1).into(), ((), ()));
 
         let result = HashMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
         assert!(matches!(result, Err(Error::InvalidLength(2))));
     }
 
     #[test]
-    fn test_decode_value_length_limit_exceeded() {
+    fn test_hashmap_decode_value_length_limit_exceeded() {
         let mut map = HashMap::new();
         map.insert(Bytes::from_static(b"key1"), vec![1u8, 2u8, 3u8, 4u8, 5u8]);
 
@@ -175,7 +612,7 @@ mod tests {
     }
 
     #[test]
-    fn test_decode_invalid_key_order() {
+    fn test_hashmap_decode_invalid_key_order() {
         let mut encoded = BytesMut::new();
         2usize.write(&mut encoded); // Map length = 2
         5u32.write(&mut encoded); // Key 5
@@ -194,7 +631,7 @@ mod tests {
     }
 
     #[test]
-    fn test_decode_duplicate_key() {
+    fn test_hashmap_decode_duplicate_key() {
         let mut encoded = BytesMut::new();
         2usize.write(&mut encoded); // Map length = 2
         1u32.write(&mut encoded); // Key 1
@@ -213,7 +650,7 @@ mod tests {
     }
 
     #[test]
-    fn test_decode_end_of_buffer_key() {
+    fn test_hashmap_decode_end_of_buffer_key() {
         let mut map = HashMap::new();
         map.insert(1u32, 100u64);
         map.insert(5u32, 500u64);
@@ -228,7 +665,7 @@ mod tests {
     }
 
     #[test]
-    fn test_decode_end_of_buffer_value() {
+    fn test_hashmap_decode_end_of_buffer_value() {
         let mut map = HashMap::new();
         map.insert(1u32, 100u64);
         map.insert(5u32, 500u64);
@@ -243,17 +680,15 @@ mod tests {
     }
 
     #[test]
-    fn test_decode_extra_data() {
+    fn test_hashmap_decode_extra_data() {
         let mut map = HashMap::new();
         map.insert(1u32, 100u64);
 
         let mut encoded = map.encode();
         encoded.put_u8(0xFF); // Add extra byte
 
-        let range = RangeCfg::from(..);
-        let config_tuple = (range, ((), ()));
-
         // Use decode_cfg which enforces buffer is fully consumed
+        let config_tuple = ((..).into(), ((), ()));
         let result = HashMap::<u32, u64>::decode_cfg(encoded.clone(), &config_tuple);
         assert!(matches!(result, Err(Error::ExtraData(1))));
 
@@ -266,48 +701,80 @@ mod tests {
     }
 
     #[test]
-    fn test_conformity() {
-        let mut map1 = HashMap::<u8, u16>::new();
-        assert_eq!(map1.encode(), &[0x00][..]); // Empty map
-
-        map1.insert(1u8, 0xAAAAu16);
-        map1.insert(2u8, 0xBBBBu16);
-        // Expected: len=2 (0x02)
-        // Key 1 (0x01), Value 0xAAAA (0xAA, 0xAA)
-        // Key 2 (0x02), Value 0xBBBB (0xBB, 0xBB)
-        // Keys are sorted for encoding.
-        assert_eq!(
-            map1.encode(),
-            &[0x02, 0x01, 0xAA, 0xAA, 0x02, 0xBB, 0xBB][..]
-        );
-
-        let mut map2 = HashMap::<u16, bool>::new();
-        map2.insert(0x0303u16, true);
-        map2.insert(0x0101u16, false);
-        map2.insert(0x0202u16, true);
-        // Expected: len=3 (0x03)
-        // Key 0x0101, Value false (0x00)
-        // Key 0x0202, Value true (0x01)
-        // Key 0x0303, Value true (0x01)
-        assert_eq!(
-            map2.encode(),
-            &[0x03, 0x01, 0x01, 0x00, 0x02, 0x02, 0x01, 0x03, 0x03, 0x01][..]
-        );
+    fn test_hashmap_deterministic_encoding() {
+        // In-order
+        let mut map2 = HashMap::new();
+        (0..=1000u32).for_each(|i| {
+            map2.insert(i, i * 2);
+        });
+
+        // Reverse order
+        let mut map1 = HashMap::new();
+        (0..=1000u32).rev().for_each(|i| {
+            map1.insert(i, i * 2);
+        });
+
+        assert_eq!(map1.encode(), map2.encode());
+    }
 
-        // Map with Bytes as key and Vec<u8> as value
-        let mut map3 = HashMap::<Bytes, Vec<u8>>::new();
-        map3.insert(Bytes::from_static(b"b"), vec![20u8, 21u8]);
-        map3.insert(Bytes::from_static(b"a"), vec![10u8]);
-        // Expected: len=2 (0x02)
-        // Key "a": len=1 (0x01), 'a' (0x61)
-        // Value vec![10u8]: len=1 (0x01), 10u8 (0x0A)
-        // Key "b": len=1 (0x01), 'b' (0x62)
-        // Value vec![20u8, 21u8]: len=2 (0x02), 20u8 (0x14), 21u8 (0x15)
-        let mut expected_map3 = vec![0x02]; // Map length
-        expected_map3.extend_from_slice(&[0x01, 0x61]); // Key "a"
-        expected_map3.extend_from_slice(&[0x01, 0x0A]); // Value vec![10u8]
-        expected_map3.extend_from_slice(&[0x01, 0x62]); // Key "b"
-        expected_map3.extend_from_slice(&[0x02, 0x14, 0x15]); // Value vec![20u8, 21u8]
-        assert_eq!(map3.encode(), expected_map3.as_slice());
+    #[test]
+    fn test_hashmap_conformity() {
+        // Case 1: Empty HashMap<u8, u16>
+        let map1 = HashMap::<u8, u16>::new();
+        let mut expected1 = BytesMut::new();
+        0usize.write(&mut expected1); // Length 0
+        assert_eq!(map1.encode(), expected1.freeze());
+
+        // Case 2: Simple HashMap<u8, u16>
+        // Keys are sorted for encoding: 1, 2
+        let mut map2 = HashMap::<u8, u16>::new();
+        map2.insert(2u8, 0xBBBBu16); // Inserted out of order
+        map2.insert(1u8, 0xAAAAu16);
+
+        let mut expected2 = BytesMut::new();
+        2usize.write(&mut expected2); // Length 2
+        1u8.write(&mut expected2); // Key 1
+        0xAAAAu16.write(&mut expected2); // Value for key 1
+        2u8.write(&mut expected2); // Key 2
+        0xBBBBu16.write(&mut expected2); // Value for key 2
+        assert_eq!(map2.encode(), expected2.freeze());
+
+        // Case 3: HashMap<u16, bool>
+        // Keys are sorted for encoding: 0x0101, 0x0202, 0x0303
+        let mut map3 = HashMap::<u16, bool>::new();
+        map3.insert(0x0303u16, true);
+        map3.insert(0x0101u16, false);
+        map3.insert(0x0202u16, true);
+
+        let mut expected3 = BytesMut::new();
+        3usize.write(&mut expected3); // Length 3
+        0x0101u16.write(&mut expected3); // Key 0x0101
+        false.write(&mut expected3); // Value false (0x00)
+        0x0202u16.write(&mut expected3); // Key 0x0202
+        true.write(&mut expected3); // Value true (0x01)
+        0x0303u16.write(&mut expected3); // Key 0x0303
+        true.write(&mut expected3); // Value true (0x01)
+        assert_eq!(map3.encode(), expected3.freeze());
+
+        // Case 4: HashMap with Bytes as key and Vec<u8> as value
+        // Keys are sorted for encoding: "a", "b"
+        let mut map4 = HashMap::<Bytes, Vec<u8>>::new();
+        map4.insert(Bytes::from_static(b"b"), vec![20u8, 21u8]);
+        map4.insert(Bytes::from_static(b"a"), vec![10u8]);
+
+        let mut expected4 = BytesMut::new();
+        2usize.write(&mut expected4); // Map length = 2
+
+        // Key "a" (length 1, 'a')
+        Bytes::from_static(b"a").write(&mut expected4);
+        // Value vec![10u8] (length 1, 10u8)
+        vec![10u8].write(&mut expected4);
+
+        // Key "b" (length 1, 'b')
+        Bytes::from_static(b"b").write(&mut expected4);
+        // Value vec![20u8, 21u8] (length 2, 20u8, 21u8)
+        vec![20u8, 21u8].write(&mut expected4);
+
+        assert_eq!(map4.encode(), expected4.freeze());
     }
 }
diff --git a/codec/src/types/mod.rs b/codec/src/types/mod.rs
index 77adf04849..d2d6f3d4d0 100644
--- a/codec/src/types/mod.rs
+++ b/codec/src/types/mod.rs
@@ -4,5 +4,6 @@ pub mod bytes;
 pub mod map;
 pub mod net;
 pub mod primitives;
+pub mod set;
 pub mod tuple;
 pub mod vec;
diff --git a/codec/src/types/set.rs b/codec/src/types/set.rs
new file mode 100644
index 0000000000..defe269e39
--- /dev/null
+++ b/codec/src/types/set.rs
@@ -0,0 +1,589 @@
+//! Codec implementations for various set types.
+//!
+//! For portability and consistency between architectures,
+//! the size of the set must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    RangeCfg,
+};
+use bytes::{Buf, BufMut};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashSet},
+    hash::Hash,
+};
+
+const BTREESET_TYPE: &str = "BTreeSet";
+const HASHSET_TYPE: &str = "HashSet";
+
+/// Read items from [Buf] in ascending order.
+fn read_ordered_set<K, F>(
+    buf: &mut impl Buf,
+    len: usize,
+    cfg: &K::Cfg,
+    mut insert: F,
+    set_type: &'static str,
+) -> Result<(), Error>
+where
+    K: Read + Ord,
+    F: FnMut(K) -> bool,
+{
+    let mut last: Option<K> = None;
+    for _ in 0..len {
+        // Read item
+        let item = K::read_cfg(buf, cfg)?;
+
+        // Check if items are in ascending order
+        if let Some(ref last) = last {
+            match item.cmp(last) {
+                Ordering::Equal => return Err(Error::Invalid(set_type, "Duplicate item")),
+                Ordering::Less => return Err(Error::Invalid(set_type, "Items must ascend")),
+                _ => {}
+            }
+        }
+
+        // Add previous item, if exists
+        if let Some(last) = last.take() {
+            insert(last);
+        }
+        last = Some(item);
+    }
+
+    // Add last item, if exists
+    if let Some(last) = last {
+        insert(last);
+    }
+
+    Ok(())
+}
+
+// ---------- BTreeSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for BTreeSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Items are already sorted in BTreeSet, so we can iterate directly
+        for item in self {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for BTreeSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<K: Read + Clone + Ord + Hash + Eq> Read for BTreeSet<K> {
+    type Cfg = (RangeCfg, K::Cfg);
+
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &Self::Cfg) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = BTreeSet::new();
+
+        // Read items in ascending order
+        read_ordered_set(buf, len, cfg, |item| set.insert(item), BTREESET_TYPE)?;
+
+        Ok(set)
+    }
+}
+
+// ---------- HashSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for HashSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Sort the items to ensure deterministic encoding
+        let mut items: Vec<_> = self.iter().collect();
+        items.sort();
+        for item in items {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for HashSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+
+        // Note: Iteration order doesn't matter for size calculation.
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<K: Read + Clone + Ord + Hash + Eq> Read for HashSet<K> {
+    type Cfg = (RangeCfg, K::Cfg);
+
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &Self::Cfg) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = HashSet::with_capacity(len);
+
+        // Read items in ascending order
+        read_ordered_set(buf, len, cfg, |item| set.insert(item), HASHSET_TYPE)?;
+
+        Ok(set)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        codec::{Decode, Encode},
+        FixedSize,
+    };
+    use bytes::{Bytes, BytesMut};
+    use std::collections::{BTreeSet, HashSet};
+    use std::fmt::Debug;
+
+    // Generic round trip test function for BTreeSet
+    fn round_trip_btree<K>(set: &BTreeSet<K>, range_cfg: RangeCfg, item_cfg: K::Cfg)
+    where
+        K: Write + EncodeSize + Read + Clone + Ord + Hash + Eq + Debug + PartialEq,
+        BTreeSet<K>: Read<Cfg = (RangeCfg, K::Cfg)>
+            + Decode<Cfg = (RangeCfg, K::Cfg)>
+            + Debug
+            + PartialEq
+            + Write
+            + EncodeSize,
+    {
+        let encoded = set.encode();
+        assert_eq!(set.encode_size(), encoded.len());
+        let config_tuple = (range_cfg, item_cfg);
+        let decoded = BTreeSet::<K>::decode_cfg(encoded, &config_tuple).expect("decode_cfg failed");
+        assert_eq!(set, &decoded);
+    }
+
+    // Generic round trip test function for HashSet
+    fn round_trip_hash<K>(set: &HashSet<K>, range_cfg: RangeCfg, item_cfg: K::Cfg)
+    where
+        K: Write + EncodeSize + Read + Clone + Ord + Hash + Eq + Debug + PartialEq,
+        HashSet<K>: Read<Cfg = (RangeCfg, K::Cfg)>
+            + Decode<Cfg = (RangeCfg, K::Cfg)>
+            + Debug
+            + PartialEq
+            + Write
+            + EncodeSize,
+    {
+        let encoded = set.encode();
+        assert_eq!(set.encode_size(), encoded.len());
+        let config_tuple = (range_cfg, item_cfg);
+        let decoded = HashSet::<K>::decode_cfg(encoded, &config_tuple).expect("decode_cfg failed");
+        assert_eq!(set, &decoded);
+    }
+
+    // --- BTreeSet Tests ---
+
+    #[test]
+    fn test_empty_btreeset() {
+        let set = BTreeSet::<u32>::new();
+        round_trip_btree(&set, (..).into(), ());
+        assert_eq!(set.encode_size(), 1); // varint 0
+        let encoded = set.encode();
+        assert_eq!(encoded, Bytes::from_static(&[0]));
+    }
+
+    #[test]
+    fn test_simple_btreeset_u32() {
+        let mut set = BTreeSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+        set.insert(2u32);
+        round_trip_btree(&set, (..).into(), ());
+        assert_eq!(set.encode_size(), 1 + 3 * u32::SIZE);
+    }
+
+    #[test]
+    fn test_large_btreeset() {
+        // Fixed-size items
+        let set: BTreeSet<_> = (0..1000u16).collect();
+        round_trip_btree(&set, (1000..=1000).into(), ());
+
+        // Variable-size items
+        let set: BTreeSet<_> = (0..1000usize).collect();
+        round_trip_btree(&set, (1000..=1000).into(), (..=1000).into());
+    }
+
+    #[test]
+    fn test_btreeset_with_variable_items() {
+        let mut set = BTreeSet::new();
+        set.insert(Bytes::from_static(b"apple"));
+        set.insert(Bytes::from_static(b"banana"));
+        set.insert(Bytes::from_static(b"cherry"));
+
+        let set_range = 0..=10;
+        let item_range = ..=10; // Range for Bytes length
+
+        round_trip_btree(&set, set_range.into(), item_range.into());
+    }
+
+    #[test]
+    fn test_btreeset_decode_length_limit_exceeded() {
+        let mut set = BTreeSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+        let encoded = set.encode();
+
+        let config_tuple = ((0..=1).into(), ());
+        let result = BTreeSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::InvalidLength(2))));
+    }
+
+    #[test]
+    fn test_btreeset_decode_item_length_limit_exceeded() {
+        let mut set = BTreeSet::new();
+        set.insert(Bytes::from_static(b"longitem")); // 8 bytes
+        let encoded = set.encode();
+
+        let set_range = 0..=10;
+        let restrictive_item_range = ..=5; // Limit item length
+        let config_tuple = (set_range.into(), restrictive_item_range.into());
+        let result = BTreeSet::<Bytes>::decode_cfg(encoded, &config_tuple);
+
+        assert!(matches!(result, Err(Error::InvalidLength(8))));
+    }
+
+    #[test]
+    fn test_btreeset_decode_invalid_item_order() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Set length = 2
+        5u32.write(&mut encoded); // Item 5
+        2u32.write(&mut encoded); // Item 2 (out of order)
+
+        let config_tuple = ((..).into(), ());
+        let result = BTreeSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("BTreeSet", "Items must ascend")) // Note: Error message uses HashSet currently
+        ));
+    }
+
+    #[test]
+    fn test_btreeset_decode_duplicate_item() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Set length = 2
+        1u32.write(&mut encoded); // Item 1
+        1u32.write(&mut encoded); // Duplicate Item 1
+
+        let config_tuple = ((..).into(), ());
+        let result = BTreeSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("BTreeSet", "Duplicate item")) // Note: Error message uses HashSet currently
+        ));
+    }
+
+    #[test]
+    fn test_btreeset_decode_end_of_buffer() {
+        let mut set = BTreeSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+
+        let mut encoded = set.encode();
+        encoded.truncate(set.encode_size() - 2); // Truncate during last item
+
+        let config_tuple = ((..).into(), ());
+        let result = BTreeSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_btreeset_decode_extra_data() {
+        let mut set = BTreeSet::new();
+        set.insert(1u32);
+
+        let mut encoded = set.encode();
+        encoded.put_u8(0xFF); // Add extra byte
+
+        // Use decode_cfg which enforces buffer is fully consumed
+        let config_tuple = ((..).into(), ());
+        let result = BTreeSet::<u32>::decode_cfg(encoded.clone(), &config_tuple);
+        assert!(matches!(result, Err(Error::ExtraData(1))));
+
+        // Verify that read_cfg would succeed (doesn't check for extra data)
+        let read_result = BTreeSet::<u32>::read_cfg(&mut encoded.clone(), &config_tuple);
+        assert!(read_result.is_ok());
+        let decoded_set = read_result.unwrap();
+        assert_eq!(decoded_set.len(), 1);
+        assert!(decoded_set.contains(&1u32));
+    }
+
+    #[test]
+    fn test_btreeset_deterministic_encoding() {
+        let mut set1 = BTreeSet::new();
+        (0..1000u32).for_each(|i| {
+            set1.insert(i);
+        });
+
+        let mut set2 = BTreeSet::new();
+        (0..1000u32).rev().for_each(|i| {
+            set2.insert(i);
+        });
+
+        assert_eq!(set1.encode(), set2.encode());
+    }
+
+    #[test]
+    fn test_btreeset_conformity() {
+        // Case 1: Empty BTreeSet<u8>
+        let set1 = BTreeSet::<u8>::new();
+        let mut expected1 = BytesMut::new();
+        0usize.write(&mut expected1); // Length 0
+        assert_eq!(set1.encode(), expected1.freeze());
+        assert_eq!(set1.encode_size(), 1);
+
+        // Case 2: Simple BTreeSet<u8>
+        // BTreeSet will store and encode items in sorted order: 1, 2, 5
+        let mut set2 = BTreeSet::<u8>::new();
+        set2.insert(5u8);
+        set2.insert(1u8);
+        set2.insert(2u8);
+
+        let mut expected2 = BytesMut::new();
+        3usize.write(&mut expected2); // Length 3
+        1u8.write(&mut expected2); // Item 1
+        2u8.write(&mut expected2); // Item 2
+        5u8.write(&mut expected2); // Item 5
+        assert_eq!(set2.encode(), expected2.freeze());
+        assert_eq!(set2.encode_size(), 1 + 3 * u8::SIZE);
+
+        // Case 3: BTreeSet<Bytes>
+        // BTreeSet sorts items: "apple", "banana", "cherry"
+        let mut set3 = BTreeSet::<Bytes>::new();
+        set3.insert(Bytes::from_static(b"cherry"));
+        set3.insert(Bytes::from_static(b"apple"));
+        set3.insert(Bytes::from_static(b"banana"));
+
+        let mut expected3 = BytesMut::new();
+        3usize.write(&mut expected3); // Length 3
+        Bytes::from_static(b"apple").write(&mut expected3);
+        Bytes::from_static(b"banana").write(&mut expected3);
+        Bytes::from_static(b"cherry").write(&mut expected3);
+        assert_eq!(set3.encode(), expected3.freeze());
+        let expected_size = 1usize.encode_size()
+            + Bytes::from_static(b"apple").encode_size()
+            + Bytes::from_static(b"banana").encode_size()
+            + Bytes::from_static(b"cherry").encode_size();
+        assert_eq!(set3.encode_size(), expected_size);
+    }
+
+    // --- HashSet Tests ---
+
+    #[test]
+    fn test_empty_hashset() {
+        let set = HashSet::<u32>::new();
+        round_trip_hash(&set, (..).into(), ());
+        assert_eq!(set.encode_size(), 1); // varint 0
+        let encoded = set.encode();
+        assert_eq!(encoded, Bytes::from_static(&[0]));
+    }
+
+    #[test]
+    fn test_simple_hashset_u32() {
+        let mut set = HashSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+        set.insert(2u32);
+        round_trip_hash(&set, (..).into(), ());
+        // Size calculation: varint len + size of each item (order doesn't matter for size)
+        assert_eq!(set.encode_size(), 1 + 3 * u32::SIZE);
+        // Encoding check: items must be sorted (1, 2, 5)
+        let mut expected = BytesMut::new();
+        3usize.write(&mut expected); // Set length = 3
+        1u32.write(&mut expected);
+        2u32.write(&mut expected);
+        5u32.write(&mut expected);
+        assert_eq!(set.encode(), expected.freeze());
+    }
+
+    #[test]
+    fn test_large_hashset() {
+        // Fixed-size items
+        let set: HashSet<_> = (0..1000u16).collect();
+        round_trip_hash(&set, (1000..=1000).into(), ());
+
+        // Variable-size items
+        let set: HashSet<_> = (0..1000usize).collect();
+        round_trip_hash(&set, (1000..=1000).into(), (..=1000).into());
+    }
+
+    #[test]
+    fn test_hashset_with_variable_items() {
+        let mut set = HashSet::new();
+        set.insert(Bytes::from_static(b"apple"));
+        set.insert(Bytes::from_static(b"banana"));
+        set.insert(Bytes::from_static(b"cherry"));
+
+        let set_range = 0..=10;
+        let item_range = ..=10; // Range for Bytes length
+
+        round_trip_hash(&set, set_range.into(), item_range.into());
+    }
+
+    #[test]
+    fn test_hashset_decode_length_limit_exceeded() {
+        let mut set = HashSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+
+        let encoded = set.encode();
+        let config_tuple = ((0..=1).into(), ());
+
+        let result = HashSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::InvalidLength(2))));
+    }
+
+    #[test]
+    fn test_hashset_decode_item_length_limit_exceeded() {
+        let mut set = HashSet::new();
+        set.insert(Bytes::from_static(b"longitem")); // 8 bytes
+
+        let set_range = 0..=10;
+        let restrictive_item_range = ..=5; // Limit item length
+
+        let encoded = set.encode();
+        let config_tuple = (set_range.into(), restrictive_item_range.into());
+        let result = HashSet::<Bytes>::decode_cfg(encoded, &config_tuple);
+
+        assert!(matches!(result, Err(Error::InvalidLength(8))));
+    }
+
+    #[test]
+    fn test_hashset_decode_invalid_item_order() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Set length = 2
+        5u32.write(&mut encoded); // Item 5
+        2u32.write(&mut encoded); // Item 2 (out of order)
+
+        let config_tuple = ((..).into(), ());
+
+        let result = HashSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("HashSet", "Items must ascend"))
+        ));
+    }
+
+    #[test]
+    fn test_hashset_decode_duplicate_item() {
+        let mut encoded = BytesMut::new();
+        2usize.write(&mut encoded); // Set length = 2
+        1u32.write(&mut encoded); // Item 1
+        1u32.write(&mut encoded); // Duplicate Item 1
+
+        let config_tuple = ((..).into(), ());
+        let result = HashSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("HashSet", "Duplicate item"))
+        ));
+    }
+
+    #[test]
+    fn test_hashset_decode_end_of_buffer() {
+        let mut set = HashSet::new();
+        set.insert(1u32);
+        set.insert(5u32);
+
+        let mut encoded = set.encode(); // Will be sorted: [1, 5]
+        encoded.truncate(set.encode_size() - 2); // Truncate during last item (5)
+
+        let config_tuple = ((..).into(), ());
+        let result = HashSet::<u32>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_hashset_decode_extra_data() {
+        let mut set = HashSet::new();
+        set.insert(1u32);
+
+        let mut encoded = set.encode();
+        encoded.put_u8(0xFF); // Add extra byte
+
+        // Use decode_cfg which enforces buffer is fully consumed
+        let config_tuple = ((..).into(), ()); // Clone range for read_cfg later
+        let result = HashSet::<u32>::decode_cfg(encoded.clone(), &config_tuple);
+        assert!(matches!(result, Err(Error::ExtraData(1))));
+
+        // Verify that read_cfg would succeed (doesn't check for extra data)
+        let read_result = HashSet::<u32>::read_cfg(&mut encoded.clone(), &config_tuple);
+        assert!(read_result.is_ok());
+        let decoded_set = read_result.unwrap();
+        assert_eq!(decoded_set.len(), 1);
+        assert!(decoded_set.contains(&1u32));
+    }
+
+    #[test]
+    fn test_hashset_deterministic_encoding() {
+        let mut set1 = HashSet::new();
+        (0..1000u32).for_each(|i| {
+            set1.insert(i);
+        });
+
+        let mut set2 = HashSet::new();
+        (0..1000u32).rev().for_each(|i| {
+            set2.insert(i);
+        });
+
+        assert_eq!(set1.encode(), set2.encode());
+    }
+
+    #[test]
+    fn test_hashset_conformity() {
+        // Case 1: Empty HashSet<u8>
+        let set1 = HashSet::<u8>::new();
+        let mut expected1 = BytesMut::new();
+        0usize.write(&mut expected1); // Length 0
+        assert_eq!(set1.encode(), expected1.freeze());
+        assert_eq!(set1.encode_size(), 1);
+
+        // Case 2: Simple HashSet<u8>
+        // HashSet will sort items for encoding: 1, 2, 5
+        let mut set2 = HashSet::<u8>::new();
+        set2.insert(5u8);
+        set2.insert(1u8);
+        set2.insert(2u8);
+
+        let mut expected2 = BytesMut::new();
+        3usize.write(&mut expected2); // Length 3
+        1u8.write(&mut expected2); // Item 1
+        2u8.write(&mut expected2); // Item 2
+        5u8.write(&mut expected2); // Item 5
+        assert_eq!(set2.encode(), expected2.freeze());
+        assert_eq!(set2.encode_size(), 1 + 3 * u8::SIZE);
+
+        // Case 3: HashSet<Bytes>
+        // HashSet sorts items for encoding: "apple", "banana", "cherry"
+        let mut set3 = HashSet::<Bytes>::new();
+        set3.insert(Bytes::from_static(b"cherry"));
+        set3.insert(Bytes::from_static(b"apple"));
+        set3.insert(Bytes::from_static(b"banana"));
+
+        let mut expected3 = BytesMut::new();
+        3usize.write(&mut expected3); // Length 3
+        Bytes::from_static(b"apple").write(&mut expected3);
+        Bytes::from_static(b"banana").write(&mut expected3);
+        Bytes::from_static(b"cherry").write(&mut expected3);
+        assert_eq!(set3.encode(), expected3.freeze());
+        let expected_size = 1usize.encode_size()
+            + Bytes::from_static(b"apple").encode_size()
+            + Bytes::from_static(b"banana").encode_size()
+            + Bytes::from_static(b"cherry").encode_size();
+        assert_eq!(set3.encode_size(), expected_size);
+    }
+}


Reviews:
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T15:54:41Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T15:55:05Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T15:55:25Z
user: login: BrendanChou
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T18:25:28Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T19:41:36Z
user: login: patrick-ogrady
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T19:52:26Z

Review comments:
diff_hunk: @@ -0,0 +1,504 @@
+//! Codec implementations for various set types.
+//!
+//! For portability and consistency between architectures,
+//! the size of the set must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashSet},
+    hash::Hash,
+};
+
+// ---------- BTreeSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for BTreeSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Items are already sorted in BTreeSet, so we can iterate directly
+        for item in self {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for BTreeSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for BTreeSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = BTreeSet::new(); // BTreeSet does not have a capacity method
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("BTreeSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("BTreeSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
+            set.insert(item);
+        }
+
+        Ok(set)
+    }
+}
+
+// ---------- HashSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for HashSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Sort the items to ensure deterministic encoding
+        let mut items: Vec<_> = self.iter().collect();
+        items.sort();
+        for item in items {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for HashSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for HashSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = HashSet::with_capacity(len);
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("HashSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("HashSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
path: codec/src/types/set.rs
user: login: patrick-ogrady
body: I wonder if we can just add this after the final iteration to avoid the clone?
created_at: 2025-05-25T15:54:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,504 @@
+//! Codec implementations for various set types.
+//!
+//! For portability and consistency between architectures,
+//! the size of the set must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashSet},
+    hash::Hash,
+};
+
+// ---------- BTreeSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for BTreeSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Items are already sorted in BTreeSet, so we can iterate directly
+        for item in self {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for BTreeSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for BTreeSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = BTreeSet::new(); // BTreeSet does not have a capacity method
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("BTreeSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("BTreeSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
path: codec/src/types/set.rs
user: login: patrick-ogrady
body: Like below, I wonder if we can avoid the clone by adding last after the final iteration.
created_at: 2025-05-25T15:55:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,40 +9,112 @@ use crate::{
     Config, RangeConfig,
 };
 use bytes::{Buf, BufMut};
-use std::{collections::HashMap, hash::Hash};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeMap, HashMap},
+    hash::Hash,
+};
+
+// ---------- BTreeMap ----------
+
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for BTreeMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Keys are already sorted in BTreeMap, so we can iterate directly
+        for (k, v) in self {
+            k.write(buf);
+            v.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize, V: EncodeSize> EncodeSize for BTreeMap<K, V> {
+    fn encode_size(&self) -> usize {
+        // Start with the size of the length prefix
+        let mut size = self.len().encode_size();
+
+        // Add the encoded size of each key and value
+        for (k, v) in self {
+            size += k.encode_size();
+            size += v.encode_size();
+        }
+        size
+    }
+}
+
+impl<
+        R: RangeConfig,
+        KCfg: Config,
+        VCfg: Config,
+        K: Read<KCfg> + Clone + Ord + Hash + Eq,
+        V: Read<VCfg> + Clone,
+    > Read<(R, (KCfg, VCfg))> for BTreeMap<K, V>
+{
+    fn read_cfg(
+        buf: &mut impl Buf,
+        (range, (k_cfg, v_cfg)): &(R, (KCfg, VCfg)),
+    ) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut map = BTreeMap::new(); // BTreeMap does not have a capacity method
+
+        // Keep track of the last key read
+        let mut last_key: Option<K> = None;
+
+        // Read each key-value pair
+        for _ in 0..len {
+            let key = K::read_cfg(buf, k_cfg)?;
+
+            // Check if keys are in ascending order relative to the previous key
+            if let Some(ref last) = last_key {
+                match key.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("BTreeMap", "Duplicate key")),
+                    Ordering::Less => return Err(Error::Invalid("BTreeMap", "Keys must ascend")),
+                    _ => {}
+                }
+            }
+            last_key = Some(key.clone());
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: see note about adding after final iteration.
created_at: 2025-05-25T15:55:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,504 @@
+//! Codec implementations for various set types.
+//!
+//! For portability and consistency between architectures,
+//! the size of the set must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashSet},
+    hash::Hash,
+};
+
+// ---------- BTreeSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for BTreeSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Items are already sorted in BTreeSet, so we can iterate directly
+        for item in self {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for BTreeSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for BTreeSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = BTreeSet::new(); // BTreeSet does not have a capacity method
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("BTreeSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("BTreeSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
+            set.insert(item);
+        }
+
+        Ok(set)
+    }
+}
+
+// ---------- HashSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for HashSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Sort the items to ensure deterministic encoding
+        let mut items: Vec<_> = self.iter().collect();
+        items.sort();
+        for item in items {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for HashSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for HashSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = HashSet::with_capacity(len);
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("HashSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("HashSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
path: codec/src/types/set.rs
user: login: BrendanChou
body: Sorry didn't understand your meaning here. Did you mean that we should skip the clone on just the last iteration as an optimization (since we don't have to check `last` again)? Or something else
created_at: 2025-05-27T18:25:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,504 @@
+//! Codec implementations for various set types.
+//!
+//! For portability and consistency between architectures,
+//! the size of the set must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashSet},
+    hash::Hash,
+};
+
+// ---------- BTreeSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for BTreeSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Items are already sorted in BTreeSet, so we can iterate directly
+        for item in self {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for BTreeSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for BTreeSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = BTreeSet::new(); // BTreeSet does not have a capacity method
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("BTreeSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("BTreeSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
+            set.insert(item);
+        }
+
+        Ok(set)
+    }
+}
+
+// ---------- HashSet ----------
+
+impl<K: Ord + Hash + Eq + Write> Write for HashSet<K> {
+    fn write(&self, buf: &mut impl BufMut) {
+        self.len().write(buf);
+
+        // Sort the items to ensure deterministic encoding
+        let mut items: Vec<_> = self.iter().collect();
+        items.sort();
+        for item in items {
+            item.write(buf);
+        }
+    }
+}
+
+impl<K: Ord + Hash + Eq + EncodeSize> EncodeSize for HashSet<K> {
+    fn encode_size(&self) -> usize {
+        let mut size = self.len().encode_size();
+        for item in self {
+            size += item.encode_size();
+        }
+        size
+    }
+}
+
+impl<R: RangeConfig, Cfg: Config, K: Read<Cfg> + Clone + Ord + Hash + Eq> Read<(R, Cfg)>
+    for HashSet<K>
+{
+    fn read_cfg(buf: &mut impl Buf, (range, cfg): &(R, Cfg)) -> Result<Self, Error> {
+        // Read and validate the length prefix
+        let len = usize::read_cfg(buf, range)?;
+        let mut set = HashSet::with_capacity(len);
+
+        // Keep track of the last item read
+        let mut last: Option<K> = None;
+
+        // Read each item
+        for _ in 0..len {
+            let item = K::read_cfg(buf, cfg)?;
+
+            // Check if items are in ascending order
+            if let Some(ref last) = last {
+                match item.cmp(last) {
+                    Ordering::Equal => return Err(Error::Invalid("HashSet", "Duplicate item")),
+                    Ordering::Less => return Err(Error::Invalid("HashSet", "Items must ascend")),
+                    _ => {}
+                }
+            }
+            last = Some(item.clone());
path: codec/src/types/set.rs
user: login: patrick-ogrady
body: Maybe easier to understand in code: https://github.com/commonwarexyz/monorepo/pull/1008
created_at: 2025-05-28T19:41:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: patrick-ogrady
created_at: 2025-05-28T19:42:32Z
body: > My guess is that most keys will be `Copy` or cheap to clone, so the way the code is laid out is fine.
> 
> Other possible ways to implement this:
> 
> * I could require items in the set (or keys in the map) to be `Copy` instead
> * To avoid cloning at all, I could collect all items into a `Vec` first, making sure things are ordered, then putting each item into the map/set

Put up my suggestion of how to do this (with what I think is minimal effort): https://github.com/commonwarexyz/monorepo/pull/1008
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-28T20:55:33Z
body: ## [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.93%. Comparing base [(`2efe85f`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/2efe85f3fa9cd921d0f07a2bcb6beb689ae647c7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) to head [(`a937336`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/a937336813fd83bfc33e42bbc68dde2f8d70d7eb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> Report is 6 commits behind head on main.

```diff
@@            Coverage Diff             @@
##             main     #770      +/-   ##
==========================================
+ Coverage   90.79%   90.93%   +0.13%     
==========================================
  Files         190      191       +1     
  Lines       52630    53440     +810     
==========================================
+ Hits        47787    48597     +810     
  Misses       4843     4843              
```

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) | Coverage Δ | |
|---|---|---|
| [codec/src/types/map.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?src=pr&el=tree&filepath=codec%2Fsrc%2Ftypes%2Fmap.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-Y29kZWMvc3JjL3R5cGVzL21hcC5ycw==) | `100.00% <100.00%> (ø)` | |
| [codec/src/types/set.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?src=pr&el=tree&filepath=codec%2Fsrc%2Ftypes%2Fset.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-Y29kZWMvc3JjL3R5cGVzL3NldC5ycw==) | `100.00% <100.00%> (ø)` | |

... and [5 files with indirect coverage changes](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Last update [2efe85f...a937336](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/770?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE