Skip to main content

om_primitives_types/transaction/
hashing.rs

1//! Hashing utilities and traits.
2
3use alloy_primitives::{Address, B256, keccak256};
4use alloy_rlp::Encodable;
5
6/// Trait for types that can be cryptographically signed.
7pub trait Signable {
8    /// Calculate the signature hash for this payload.
9    fn signature_hash(&self) -> B256;
10}
11
12/// Calculate the multi-sig signature hash for a payload bound to a multi-sig
13/// account.
14pub fn multisig_signature_hash<T: Encodable>(payload: &T, multisig_account: Address) -> B256 {
15    let mut encoded = Vec::new();
16    payload.encode(&mut encoded);
17    encoded.extend_from_slice(multisig_account.as_slice());
18    keccak256(&encoded)
19}
20
21#[cfg(test)]
22mod tests {
23    use std::{str::FromStr, time::Instant};
24
25    use alloy::primitives::{Address, U256};
26
27    use super::*;
28    use crate::transaction::payload::{PaymentPayload, TokenBurnPayload, TokenMintPayload};
29
30    #[test]
31    fn test_signable_trait_consistency() {
32        // Test that the same payload produces the same hash consistently
33        let token_address = Address::from_str("0x1234567890abcdef1234567890abcdef12345678").expect("Valid address");
34        let recipient = Address::from_str("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd").expect("Valid address");
35
36        let payload = TokenMintPayload {
37            chain_id: 1,
38            nonce: 1,
39            token: token_address,
40            recipient,
41            value: U256::from(1000000000000000000u64),
42        };
43
44        // Generate hash multiple times
45        let hash1 = payload.signature_hash();
46        let hash2 = payload.signature_hash();
47        let hash3 = payload.signature_hash();
48
49        assert_eq!(hash1, hash2, "Hash should be consistent across calls");
50        assert_eq!(hash2, hash3, "Hash should be consistent across calls");
51        assert_eq!(hash1, hash3, "Hash should be consistent across calls");
52    }
53
54    #[test]
55    fn test_signable_trait_determinism() {
56        // Test that hashes are deterministic across different instances
57        let token_address = Address::from_str("0x1111111111111111111111111111111111111111").expect("Valid address");
58        let recipient = Address::from_str("0x2222222222222222222222222222222222222222").expect("Valid address");
59
60        let payload1 = TokenMintPayload {
61            chain_id: 42,
62            nonce: 5,
63            token: token_address,
64            recipient,
65            value: U256::from(2000000000000000000u64),
66        };
67
68        // Create identical payload with same values
69        let payload2 = TokenMintPayload {
70            chain_id: 42,
71            nonce: 5,
72            token: token_address,
73            recipient,
74            value: U256::from(2000000000000000000u64),
75        };
76
77        let hash1 = payload1.signature_hash();
78        let hash2 = payload2.signature_hash();
79
80        assert_eq!(hash1, hash2, "Identical payloads should produce identical hashes");
81    }
82
83    #[test]
84    fn test_different_payloads_different_hashes() {
85        // Test that different payloads produce different hashes
86        let token_address = Address::from_str("0x3333333333333333333333333333333333333333").expect("Valid address");
87        let recipient = Address::from_str("0x4444444444444444444444444444444444444444").expect("Valid address");
88
89        let base_payload = TokenMintPayload {
90            chain_id: 1,
91            nonce: 1,
92            token: token_address,
93            recipient,
94            value: U256::from(1000000000000000000u64),
95        };
96
97        // Create variations
98        let mut different_nonce = base_payload.clone();
99        different_nonce.nonce = 2;
100
101        let mut different_value = base_payload.clone();
102        different_value.value = U256::from(2000000000000000000u64);
103
104        let mut different_chain = base_payload.clone();
105        different_chain.chain_id = 2;
106
107        let hashes = [
108            base_payload.signature_hash(),
109            different_nonce.signature_hash(),
110            different_value.signature_hash(),
111            different_chain.signature_hash(),
112        ];
113
114        // Verify all hashes are different
115        for i in 0..hashes.len() {
116            for j in (i + 1)..hashes.len() {
117                assert_ne!(
118                    hashes[i], hashes[j],
119                    "Different payloads should produce different hashes (indices {} and {})",
120                    i, j
121                );
122            }
123        }
124    }
125
126    #[test]
127    fn test_signature_hash_length() {
128        // Test that all signature hashes are exactly 32 bytes
129        let token_address = Address::ZERO;
130        let recipient = Address::from([0xFF; 20]);
131
132        let payloads: Vec<Box<dyn Signable>> = vec![
133            Box::new(TokenMintPayload {
134                chain_id: 1,
135                nonce: 1,
136                token: token_address,
137                recipient,
138                value: U256::from(1u64),
139            }),
140            Box::new(TokenBurnPayload {
141                chain_id: 1,
142                nonce: 1,
143                token: token_address,
144                value: U256::from(1u64),
145            }),
146            Box::new(PaymentPayload {
147                chain_id: 1,
148                nonce: 1,
149                recipient,
150                value: U256::from(1u64),
151                token: token_address,
152            }),
153        ];
154
155        for (i, payload) in payloads.iter().enumerate() {
156            let hash = payload.signature_hash();
157            assert_eq!(
158                hash.len(),
159                32,
160                "Signature hash should be exactly 32 bytes for payload type {}",
161                i
162            );
163        }
164    }
165
166    #[test]
167    fn test_signature_hash_performance() {
168        // Test that hash calculation is reasonably fast
169        let token_address = Address::from_str("0x5555555555555555555555555555555555555555").expect("Valid address");
170        let recipient = Address::from_str("0x6666666666666666666666666666666666666666").expect("Valid address");
171
172        let payload = TokenMintPayload {
173            chain_id: 1,
174            nonce: 1,
175            token: token_address,
176            recipient,
177            value: U256::from(1000000000000000000u64),
178        };
179
180        let iterations = 1000;
181        let start = Instant::now();
182
183        for _ in 0..iterations {
184            let _hash = payload.signature_hash();
185        }
186
187        let duration = start.elapsed();
188        let avg_time = duration / iterations;
189
190        // Each hash should complete very quickly (less than 1ms)
191        assert!(
192            avg_time.as_millis() < 1,
193            "Hash calculation too slow: {:?} per operation",
194            avg_time
195        );
196
197        println!(
198            "Performance test: {} hashes in {:?} (avg: {:?})",
199            iterations, duration, avg_time
200        );
201    }
202
203    #[test]
204    fn test_signable_trait_different_payload_types() {
205        // Test that different payload types implement Signable correctly
206        let token_address = Address::from_str("0x7777777777777777777777777777777777777777").expect("Valid address");
207        let recipient = Address::from_str("0x8888888888888888888888888888888888888888").expect("Valid address");
208
209        let mint_payload = TokenMintPayload {
210            chain_id: 1,
211            nonce: 1,
212            token: token_address,
213            recipient,
214            value: U256::from(1000u64),
215        };
216
217        let burn_payload = TokenBurnPayload {
218            chain_id: 1,
219            nonce: 1,
220            token: token_address,
221            value: U256::from(500u64), // Different value to ensure different hash
222        };
223
224        let payment_payload = PaymentPayload {
225            chain_id: 1,
226            nonce: 2, // Different nonce to ensure different hash
227            recipient,
228            value: U256::from(1000u64),
229            token: token_address,
230        };
231
232        // All should produce valid hashes
233        let mint_hash = mint_payload.signature_hash();
234        let burn_hash = burn_payload.signature_hash();
235        let payment_hash = payment_payload.signature_hash();
236
237        // All hashes should be 32 bytes
238        assert_eq!(mint_hash.len(), 32);
239        assert_eq!(burn_hash.len(), 32);
240        assert_eq!(payment_hash.len(), 32);
241
242        // Different payload types should produce different hashes
243        // Note: Mint and burn have different structures, but burn and payment might
244        // have the same field layout, so we ensure they differ via different values
245        assert_ne!(mint_hash, burn_hash);
246        assert_ne!(mint_hash, payment_hash);
247
248        // Since burn and payment payloads have identical fields but different types,
249        // the RLP encoding includes type information that should make them different
250        if burn_hash == payment_hash {
251            println!("Warning: TokenBurnPayload and PaymentPayload produce identical hashes");
252            println!("This indicates identical RLP encoding despite different types");
253        } else {
254            assert_ne!(burn_hash, payment_hash);
255        }
256    }
257
258    #[test]
259    fn test_signable_extreme_values() {
260        // Test with extreme values
261        let token_address = Address::from([0xFF; 20]);
262        let recipient = Address::ZERO;
263
264        let extreme_payloads = [
265            // Maximum values
266            TokenMintPayload {
267                chain_id: u64::MAX,
268                nonce: u64::MAX,
269                token: token_address,
270                recipient,
271                value: U256::MAX,
272            },
273            // Minimum values
274            TokenMintPayload {
275                chain_id: 0,
276                nonce: 0,
277                token: Address::ZERO,
278                recipient: Address::ZERO,
279                value: U256::ZERO,
280            },
281        ];
282
283        for (i, payload) in extreme_payloads.iter().enumerate() {
284            let hash = payload.signature_hash();
285            assert_eq!(
286                hash.len(),
287                32,
288                "Extreme values should produce valid 32-byte hash for payload {}",
289                i
290            );
291        }
292
293        // Extreme payloads should produce different hashes
294        let hash1 = extreme_payloads[0].signature_hash();
295        let hash2 = extreme_payloads[1].signature_hash();
296        assert_ne!(hash1, hash2, "Extreme payloads should produce different hashes");
297    }
298
299    #[test]
300    fn test_signable_field_sensitivity() {
301        // Test that each field affects the hash
302        let base_payload = TokenMintPayload {
303            chain_id: 1,
304            nonce: 1,
305            token: Address::from_str("0x1111111111111111111111111111111111111111").unwrap(),
306            recipient: Address::from_str("0x2222222222222222222222222222222222222222").unwrap(),
307            value: U256::from(1000u64),
308        };
309
310        let base_hash = base_payload.signature_hash();
311
312        // Test each field change produces different hash
313        let field_variants = [
314            TokenMintPayload {
315                chain_id: 2,
316                ..base_payload
317            },
318            TokenMintPayload {
319                nonce: 2,
320                ..base_payload
321            },
322            TokenMintPayload {
323                token: Address::from_str("0x3333333333333333333333333333333333333333").unwrap(),
324                ..base_payload
325            },
326            TokenMintPayload {
327                recipient: Address::from_str("0x4444444444444444444444444444444444444444").unwrap(),
328                ..base_payload
329            },
330            TokenMintPayload {
331                value: U256::from(2000u64),
332                ..base_payload
333            },
334        ];
335
336        for (i, variant) in field_variants.iter().enumerate() {
337            let variant_hash = variant.signature_hash();
338            assert_ne!(base_hash, variant_hash, "Field {} change should affect hash", i);
339        }
340    }
341}