Repository: commonwarexyz/monorepo
Pull request #711: [Codec] Add `HashMap` implementation
Author: login: BrendanChou
Created: 2025-04-11T19:15:46Z | Merged: 2025-04-11T21:44:37Z

Fixes #708 

Keys must be sorted (ascending). This provides deterministic guarantees around the encoding.

Linked issue #708: [codec] Add a deterministic `HashMap` implementation
Example code:

```
use crate::{varint, Config, Encode, Error, Read, Write};
use bytes::{Buf, BufMut};
use std::{collections::HashMap, hash::Hash, ops::RangeBounds};

impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
    fn write(&self, buf: &mut impl BufMut) {
        let len = u32::try_from(self.len()).unwrap();
        varint::write(len, 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);
        }
    }
}

impl<K: Ord + Hash + Eq + Write + Encode, V: Write + Encode> Encode for HashMap<K, V> {
    fn len_encoded(&self) -> usize {
        let len = u32::try_from(self.len()).unwrap();
        let mut len = varint::size(len);
        for (key, value) in self {
            len += key.len_encoded();
            len += value.len_encoded();
        }
        len
    }
}

impl<
        R: RangeBounds<usize> + Config,
        KCfg: Config,
        VCfg: Config,
        K: Read<KCfg> + Clone + Ord + Hash + Eq,
        V: Read<VCfg> + Clone,
    > Read<(R, KCfg, VCfg)> for HashMap<K, V>
{
    fn read_cfg(
        buf: &mut impl Buf,
        (range, k_cfg, v_cfg): (R, KCfg, VCfg),
    ) -> Result<Self, Error> {
        // Initialize the map with a capacity
        let len = usize::try_from(varint::read::<u32>(buf)?).unwrap();
        if !range.contains(&len) {
            return Err(Error::InvalidLength(len));
        }
        let mut map = HashMap::with_capacity(len);

        // Read the keys and values
        // Keys must be in ascending order for deterministic encoding
        let mut last_key: Option<K> = None;
        for _ in 0..len {
            let key = K::read_cfg(buf, k_cfg.clone())?;
            if let Some(ref last) = last_key {
                if key <= *last {
                    return Err(Error::Invalid("HashMap", "Keys must be in ascending order"));
                }
            }
            last_key = Some(key.clone());

            let value = V::read_cfg(buf, v_cfg.clone())?;
            map.insert(key, value);
        }
        Ok(map)
    }
}
```

Diff:
diff --git a/codec/src/extensions.rs b/codec/src/extensions.rs
index 2e48211223..9d46be5a9d 100644
--- a/codec/src/extensions.rs
+++ b/codec/src/extensions.rs
@@ -4,7 +4,7 @@
 //! `decode_range()`) that simplify common use cases of the core [`Read`] and [`Decode`] traits,
 //! particularly when default configurations (`()`) or [`RangeConfig`] are involved.
 
-use crate::{Decode, Error, RangeConfig, Read};
+use crate::{Config, Decode, Error, RangeConfig, Read};
 use bytes::Buf;
 
 /// Extension trait providing ergonomic read method for types requiring no configuration
