@@ -9,7 +9,7 @@ use super::{
99 constants:: * ,
1010 types:: { DepositMessage , ForkData , SigningData } ,
1111} ;
12- use crate :: network;
12+ use crate :: { helpers , network} ;
1313
1414use super :: constants:: { Domain , Root , Version } ;
1515
@@ -28,35 +28,34 @@ pub enum DomainError {
2828 #[ error( "Network error: {0}" ) ]
2929 NetworkError ( #[ from] network:: NetworkError ) ,
3030
31- /// Invalid address checksum
32- #[ error( "Invalid address checksum : {0}" ) ]
33- InvalidChecksum ( String ) ,
31+ /// Helper error
32+ #[ error( "Address validation error : {0}" ) ]
33+ HelperError ( # [ from ] helpers :: HelperError ) ,
3434}
3535
3636/// Converts an Ethereum address to withdrawal credentials.
3737///
3838/// # Arguments
39- /// * `addr` - Ethereum address ( with or without 0x prefix)
39+ /// * `addr` - Ethereum address with 0x prefix (format validation only, checksum not enforced )
4040/// * `compounding` - Whether to use EIP-7251 compounding withdrawal credentials
4141///
4242/// # Returns
4343/// 32-byte withdrawal credentials
4444///
4545/// # Errors
46- /// Returns error if address is invalid or checksum fails
47- /// NOTE: Done
46+ /// Returns error if address format is invalid
47+ /// NOTE: Done - Uses helpers::checksum_address to match Go's eth2util.ChecksumAddress behavior
48+ /// Go's ChecksumAddress accepts any valid hex without validating existing EIP-55 checksums
4849pub ( crate ) fn withdrawal_creds_from_addr (
4950 addr : & str ,
5051 compounding : bool ,
5152) -> Result < [ u8 ; 32 ] , DomainError > {
52- // Validate checksum
53- validate_checksum ( addr) ?;
53+ // Validate address format and get checksummed version
54+ // This matches Go's eth2util.ChecksumAddress: validates format but doesn't validate checksums
55+ helpers:: checksum_address ( addr) ?;
5456
55- // Remove 0x prefix if present
56- let addr_hex = addr. strip_prefix ( "0x" ) . unwrap_or ( addr) ;
57-
58- // Decode address bytes
59- let addr_bytes = hex:: decode ( addr_hex) ?;
57+ // Decode address bytes (we already validated format, so this should succeed)
58+ let addr_bytes = hex:: decode ( & addr[ 2 ..] ) ?;
6059
6160 let mut creds = [ 0u8 ; 32 ] ;
6261
@@ -79,58 +78,6 @@ pub(crate) fn withdrawal_creds_from_addr(
7978 Ok ( creds)
8079}
8180
82- /// Validates Ethereum address checksum using EIP-55.
83- ///
84- /// # Arguments
85- /// * `addr` - Ethereum address to validate
86- ///
87- /// # Errors
88- /// Returns error if checksum validation fails
89- /// NOTE: Consider to use alloy-primitive
90- /// NOTE: or create eth2util/helper
91- fn validate_checksum ( addr : & str ) -> Result < ( ) , DomainError > {
92- let addr_no_prefix = addr. strip_prefix ( "0x" ) . unwrap_or ( addr) ;
93-
94- // Check length
95- if addr_no_prefix. len ( ) != 40 {
96- return Err ( DomainError :: InvalidAddress ( format ! (
97- "Address must be 40 hex characters, got {}" ,
98- addr_no_prefix. len( )
99- ) ) ) ;
100- }
101-
102- // If all lowercase or all uppercase, skip checksum validation
103- let has_uppercase = addr_no_prefix. chars ( ) . any ( |c| c. is_uppercase ( ) ) ;
104- let has_lowercase = addr_no_prefix. chars ( ) . any ( |c| c. is_lowercase ( ) ) ;
105-
106- if !has_uppercase || !has_lowercase {
107- // Mixed case not present, skip validation
108- return Ok ( ( ) ) ;
109- }
110-
111- // Compute checksum using Keccak256
112- use sha3:: { Digest , Keccak256 } ;
113- let hash = Keccak256 :: digest ( addr_no_prefix. to_lowercase ( ) . as_bytes ( ) ) ;
114-
115- for ( i, ch) in addr_no_prefix. chars ( ) . enumerate ( ) {
116- if ch. is_alphabetic ( ) {
117- let hash_byte = hash[ i / 2 ] ;
118- let hash_nibble = if i % 2 == 0 {
119- hash_byte >> 4
120- } else {
121- hash_byte & 0x0f
122- } ;
123-
124- let should_be_uppercase = hash_nibble >= 8 ;
125-
126- if ch. is_uppercase ( ) != should_be_uppercase {
127- return Err ( DomainError :: InvalidChecksum ( addr. to_string ( ) ) ) ;
128- }
129- }
130- }
131-
132- Ok ( ( ) )
133- }
13481
13582/// Returns the deposit domain for the given fork version.
13683///
@@ -225,46 +172,63 @@ mod tests {
225172
226173 #[ test]
227174 fn test_withdrawal_creds_without_prefix ( ) {
175+ // Address without 0x prefix should fail (matching Go's behavior)
228176 let addr = "321dcb529f3945bc94fecea9d3bc5caf35253b94" ;
229- let creds = withdrawal_creds_from_addr ( addr, false ) . unwrap ( ) ;
230- assert_eq ! ( creds[ 0 ] , ETH1_ADDRESS_WITHDRAWAL_PREFIX ) ;
177+ let err = withdrawal_creds_from_addr ( addr, false ) . unwrap_err ( ) ;
178+ // Error is HelperError wrapped in DomainError
179+ assert ! ( matches!( err, DomainError :: HelperError ( _) ) ) ;
231180 }
232181
233182 #[ test]
234183 fn test_invalid_address_length ( ) {
235184 let addr = "0x321dcb5" ; // Too short
236185 let err = withdrawal_creds_from_addr ( addr, false ) . unwrap_err ( ) ;
237- assert ! ( matches!( err, DomainError :: InvalidAddress ( _) ) ) ;
186+ // Error is HelperError wrapped in DomainError
187+ assert ! ( matches!( err, DomainError :: HelperError ( _) ) ) ;
238188 }
239189
240190 #[ test]
241- fn test_validate_checksum_all_lowercase ( ) {
242- // All lowercase should pass
243- assert ! ( validate_checksum( "0x321dcb529f3945bc94fecea9d3bc5caf35253b94" ) . is_ok( ) ) ;
191+ fn test_address_parsing_all_lowercase ( ) {
192+ // All lowercase with 0x prefix should pass (matching Go's lenient behavior)
193+ let addr = "0x321dcb529f3945bc94fecea9d3bc5caf35253b94" ;
194+ assert ! ( helpers:: checksum_address( addr) . is_ok( ) ) ;
195+ assert ! ( withdrawal_creds_from_addr( addr, false ) . is_ok( ) ) ;
244196 }
245197
246198 #[ test]
247- fn test_validate_checksum_all_uppercase ( ) {
248- // All uppercase should pass
249- assert ! ( validate_checksum( "0x321DCB529F3945BC94FECEA9D3BC5CAF35253B94" ) . is_ok( ) ) ;
199+ fn test_address_parsing_all_uppercase ( ) {
200+ // All uppercase with 0x prefix should pass (matching Go's lenient behavior)
201+ let addr = "0x321DCB529F3945BC94FECEA9D3BC5CAF35253B94" ;
202+ assert ! ( helpers:: checksum_address( addr) . is_ok( ) ) ;
203+ assert ! ( withdrawal_creds_from_addr( addr, false ) . is_ok( ) ) ;
250204 }
251205
252206 #[ test]
253- fn test_validate_checksum_valid_mixed_case ( ) {
254- // Valid EIP-55 checksummed address
207+ fn test_address_parsing_valid_checksum ( ) {
208+ // Valid EIP-55 checksummed address should pass
255209 let addr = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" ;
256- assert ! ( validate_checksum( addr) . is_ok( ) ) ;
210+ assert ! ( helpers:: checksum_address( addr) . is_ok( ) ) ;
211+ assert ! ( withdrawal_creds_from_addr( addr, false ) . is_ok( ) ) ;
257212 }
258213
259214 #[ test]
260- fn test_validate_checksum_invalid ( ) {
261- // Invalid checksum (wrong case for some letters)
262- let addr = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" ; // Should have some uppercase
263- // This is all lowercase, so it passes (no checksum validation)
264- assert ! ( validate_checksum( addr) . is_ok( ) ) ;
265-
266- // But this should fail (mixed case but wrong checksum)
267- let addr_wrong = "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed" ; // Wrong case
268- assert ! ( validate_checksum( addr_wrong) . is_err( ) ) ;
215+ fn test_address_parsing_invalid_checksum_accepted ( ) {
216+ // Mixed case with WRONG checksum is ACCEPTED (matching Go's lenient behavior)
217+ // Go doesn't validate checksums, just accepts valid hex
218+ let addr_wrong = "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed" ;
219+ assert ! ( helpers:: checksum_address( addr_wrong) . is_ok( ) ) ;
220+ assert ! ( withdrawal_creds_from_addr( addr_wrong, false ) . is_ok( ) ) ;
269221 }
222+
223+ #[ test]
224+ fn test_address_requires_prefix ( ) {
225+ // Address without 0x prefix should fail (matching Go's behavior)
226+ let addr = "321dcb529f3945bc94fecea9d3bc5caf35253b94" ;
227+ assert ! ( withdrawal_creds_from_addr( addr, false ) . is_err( ) ) ;
228+
229+ // With prefix should work
230+ let addr_with_prefix = "0x321dcb529f3945bc94fecea9d3bc5caf35253b94" ;
231+ assert ! ( withdrawal_creds_from_addr( addr_with_prefix, false ) . is_ok( ) ) ;
232+ }
233+
270234}
0 commit comments