@@ -2757,6 +2757,139 @@ mod frame_sig_validation_tests {
27572757 }
27582758 }
27592759
2760+ /// Sign `msg_hash` with a fixed devnet key and return
2761+ /// `(recovery_id, r || s, signer)`. `sign_prehash_recoverable` normalizes
2762+ /// `s` to low-s, so every vector produced here is canonical per EIP-8141.
2763+ #[ expect(
2764+ clippy:: indexing_slicing,
2765+ reason = "fixed-size buffers with well-known bounds in test code"
2766+ ) ]
2767+ fn secp256k1_vector ( msg_hash : H256 ) -> ( u8 , [ u8 ; 64 ] , Address ) {
2768+ use k256:: ecdsa:: SigningKey ;
2769+
2770+ let pk_hex = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" ;
2771+ let pk_bytes: Vec < u8 > = ( 0 ..pk_hex. len ( ) )
2772+ . step_by ( 2 )
2773+ . map ( |i| u8:: from_str_radix ( & pk_hex[ i..i + 2 ] , 16 ) . unwrap ( ) )
2774+ . collect ( ) ;
2775+ let private_key: [ u8 ; 32 ] = pk_bytes. try_into ( ) . unwrap ( ) ;
2776+ let signing_key = SigningKey :: from_bytes ( & private_key. into ( ) ) . unwrap ( ) ;
2777+
2778+ let ( raw_sig, recovery_id) = signing_key
2779+ . sign_prehash_recoverable ( msg_hash. as_bytes ( ) )
2780+ . unwrap ( ) ;
2781+
2782+ // Signer address = keccak(uncompressed pubkey without the 0x04 tag)[12..].
2783+ let uncompressed = signing_key. verifying_key ( ) . to_encoded_point ( false ) ;
2784+ let pub_hash = ethrex_crypto:: keccak:: keccak_hash ( & uncompressed. as_bytes ( ) [ 1 ..] ) ;
2785+ let signer = Address :: from_slice ( & pub_hash[ 12 ..] ) ;
2786+
2787+ let mut rs = [ 0u8 ; 64 ] ;
2788+ rs. copy_from_slice ( & raw_sig. to_bytes ( ) ) ;
2789+ ( recovery_id. to_byte ( ) , rs, signer)
2790+ }
2791+
2792+ /// The `[v | r | s]` frame signature for [`secp256k1_vector`] over
2793+ /// `msg_hash`, with `signature[0]` forced to `v_byte` so the encoding of
2794+ /// that single byte is the only variable under test.
2795+ #[ expect(
2796+ clippy:: indexing_slicing,
2797+ reason = "fixed-size buffers with well-known bounds in test code"
2798+ ) ]
2799+ fn secp_frame_sig ( msg_hash : H256 , v_byte : u8 , signer : Option < Address > ) -> FrameSignature {
2800+ let ( _, rs, _) = secp256k1_vector ( msg_hash) ;
2801+ let mut bytes = vec ! [ 0u8 ; 65 ] ;
2802+ bytes[ 0 ] = v_byte;
2803+ bytes[ 1 ..65 ] . copy_from_slice ( & rs) ;
2804+ FrameSignature {
2805+ scheme : FRAME_SIG_SCHEME_SECP256K1 ,
2806+ signer,
2807+ msg : Bytes :: new ( ) , // empty -> the signature is over `sig_hash`
2808+ signature : Bytes :: from ( bytes) ,
2809+ }
2810+ }
2811+
2812+ /// Deterministically pick a prehash whose RFC-6979 signature has the
2813+ /// requested recovery id, so both `v = 0` and `v = 1` are exercised with
2814+ /// real signatures rather than one arbitrary parity.
2815+ fn msg_hash_with_recovery_id ( want : u8 ) -> H256 {
2816+ ( 1u64 ..64 )
2817+ . map ( H256 :: from_low_u64_be)
2818+ . find ( |h| secp256k1_vector ( * h) . 0 == want)
2819+ . expect ( "no prehash in range yields the requested recovery id" )
2820+ }
2821+
2822+ #[ test]
2823+ fn secp256k1_spec_recovery_id_is_accepted ( ) {
2824+ // EIP-8141 (spec commit fe0940cae2) Signature Validation: `signature[0]`
2825+ // is the recovery id `0`/`1`, matching EIP-2718 typed transactions, not
2826+ // the EVM `ecrecover` `v` (`27`/`28`). A conformant, canonical low-s
2827+ // signature must authenticate on the block-validation path.
2828+ for want_v in [ 0u8 , 1u8 ] {
2829+ let msg_hash = msg_hash_with_recovery_id ( want_v) ;
2830+ let ( v, _, signer) = secp256k1_vector ( msg_hash) ;
2831+ assert_eq ! ( v, want_v, "vector must have the requested recovery id" ) ;
2832+ let sig = secp_frame_sig ( msg_hash, want_v, Some ( signer) ) ;
2833+ assert ! (
2834+ frame_signatures_are_low_s( std:: slice:: from_ref( & sig) ) ,
2835+ "k256 normalizes to low-s; v = {want_v} vector must be canonical"
2836+ ) ;
2837+ assert ! (
2838+ validate_frame_signatures(
2839+ & [ sig] ,
2840+ msg_hash,
2841+ Address :: zero( ) ,
2842+ hegota( ) ,
2843+ & ethrex_crypto:: NativeCrypto
2844+ ) ,
2845+ "spec-encoded recovery id v = {want_v} must be accepted"
2846+ ) ;
2847+ }
2848+ }
2849+
2850+ #[ test]
2851+ fn secp256k1_evm_v_encoding_is_rejected ( ) {
2852+ // `27`/`28` is the EVM `ecrecover` encoding. EIP-8141 rejects any
2853+ // `v > 1`, so the legacy encoding must never authenticate a frame tx,
2854+ // otherwise this client accepts blocks a conformant client rejects.
2855+ for v in [ 27u8 , 28u8 ] {
2856+ let msg_hash = msg_hash_with_recovery_id ( v - 27 ) ;
2857+ let ( _, _, signer) = secp256k1_vector ( msg_hash) ;
2858+ let sig = secp_frame_sig ( msg_hash, v, Some ( signer) ) ;
2859+ assert ! (
2860+ !validate_frame_signatures(
2861+ & [ sig] ,
2862+ msg_hash,
2863+ Address :: zero( ) ,
2864+ hegota( ) ,
2865+ & ethrex_crypto:: NativeCrypto
2866+ ) ,
2867+ "EVM ecrecover encoding v = {v} must be rejected"
2868+ ) ;
2869+ }
2870+ }
2871+
2872+ #[ test]
2873+ fn secp256k1_out_of_range_v_is_rejected ( ) {
2874+ // Everything else above the recovery-id range is invalid too: the
2875+ // neighbours of `27`/`28`, the EIP-155 chain-id forms, and 0xFF.
2876+ let msg_hash = msg_hash_with_recovery_id ( 0 ) ;
2877+ let ( _, _, signer) = secp256k1_vector ( msg_hash) ;
2878+ for v in [ 2u8 , 3 , 26 , 29 , 35 , 36 , 0xFF ] {
2879+ let sig = secp_frame_sig ( msg_hash, v, Some ( signer) ) ;
2880+ assert ! (
2881+ !validate_frame_signatures(
2882+ & [ sig] ,
2883+ msg_hash,
2884+ Address :: zero( ) ,
2885+ hegota( ) ,
2886+ & ethrex_crypto:: NativeCrypto
2887+ ) ,
2888+ "out-of-range v = {v} must be rejected"
2889+ ) ;
2890+ }
2891+ }
2892+
27602893 fn p256_sig_with_s ( s : & [ u8 ; 32 ] ) -> FrameSignature {
27612894 // [r(32) | s(32) | qx(32) | qy(32)]
27622895 let mut bytes = vec ! [ 0u8 ; 128 ] ;
@@ -2920,46 +3053,14 @@ mod frame_sig_validation_tests {
29203053 }
29213054
29223055 #[ test]
2923- #[ expect(
2924- clippy:: indexing_slicing,
2925- reason = "fixed-size buffers with well-known bounds in test code"
2926- ) ]
29273056 fn secp256k1_positive_and_tampered ( ) {
2928- // Build a real secp256k1 signature vector using k256.
2929- use k256:: ecdsa:: SigningKey ;
2930-
2931- let pk_hex = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" ;
2932- let pk_bytes: Vec < u8 > = ( 0 ..pk_hex. len ( ) )
2933- . step_by ( 2 )
2934- . map ( |i| u8:: from_str_radix ( & pk_hex[ i..i + 2 ] , 16 ) . unwrap ( ) )
2935- . collect ( ) ;
2936- let private_key: [ u8 ; 32 ] = pk_bytes. try_into ( ) . unwrap ( ) ;
2937- let signing_key = SigningKey :: from_bytes ( & private_key. into ( ) ) . unwrap ( ) ;
2938-
3057+ // Build a real secp256k1 signature vector using k256. The outer
3058+ // signature is `v || r || s` (65 bytes) where `v` is the EIP-8141
3059+ // recovery id (0/1), NOT the EVM ecrecover `v` (27/28).
29393060 let msg_hash: H256 = H256 :: from_low_u64_be ( 0xDEADBEEF_CAFEBABE ) ;
3061+ let ( recovery_id, _, expected_signer) = secp256k1_vector ( msg_hash) ;
29403062
2941- let ( raw_sig, recovery_id) = signing_key
2942- . sign_prehash_recoverable ( msg_hash. as_bytes ( ) )
2943- . unwrap ( ) ;
2944-
2945- // Derive the expected signer address
2946- let uncompressed = signing_key. verifying_key ( ) . to_encoded_point ( false ) ;
2947- let pub_hash = ethrex_crypto:: keccak:: keccak_hash ( & uncompressed. as_bytes ( ) [ 1 ..] ) ;
2948- let expected_signer = Address :: from_slice ( & pub_hash[ 12 ..] ) ;
2949-
2950- // Build the outer signature: v || r || s (65 bytes).
2951- // EVM ecrecover expects v ∈ {27, 28}, so add 27 to the raw recovery id.
2952- let mut sig_bytes = vec ! [ 0u8 ; 65 ] ;
2953- sig_bytes[ 0 ] = 27 + recovery_id. to_byte ( ) ;
2954- sig_bytes[ 1 ..33 ] . copy_from_slice ( & raw_sig. to_bytes ( ) [ ..32 ] ) ; // r
2955- sig_bytes[ 33 ..65 ] . copy_from_slice ( & raw_sig. to_bytes ( ) [ 32 ..] ) ; // s
2956-
2957- let valid_sig = FrameSignature {
2958- scheme : FRAME_SIG_SCHEME_SECP256K1 ,
2959- signer : Some ( expected_signer) ,
2960- msg : Bytes :: new ( ) , // empty → use sig_hash
2961- signature : Bytes :: from ( sig_bytes. clone ( ) ) ,
2962- } ;
3063+ let valid_sig = secp_frame_sig ( msg_hash, recovery_id, Some ( expected_signer) ) ;
29633064
29643065 // Positive: correct signer → valid
29653066 assert ! (
@@ -2991,12 +3092,7 @@ mod frame_sig_validation_tests {
29913092 ) ;
29923093
29933094 // Empty signer (None) resolves to tx.sender: valid iff sender == recovered.
2994- let empty_signer_sig = FrameSignature {
2995- scheme : FRAME_SIG_SCHEME_SECP256K1 ,
2996- signer : None ,
2997- msg : Bytes :: new ( ) ,
2998- signature : Bytes :: from ( sig_bytes. clone ( ) ) ,
2999- } ;
3095+ let empty_signer_sig = secp_frame_sig ( msg_hash, recovery_id, None ) ;
30003096 assert ! (
30013097 validate_frame_signatures(
30023098 std:: slice:: from_ref( & empty_signer_sig) ,
0 commit comments