ethportal_api/utils/
bytes.rs

1use hex::FromHexError;
2use rand::{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    rand::thread_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        rand::thread_rng().gen_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        rand::thread_rng()
79            .gen_range(min_nonzero_byte_value..min_nonzero_byte_value.saturating_mul(2))
80    };
81
82    bytes
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used)]
87mod test {
88    use super::*;
89
90    #[test]
91    fn test_hex_encode() {
92        let to_encode = vec![176, 15];
93        let encoded = hex_encode(to_encode);
94        assert_eq!(encoded, "0xb00f");
95    }
96
97    #[test]
98    fn test_hex_decode() {
99        let to_decode = "0xb00f";
100        let decoded = hex_decode(to_decode).unwrap();
101        assert_eq!(decoded, vec![176, 15]);
102    }
103
104    #[test]
105    fn test_hex_decode_invalid_start() {
106        let to_decode = "b00f";
107        let result = hex_decode(to_decode);
108        assert!(result.is_err());
109        let error = result.unwrap_err();
110        assert_eq!(
111            error.to_string(),
112            "Hex string starts with b0, expected 0x".to_string()
113        );
114        assert_eq!(
115            error,
116            ByteUtilsError::WrongPrefix {
117                first_two: "b0".to_string()
118            }
119        );
120    }
121
122    #[test]
123    fn test_hex_decode_invalid_char() {
124        let to_decode = "0xb00g";
125        let result = hex_decode(to_decode);
126        assert!(result.is_err());
127        let error = result.unwrap_err();
128        assert_eq!(
129            error.to_string(),
130            "Unable to decode hex string 0xb00g due to Invalid character 'g' at position 3"
131                .to_string()
132        );
133        assert_eq!(
134            error,
135            ByteUtilsError::HexDecode {
136                source: FromHexError::InvalidHexCharacter { c: 'g', index: 3 },
137                data: "0xb00g".to_string()
138            }
139        );
140    }
141
142    #[test]
143    fn test_hex_decode_empty_string() {
144        let to_decode = "";
145        let result = hex_decode(to_decode);
146        assert!(result.is_err());
147        let error = result.unwrap_err();
148        assert_eq!(
149            error.to_string(),
150            "Hex string is '', expected to start with 0x".to_string()
151        );
152        assert_eq!(
153            error,
154            ByteUtilsError::NoPrefix {
155                data: "".to_string()
156            }
157        );
158    }
159
160    #[test]
161    fn test_hex_decode_no_prefix() {
162        let to_decode = "0";
163        let result = hex_decode(to_decode);
164        assert!(result.is_err());
165        let error = result.unwrap_err();
166        assert_eq!(
167            error.to_string(),
168            "Hex string is '0', expected to start with 0x".to_string()
169        );
170        assert_eq!(
171            error,
172            ByteUtilsError::NoPrefix {
173                data: "0".to_string()
174            }
175        );
176    }
177
178    #[test]
179    fn test_hex_decode_prefix_only_returns_empty_byte_vector() {
180        let to_decode = "0x";
181        let result = hex_decode(to_decode).unwrap();
182        assert_eq!(result, vec![] as Vec<u8>);
183        // Confirm this matches behaviour of hex crate.
184        assert_eq!(hex::decode("").unwrap(), vec![] as Vec<u8>);
185    }
186
187    #[test]
188    fn test_hex_decode_odd_count() {
189        let to_decode = "0x0";
190        let result = hex_decode(to_decode);
191        assert!(result.is_err());
192        let error = result.unwrap_err();
193        assert_eq!(
194            error.to_string(),
195            "Unable to decode hex string 0x0 due to Odd number of digits".to_string()
196        );
197        assert_eq!(
198            error,
199            ByteUtilsError::HexDecode {
200                source: FromHexError::OddLength,
201                data: "0x0".to_string()
202            }
203        );
204    }
205
206    #[test]
207    fn test_random_32byte_array_1() {
208        let bytes = random_32byte_array(17);
209
210        assert_eq!(bytes.len(), 32);
211        assert_eq!(bytes[0..2], vec![0, 0]);
212        assert!((bytes[2] >= 64) && (bytes[2] < 128));
213    }
214
215    #[test]
216    fn test_random_32byte_array_2() {
217        let bytes = random_32byte_array(16);
218
219        assert_eq!(bytes.len(), 32);
220        assert_eq!(bytes[0..2], vec![0, 0]);
221        assert!(bytes[2] >= 128);
222    }
223
224    #[test]
225    fn test_random_32byte_array_3() {
226        let bytes = random_32byte_array(15);
227
228        assert_eq!(bytes.len(), 32);
229        assert_eq!(bytes[0], 0);
230        assert_eq!(bytes[1], 1);
231    }
232}