ethportal_api/utils/
bytes.rs

1use hex::FromHexError;
2use rand::{rng, Rng, RngCore};
3use thiserror::Error;
4
5/// An error from a byte utils operation.
6#[derive(Clone, Debug, Error, PartialEq)]
7pub enum ByteUtilsError {
8    #[error("Hex string starts with {first_two}, expected 0x")]
9    WrongPrefix { first_two: String },
10
11    #[error("Unable to decode hex string {data} due to {source}")]
12    HexDecode { source: FromHexError, data: String },
13
14    #[error("Hex string is '{data}', expected to start with 0x")]
15    NoPrefix { data: String },
16}
17
18/// Encode hex with 0x prefix
19pub fn hex_encode<T: AsRef<[u8]>>(data: T) -> String {
20    format!("0x{}", hex::encode(data))
21}
22
23/// Decode hex with 0x prefix
24pub fn hex_decode(data: &str) -> Result<Vec<u8>, ByteUtilsError> {
25    let first_two = data.get(..2).ok_or_else(|| ByteUtilsError::NoPrefix {
26        data: data.to_string(),
27    })?;
28
29    if first_two.to_lowercase() != "0x" {
30        return Err(ByteUtilsError::WrongPrefix {
31            first_two: first_two.to_string(),
32        });
33    }
34
35    let post_prefix = data.get(2..).unwrap_or("");
36
37    hex::decode(post_prefix).map_err(|e| ByteUtilsError::HexDecode {
38        source: e,
39        data: data.to_string(),
40    })
41}
42
43/// Returns a compact hex-encoded `String` representation of `data`.
44pub fn hex_encode_compact<T: AsRef<[u8]>>(data: T) -> String {
45    if data.as_ref().len() <= 8 {
46        hex_encode(data)
47    } else {
48        let hex = hex::encode(data);
49        format!("0x{}..{}", &hex[0..4], &hex[hex.len() - 4..])
50    }
51}
52
53/// Returns a upper-case, 0x-prefixed, hex-encoded `String` representation of `data`.
54pub fn hex_encode_upper<T: AsRef<[u8]>>(data: T) -> String {
55    format!("0x{}", hex::encode_upper(data))
56}
57
58/// Generate 32 byte array with N leading bit zeros
59pub fn random_32byte_array(leading_bit_zeros: u8) -> [u8; 32] {
60    let first_zero_bytes: usize = leading_bit_zeros as usize / 8;
61    let first_nonzero_byte_leading_zeros = leading_bit_zeros % 8u8;
62
63    let mut bytes = [0; 32];
64    rng().fill_bytes(&mut bytes[first_zero_bytes..]);
65
66    if first_zero_bytes == 32 {
67        return bytes;
68    }
69
70    bytes[first_zero_bytes] = if first_nonzero_byte_leading_zeros == 0 {
71        // We want the byte after first zero bytes to start with 1 bit, i.e value > 128
72        rng().random_range(128..=255)
73    } else {
74        // Based on the leading zeroes in this byte, we want to generate a random value within
75        // min and max u8 range
76        let min_nonzero_byte_value =
77            (128_f32 * 0.5_f32.powi(first_nonzero_byte_leading_zeros as i32)) as u8;
78        rng().random_range(min_nonzero_byte_value..min_nonzero_byte_value.saturating_mul(2))
79    };
80
81    bytes
82}
83
84#[cfg(test)]
85#[allow(clippy::unwrap_used)]
86mod test {
87    use super::*;
88
89    #[test]
90    fn test_hex_encode() {
91        let to_encode = vec![176, 15];
92        let encoded = hex_encode(to_encode);
93        assert_eq!(encoded, "0xb00f");
94    }
95
96    #[test]
97    fn test_hex_decode() {
98        let to_decode = "0xb00f";
99        let decoded = hex_decode(to_decode).unwrap();
100        assert_eq!(decoded, vec![176, 15]);
101    }
102
103    #[test]
104    fn test_hex_decode_invalid_start() {
105        let to_decode = "b00f";
106        let result = hex_decode(to_decode);
107        assert!(result.is_err());
108        let error = result.unwrap_err();
109        assert_eq!(
110            error.to_string(),
111            "Hex string starts with b0, expected 0x".to_string()
112        );
113        assert_eq!(
114            error,
115            ByteUtilsError::WrongPrefix {
116                first_two: "b0".to_string()
117            }
118        );
119    }
120
121    #[test]
122    fn test_hex_decode_invalid_char() {
123        let to_decode = "0xb00g";
124        let result = hex_decode(to_decode);
125        assert!(result.is_err());
126        let error = result.unwrap_err();
127        assert_eq!(
128            error.to_string(),
129            "Unable to decode hex string 0xb00g due to Invalid character 'g' at position 3"
130                .to_string()
131        );
132        assert_eq!(
133            error,
134            ByteUtilsError::HexDecode {
135                source: FromHexError::InvalidHexCharacter { c: 'g', index: 3 },
136                data: "0xb00g".to_string()
137            }
138        );
139    }
140
141    #[test]
142    fn test_hex_decode_empty_string() {
143        let to_decode = "";
144        let result = hex_decode(to_decode);
145        assert!(result.is_err());
146        let error = result.unwrap_err();
147        assert_eq!(
148            error.to_string(),
149            "Hex string is '', expected to start with 0x".to_string()
150        );
151        assert_eq!(
152            error,
153            ByteUtilsError::NoPrefix {
154                data: "".to_string()
155            }
156        );
157    }
158
159    #[test]
160    fn test_hex_decode_no_prefix() {
161        let to_decode = "0";
162        let result = hex_decode(to_decode);
163        assert!(result.is_err());
164        let error = result.unwrap_err();
165        assert_eq!(
166            error.to_string(),
167            "Hex string is '0', expected to start with 0x".to_string()
168        );
169        assert_eq!(
170            error,
171            ByteUtilsError::NoPrefix {
172                data: "0".to_string()
173            }
174        );
175    }
176
177    #[test]
178    fn test_hex_decode_prefix_only_returns_empty_byte_vector() {
179        let to_decode = "0x";
180        let result = hex_decode(to_decode).unwrap();
181        assert_eq!(result, vec![] as Vec<u8>);
182        // Confirm this matches behaviour of hex crate.
183        assert_eq!(hex::decode("").unwrap(), vec![] as Vec<u8>);
184    }
185
186    #[test]
187    fn test_hex_decode_odd_count() {
188        let to_decode = "0x0";
189        let result = hex_decode(to_decode);
190        assert!(result.is_err());
191        let error = result.unwrap_err();
192        assert_eq!(
193            error.to_string(),
194            "Unable to decode hex string 0x0 due to Odd number of digits".to_string()
195        );
196        assert_eq!(
197            error,
198            ByteUtilsError::HexDecode {
199                source: FromHexError::OddLength,
200                data: "0x0".to_string()
201            }
202        );
203    }
204
205    #[test]
206    fn test_random_32byte_array_1() {
207        let bytes = random_32byte_array(17);
208
209        assert_eq!(bytes.len(), 32);
210        assert_eq!(bytes[0..2], vec![0, 0]);
211        assert!((bytes[2] >= 64) && (bytes[2] < 128));
212    }
213
214    #[test]
215    fn test_random_32byte_array_2() {
216        let bytes = random_32byte_array(16);
217
218        assert_eq!(bytes.len(), 32);
219        assert_eq!(bytes[0..2], vec![0, 0]);
220        assert!(bytes[2] >= 128);
221    }
222
223    #[test]
224    fn test_random_32byte_array_3() {
225        let bytes = random_32byte_array(15);
226
227        assert_eq!(bytes.len(), 32);
228        assert_eq!(bytes[0], 0);
229        assert_eq!(bytes[1], 1);
230    }
231}