@@ -35,31 +35,40 @@ pub trait DecodeExt: Decode<()> {
 // Automatically implement `DecodeExt` for types that implement `Decode` with no config.
 impl<T: Decode<()>> DecodeExt for T {}
 
-/// Extension trait for reading types whose config is `(RangeConfig, ())`,
-/// i.e., requiring a range but no specific inner configuration.
+/// Extension trait for reading types whose config is `(RangeConfig, T)` where `T` is a unit type
+/// (a type of one possible value, such as `()` or `((), ())`).
 ///
 /// Useful for reading collections like `Vec<T>` where `T` implements `Read<()>`.
 /// Import this trait to use the `.read_range()` method.
-pub trait ReadRangeExt<R: RangeConfig>: Read<(R, ())> {
-    /// Reads a value using only a range configuration, assuming the inner config is `()`.
+pub trait ReadRangeExt<T: Config + Default, R: RangeConfig>: Read<(R, T)> {
+    /// Reads a value using only a range configuration.
+    /// Assumes the inner configuration is a unit type.
     fn read_range(buf: &mut impl Buf, range: R) -> Result<Self, Error> {
-        Self::read_cfg(buf, &(range, ()))
+        Self::read_cfg(buf, &(range, T::default()))
     }
 }
 
-// Automatically implement `ReadRangeExt` for types that implement `Read` with a range and no config.
-impl<R: RangeConfig, T: Read<(R, ())>> ReadRangeExt<R> for T {}
+// Blanket implementation ONLY for T = ()
+// Applies to types like Vec<U> where U: Read<()>
+impl<R: RangeConfig, U: Read<(R, ())>> ReadRangeExt<(), R> for U {}
 
-/// Extension trait for decoding types whose config is `(RangeConfig, ())`,
+// Blanket implementation ONLY for T = ((), ())
+// Applies to types like HashMap<K, V> where K: Read<()>, V: Read<()>
+impl<R: RangeConfig, U: Read<(R, ((), ()))>> ReadRangeExt<((), ()), R> for U {}
+
+/// Extension trait for decoding types whose config is `(RangeConfig, T)`,
 /// i.e., requiring a range but no specific inner configuration, ensuring the buffer is consumed.
 ///
 /// Useful for decoding collections like `Vec<T>` where `T` implements `Read<()>`.
 /// Import this trait to use the `.decode_range()` method.
-pub trait DecodeRangeExt<R: RangeConfig>: Decode<(R, ())> {
+pub trait DecodeRangeExt<T: Config + Default, R: RangeConfig>: Decode<(R, T)> {
     fn decode_range(buf: impl Buf, range: R) -> Result<Self, Error> {
-        Self::decode_cfg(buf, &(range, ()))
+        Self::decode_cfg(buf, &(range, T::default()))
     }
 }
 
-// Automatically implement `DecodeRangeExt` for types that implement `Decode` with a range and no config.
-impl<R: RangeConfig, T: Decode<(R, ())>> DecodeRangeExt<R> for T {}
+// Blanket implementation ONLY for T = ()
+impl<R: RangeConfig, U: Decode<(R, ())>> DecodeRangeExt<(), R> for U {}
+
+// Blanket implementation ONLY for T = ((), ())
+impl<R: RangeConfig, U: Decode<(R, ((), ()))>> DecodeRangeExt<((), ()), R> for U {}
diff --git a/codec/src/types/map.rs b/codec/src/types/map.rs
new file mode 100644
index 0000000000..d5522a3666
--- /dev/null
+++ b/codec/src/types/map.rs
@@ -0,0 +1,295 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, 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);
+        }
+    }
+}
+
+// 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 varint size of the length
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        let mut size = varint::size(len);
+
+        // 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();
+        }
+        size
+    }
+}
+
+// Read implementation for HashMap
+impl<
+        R: RangeConfig,
+        KCfg: Config,
+        VCfg: Config,
+        K: Read<KCfg> + Clone + Ord + Hash + Eq,
+        V: Read<VCfg> + Clone,
+    > Read<(R, (KCfg, VCfg))> for HashMap<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 len32 = varint::read::<u32>(buf)?;
+        let len = usize::try_from(len32).map_err(|_| Error::InvalidVarint)?;
+        if !range.contains(&len) {
+            return Err(Error::InvalidLength(len));
+        }
+        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);
+        }
+
+        Ok(map)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        codec::{Decode, Encode, EncodeSize, FixedSize, Read, Write},
+        error::Error,
+        varint, Config, RangeConfig,
+    };
+    use bytes::{BufMut, Bytes, BytesMut};
+    use std::collections::HashMap;
+    use std::fmt::Debug;
+    use std::hash::Hash;
+    use std::ops::RangeInclusive;
+
+    // Manual round trip test function for non-default configs
+    fn round_trip<K, V, R, KCfg, VCfg>(map: &HashMap<K, V>, range_cfg: R, k_cfg: KCfg, v_cfg: VCfg)
+    where
+        K: Write + EncodeSize + Read<KCfg> + Clone + Ord + Hash + Eq + Debug + PartialEq,
+        V: Write + EncodeSize + Read<VCfg> + Clone + Debug + PartialEq,
+        R: RangeConfig + Clone,
+        KCfg: Config + Clone,
+        VCfg: Config + Clone,
+        HashMap<K, V>: Read<(R, (KCfg, VCfg))>
+            + Decode<(R, (KCfg, VCfg))>
+            + Debug
+            + 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");
+        assert_eq!(map, &decoded);
+    }
+
+    fn allow_any_len() -> RangeInclusive<usize> {
+        0..=usize::MAX
+    }
+
+    #[test]
+    fn test_empty_map() {
+        let map = HashMap::<u32, u64>::new();
+        round_trip(&map, allow_any_len(), (), ());
+        assert_eq!(map.encode_size(), 1);
+        let encoded = map.encode();
+        assert_eq!(encoded, Bytes::from_static(&[0])); // varint 0
+    }
+
+    #[test]
+    fn test_simple_map_u32_u64() {
+        let mut map = HashMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+        map.insert(2u32, 200u64);
+        round_trip(&map, allow_any_len(), (), ());
+        assert_eq!(map.encode_size(), 1 + 3 * (u32::SIZE + u64::SIZE));
+    }
+
+    #[test]
+    fn test_large_map() {
+        let mut map = HashMap::new();
+        for i in 0..1000 {
+            map.insert(i, i as u64 * 2);
+        }
+        round_trip(&map, 0..=1000, (), ());
+    }
+
+    #[test]
+    fn test_map_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]);
+        map.insert(Bytes::from_static(b"cherry"), vec![]);
+
+        let map_range = 0..=10;
+        let key_range = ..=10;
+        let val_range = 0..=100;
+
+        round_trip(&map, map_range, key_range, (val_range, ()));
+    }
+
+    #[test]
+    fn test_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;
+        let config_tuple = (restrictive_range, ((), ()));
+
+        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() {
+        let mut map = HashMap::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, (key_range, (restrictive_val_range, ())));
+        let result = HashMap::<Bytes, Vec<u8>>::decode_cfg(encoded, &config_tuple);
+
+        assert!(matches!(result, Err(Error::InvalidLength(5))));
+    }
+
+    #[test]
+    fn test_decode_invalid_key_order() {
+        let mut encoded = BytesMut::new();
+        varint::write(2u32, &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 = allow_any_len();
+        let config_tuple = (range, ((), ()));
+
+        let result = HashMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("HashMap", "Keys must ascend"))
+        ));
+    }
+
+    #[test]
+    fn test_decode_duplicate_key() {
+        let mut encoded = BytesMut::new();
+        varint::write(2u32, &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 = allow_any_len();
+        let config_tuple = (range, ((), ()));
+
+        let result = HashMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(
+            result,
+            Err(Error::Invalid("HashMap", "Duplicate key"))
+        ));
+    }
+
+    #[test]
+    fn test_decode_end_of_buffer_key() {
+        let mut map = HashMap::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
+
+        let range = allow_any_len();
+        let config_tuple = (range, ((), ()));
+        let result = HashMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_decode_end_of_buffer_value() {
+        let mut map = HashMap::new();
+        map.insert(1u32, 100u64);
+        map.insert(5u32, 500u64);
+
+        let mut encoded = map.encode();
+        encoded.truncate(map.encode_size() - 4); // Truncate during last value
+
+        let range = allow_any_len();
+        let config_tuple = (range, ((), ()));
+        let result = HashMap::<u32, u64>::decode_cfg(encoded, &config_tuple);
+        assert!(matches!(result, Err(Error::EndOfBuffer)));
+    }
+
+    #[test]
+    fn test_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 = allow_any_len();
+        let config_tuple = (range.clone(), ((), ())); // Clone range for read_cfg later
+
+        // Use decode_cfg which enforces buffer is fully consumed
+        let result = HashMap::<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 = HashMap::<u32, u64>::read_cfg(&mut encoded, &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));
+    }
+}
diff --git a/codec/src/types/mod.rs b/codec/src/types/mod.rs
index fcfab46991..77adf04849 100644
--- a/codec/src/types/mod.rs
+++ b/codec/src/types/mod.rs
@@ -1,6 +1,7 @@
 //! Codec implementations for common types
 
 pub mod bytes;
