From d20d9beb83f188757dadf28937c1515cafe01e85 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Tue, 27 Oct 2026 10:56:24 +0100 Subject: [PATCH 1/2] fix(primitives): reject high-s executor signatures on evnode txs recover_executor recovered the executor without enforcing EIP-2 low-s, so a malleable high-s signature was accepted. Reject it there, and route the SignerRecoverable impls for EvTxEnvelope and EvPooledTxEnvelope through recover_executor so the check applies on both paths instead of the duplicated inline recovery. Adds a regression test for the high-s rejection. --- crates/ev-primitives/src/pool.rs | 4 ++-- crates/ev-primitives/src/tx.rs | 35 ++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/ev-primitives/src/pool.rs b/crates/ev-primitives/src/pool.rs index e80a391b..56ff2a84 100644 --- a/crates/ev-primitives/src/pool.rs +++ b/crates/ev-primitives/src/pool.rs @@ -45,8 +45,8 @@ impl SignerRecoverable for EvPooledTxEnvelope { match self { EvPooledTxEnvelope::Ethereum(tx) => tx.recover_signer(), EvPooledTxEnvelope::EvNode(tx) => tx - .signature() - .recover_address_from_prehash(&tx.tx().executor_signing_hash()) + .tx() + .recover_executor(tx.signature()) .map_err(|_| alloy_consensus::crypto::RecoveryError::new()), } } diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index efa9830b..9fba4cf4 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -104,6 +104,11 @@ impl EvNodeTransaction { &self, signature: &Signature, ) -> Result { + // Reject malleable high-s signatures (EIP-2): `s` must be in the lower + // half of the curve order. + if signature.normalize_s().is_some() { + return Err(alloy_primitives::SignatureError::FromBytes("high-s signature")); + } signature.recover_address_from_prehash(&self.executor_signing_hash()) } @@ -400,8 +405,8 @@ impl SignerRecoverable for EvTxEnvelope { match self { EvTxEnvelope::Ethereum(tx) => tx.recover_signer(), EvTxEnvelope::EvNode(tx) => tx - .signature() - .recover_address_from_prehash(&tx.tx().executor_signing_hash()) + .tx() + .recover_executor(tx.signature()) .map_err(|_| alloy_consensus::crypto::RecoveryError::new()), } } @@ -683,6 +688,32 @@ mod tests { assert_ne!(tx.sponsor_signing_hash(a), tx.sponsor_signing_hash(b)); } + #[test] + fn recover_executor_rejects_high_s_signature() { + let tx = sample_tx(); + // secp256k1 curve order n. + let n = U256::from_str_radix( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + 16, + ) + .unwrap(); + + // s in the upper half of the order is malleable and must be rejected. + let high_s = Signature::new(U256::from(1u64), n - U256::from(1u64), false); + assert!(matches!( + tx.recover_executor(&high_s), + Err(alloy_primitives::SignatureError::FromBytes("high-s signature")) + )); + + // A low-s signature passes the malleability guard (recovery may then + // fail for an unrelated reason; that is not the high-s rejection). + let low_s = Signature::new(U256::from(1u64), U256::from(1u64), false); + assert!(!matches!( + tx.recover_executor(&low_s), + Err(alloy_primitives::SignatureError::FromBytes("high-s signature")) + )); + } + #[test] fn rlp_roundtrip_with_optional_signature() { let mut tx = sample_tx(); From 703677cd225109502fdbcfea3dc159fab4cbc5f2 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Tue, 27 Oct 2026 11:14:44 +0100 Subject: [PATCH 2/2] chore: cargo fmt --- crates/ev-primitives/src/tx.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index 9fba4cf4..a423e4de 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -107,7 +107,9 @@ impl EvNodeTransaction { // Reject malleable high-s signatures (EIP-2): `s` must be in the lower // half of the curve order. if signature.normalize_s().is_some() { - return Err(alloy_primitives::SignatureError::FromBytes("high-s signature")); + return Err(alloy_primitives::SignatureError::FromBytes( + "high-s signature", + )); } signature.recover_address_from_prehash(&self.executor_signing_hash()) } @@ -702,7 +704,9 @@ mod tests { let high_s = Signature::new(U256::from(1u64), n - U256::from(1u64), false); assert!(matches!( tx.recover_executor(&high_s), - Err(alloy_primitives::SignatureError::FromBytes("high-s signature")) + Err(alloy_primitives::SignatureError::FromBytes( + "high-s signature" + )) )); // A low-s signature passes the malleability guard (recovery may then @@ -710,7 +714,9 @@ mod tests { let low_s = Signature::new(U256::from(1u64), U256::from(1u64), false); assert!(!matches!( tx.recover_executor(&low_s), - Err(alloy_primitives::SignatureError::FromBytes("high-s signature")) + Err(alloy_primitives::SignatureError::FromBytes( + "high-s signature" + )) )); }