+pub mod map;
 pub mod net;
 pub mod primitives;
 pub mod tuple;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T19:34:40Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T19:40:03Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T19:40:20Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T19:42:14Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T19:42:28Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T20:59:37Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T21:00:22Z
user: login: patrick-ogrady
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T21:44:28Z

Review comments:
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, buf);
+
+        // Sort the keys to ensure deterministic encoding
+        let mut keys: Vec<_> = self.keys().collect();
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: It may be faster to convert the whole map to a vec of `(key, value)` and `sort_by(key)`?
created_at: 2025-04-11T19:40:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, 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);
+        }
+    }
+}
+
+// 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 varint size of the length
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        let mut size = varint::size(len);
+
+        // 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();
+        }
+        size
+    }
+}
+
+// Read implementation for HashMap
+impl<
+        R: RangeConfig,
+        KCfg: Config,
+        VCfg: Config,
+        K: Read<KCfg> + Clone + Ord + Hash + Eq,
+        V: Read<VCfg> + Clone,
+    > Read<(R, (KCfg, VCfg))> for HashMap<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 len32 = varint::read::<u32>(buf)?;
+        let len = usize::try_from(len32).map_err(|_| Error::InvalidVarint)?;
+        if !range.contains(&len) {
+            return Err(Error::InvalidLength(len));
+        }
+        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
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: ❤️ 
created_at: 2025-04-11T19:40:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, buf);
+
+        // Sort the keys to ensure deterministic encoding
+        let mut keys: Vec<_> = self.keys().collect();
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: ...actually, not sure. That may result in moving a lot more memory around.
created_at: 2025-04-11T19:42:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, buf);
+
+        // Sort the keys to ensure deterministic encoding
+        let mut keys: Vec<_> = self.keys().collect();
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: disregard.
created_at: 2025-04-11T19:42:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, 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);
+        }
+    }
+}
+
+// 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 varint size of the length
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        let mut size = varint::size(len);
+
+        // 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();
+        }
+        size
+    }
+}
+
+// Read implementation for HashMap
+impl<
+        R: RangeConfig,
+        KCfg: Config,
+        VCfg: Config,
+        K: Read<KCfg> + Clone + Ord + Hash + Eq,
+        V: Read<VCfg> + Clone,
+    > Read<(R, (KCfg, VCfg))> for HashMap<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 len32 = varint::read::<u32>(buf)?;
+        let len = usize::try_from(len32).map_err(|_| Error::InvalidVarint)?;
+        if !range.contains(&len) {
+            return Err(Error::InvalidLength(len));
+        }
+        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 {
+                if key <= *last {
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: Do we want to return a different error for duplicate keys vs non-ascending keys?
created_at: 2025-04-11T20:59:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,270 @@
+//! Codec implementations for HashMap.
+//!
+//! For portability and consistency between architectures,
+//! the size of the map must fit within a [`u32`].
+
+use crate::{
+    codec::{EncodeSize, Read, Write},
+    error::Error,
+    varint, Config, RangeConfig,
+};
+use bytes::{Buf, BufMut};
+use std::{collections::HashMap, hash::Hash};
+
+// Write implementation for HashMap
+impl<K: Ord + Hash + Eq + Write, V: Write> Write for HashMap<K, V> {
+    fn write(&self, buf: &mut impl BufMut) {
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        varint::write(len, 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);
+        }
+    }
+}
+
+// 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 varint size of the length
+        let len = u32::try_from(self.len()).expect("HashMap length exceeds u32::MAX");
+        let mut size = varint::size(len);
+
+        // 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();
+        }
+        size
+    }
+}
+
+// Read implementation for HashMap
+impl<
+        R: RangeConfig,
+        KCfg: Config,
+        VCfg: Config,
+        K: Read<KCfg> + Clone + Ord + Hash + Eq,
+        V: Read<VCfg> + Clone,
+    > Read<(R, (KCfg, VCfg))> for HashMap<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 len32 = varint::read::<u32>(buf)?;
+        let len = usize::try_from(len32).map_err(|_| Error::InvalidVarint)?;
+        if !range.contains(&len) {
+            return Err(Error::InvalidLength(len));
+        }
+        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 {
+                if key <= *last {
path: codec/src/types/map.rs
user: login: patrick-ogrady
body: If not, I suggest modifying the error to make it clearer that it isn't quite ascending
created_at: 2025-04-11T21:00:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-11T21:45:06Z
body: ## [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711?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 88.61%. Comparing base [(`e44902f`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/e44902fe1e2e850716fce0c62a073141be829255?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) to head [(`cc7d50e`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/cc7d50e04dd1a19dcdfb9c06c5b6e7548a53959d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> Report is 1 commits behind head on main.

```diff
@@            Coverage Diff             @@
##             main     #711      +/-   ##
==========================================
+ Coverage   88.55%   88.61%   +0.05%     
==========================================
  Files         160      161       +1     
  Lines       38654    38864     +210     
==========================================
+ Hits        34230    34438     +208     
- Misses       4424     4426       +2     
```

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711?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/extensions.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711?src=pr&el=tree&filepath=codec%2Fsrc%2Fextensions.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-Y29kZWMvc3JjL2V4dGVuc2lvbnMucnM=) | `100.00% <100.00%> (ø)` | |
| [codec/src/types/map.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711?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%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711/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/711?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/711?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Last update [e44902f...cc7d50e](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/711?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