Skip to main content

eth_valkyoth_verify/
transaction_signature.rs

1use core::fmt;
2
3use eth_valkyoth_hash::Keccak256;
4use eth_valkyoth_primitives::{Address, ChainId};
5use eth_valkyoth_protocol::{
6    UnvalidatedAccessListTransaction, UnvalidatedBlobTransaction, UnvalidatedDynamicFeeTransaction,
7    UnvalidatedLegacyTransaction, UnvalidatedTransaction,
8};
9
10#[cfg(feature = "secp256k1-k256")]
11use crate::K256Secp256k1Backend;
12pub(crate) use crate::transaction_signature_set_code::validate_set_code_transaction_signature_with_backend;
13use crate::{
14    EthereumSignature, RecoverableSecp256k1, TransactionSigningHash, TransactionSigningHashError,
15    VerifyError, access_list_transaction_signing_hash, blob_transaction_signing_hash,
16    dynamic_fee_transaction_signing_hash, legacy_eip155_transaction_signing_hash,
17    require_access_list_replay_domain, require_blob_replay_domain,
18    require_dynamic_fee_replay_domain, require_legacy_replay_domain,
19    transaction_signature_helpers::{legacy_signature, recover_and_check},
20};
21
22#[cfg(test)]
23#[path = "transaction_signature_external_tests.rs"]
24mod external_tests;
25#[cfg(test)]
26#[path = "transaction_signature_tests.rs"]
27mod tests;
28
29/// A decoded transaction signature that passed replay-domain policy,
30/// signing-hash construction, low-s/y-parity policy, and sender recovery.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct ValidatedTransactionSignature {
33    sender: Address,
34    signing_hash: TransactionSigningHash,
35}
36
37impl ValidatedTransactionSignature {
38    /// Creates a validated signature result from its checked components.
39    #[must_use]
40    pub(crate) const fn new(sender: Address, signing_hash: TransactionSigningHash) -> Self {
41        Self {
42            sender,
43            signing_hash,
44        }
45    }
46
47    /// Returns the recovered transaction sender.
48    #[must_use]
49    pub const fn sender(self) -> Address {
50        self.sender
51    }
52
53    /// Returns the transaction signing hash that was recovered against.
54    #[must_use]
55    pub const fn signing_hash(self) -> TransactionSigningHash {
56        self.signing_hash
57    }
58}
59
60/// Decoded transaction signature validation failure.
61#[non_exhaustive]
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum TransactionSignatureValidationError {
64    /// Replay-domain policy rejected the transaction.
65    ReplayDomain(VerifyError),
66    /// Signing-hash construction failed before recovery.
67    SigningHash(TransactionSigningHashError),
68    /// Signature scalar, y-parity, or public-key recovery failed.
69    InvalidSignature,
70    /// This transaction family has no full signature-validation helper yet.
71    UnsupportedTransactionType,
72    /// The recovered sender does not match the expected sender.
73    WrongSender,
74}
75
76impl TransactionSignatureValidationError {
77    /// Stable machine-readable error code.
78    #[must_use]
79    pub const fn code(self) -> &'static str {
80        match self {
81            Self::ReplayDomain(error) => error.code(),
82            Self::SigningHash(error) => error.code(),
83            Self::InvalidSignature => "ETH_TX_SIGNATURE_INVALID",
84            Self::UnsupportedTransactionType => "ETH_TX_SIGNATURE_UNSUPPORTED_TYPE",
85            Self::WrongSender => "ETH_TX_SIGNATURE_WRONG_SENDER",
86        }
87    }
88
89    /// Stable human-readable error message.
90    #[must_use]
91    pub const fn message(self) -> &'static str {
92        match self {
93            Self::ReplayDomain(error) => error.message(),
94            Self::SigningHash(_) => "transaction signing hash construction failed",
95            Self::InvalidSignature => "transaction signature is not accepted",
96            Self::UnsupportedTransactionType => {
97                "transaction type has no signature validation helper yet"
98            }
99            Self::WrongSender => "transaction signature recovered a different sender",
100        }
101    }
102
103    /// Stable high-level category for policy decisions.
104    #[must_use]
105    pub const fn category(self) -> TransactionSignatureValidationErrorCategory {
106        match self {
107            Self::ReplayDomain(_) => TransactionSignatureValidationErrorCategory::ReplayDomain,
108            Self::SigningHash(_) => TransactionSignatureValidationErrorCategory::SigningHash,
109            Self::InvalidSignature | Self::WrongSender | Self::UnsupportedTransactionType => {
110                TransactionSignatureValidationErrorCategory::Signature
111            }
112        }
113    }
114}
115
116impl fmt::Display for TransactionSignatureValidationError {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        formatter.write_str(self.message())
119    }
120}
121
122#[cfg(feature = "std")]
123impl std::error::Error for TransactionSignatureValidationError {}
124
125/// Stable high-level decoded transaction signature validation categories.
126#[non_exhaustive]
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum TransactionSignatureValidationErrorCategory {
129    /// Replay-domain or chain binding failure.
130    ReplayDomain,
131    /// Signing preimage or hash construction failure.
132    SigningHash,
133    /// Signature representation, recovery, or sender-match failure.
134    Signature,
135}
136
137/// Validates any supported decoded transaction signature.
138///
139/// This combines replay-domain checking, canonical signing-hash construction,
140/// low-s/y-parity policy, and sender recovery. It intentionally does not prove
141/// fork validity, fee validity, account-state validity, blob/KZG validity, or
142/// protocol typestate promotion.
143///
144/// # EIP-7702 set-code transactions
145///
146/// For [`UnvalidatedTransaction::SetCode`], `Ok` proves only that the outer
147/// transaction sender signature in the `0x04` domain is valid. It does not
148/// validate any authorization-tuple signature inside `authorization_list`.
149/// Callers must additionally call
150/// [`crate::validate_set_code_authorization_signature`] for every tuple before
151/// treating the authorization list as authenticated. Applying a delegation from
152/// an unchecked authorization tuple is equivalent to skipping signature
153/// verification for that authorizing account.
154#[cfg(feature = "secp256k1-k256")]
155pub fn validate_transaction_signature<H1, H2>(
156    expected_chain: ChainId,
157    transaction: UnvalidatedTransaction<'_>,
158    expected_sender: Option<Address>,
159    scratch: &mut [u8],
160    signing_hasher: H1,
161    address_hasher: H2,
162) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
163where
164    H1: Keccak256,
165    H2: Keccak256,
166{
167    validate_transaction_signature_with_backend(
168        expected_chain,
169        transaction,
170        expected_sender,
171        scratch,
172        signing_hasher,
173        K256Secp256k1Backend,
174        address_hasher,
175    )
176}
177
178/// Validates any supported decoded transaction signature through a caller-provided backend.
179///
180/// This is the default dependency-free sender-recovery entry point. Use the
181/// `secp256k1-k256` feature only when the reviewed `k256` adapter is the
182/// desired concrete backend.
183pub fn validate_transaction_signature_with_backend<B, H1, H2>(
184    expected_chain: ChainId,
185    transaction: UnvalidatedTransaction<'_>,
186    expected_sender: Option<Address>,
187    scratch: &mut [u8],
188    signing_hasher: H1,
189    mut secp256k1_backend: B,
190    address_hasher: H2,
191) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
192where
193    B: RecoverableSecp256k1,
194    H1: Keccak256,
195    H2: Keccak256,
196{
197    match transaction {
198        UnvalidatedTransaction::Legacy(tx) => validate_legacy_transaction_signature_with_backend(
199            expected_chain,
200            &tx,
201            expected_sender,
202            scratch,
203            signing_hasher,
204            &mut secp256k1_backend,
205            address_hasher,
206        ),
207        UnvalidatedTransaction::AccessList(tx) => {
208            validate_access_list_transaction_signature_with_backend(
209                expected_chain,
210                &tx,
211                expected_sender,
212                scratch,
213                signing_hasher,
214                &mut secp256k1_backend,
215                address_hasher,
216            )
217        }
218        UnvalidatedTransaction::DynamicFee(tx) => {
219            validate_dynamic_fee_transaction_signature_with_backend(
220                expected_chain,
221                &tx,
222                expected_sender,
223                scratch,
224                signing_hasher,
225                &mut secp256k1_backend,
226                address_hasher,
227            )
228        }
229        UnvalidatedTransaction::Blob(tx) => validate_blob_transaction_signature_with_backend(
230            expected_chain,
231            &tx,
232            expected_sender,
233            scratch,
234            signing_hasher,
235            &mut secp256k1_backend,
236            address_hasher,
237        ),
238        UnvalidatedTransaction::SetCode(tx) => {
239            validate_set_code_transaction_signature_with_backend(
240                expected_chain,
241                &tx,
242                expected_sender,
243                scratch,
244                signing_hasher,
245                &mut secp256k1_backend,
246                address_hasher,
247            )
248        }
249    }
250}
251
252/// Validates a decoded legacy EIP-155 transaction signature.
253#[cfg(feature = "secp256k1-k256")]
254pub fn validate_legacy_transaction_signature<H1, H2>(
255    expected_chain: ChainId,
256    transaction: &UnvalidatedLegacyTransaction<'_>,
257    expected_sender: Option<Address>,
258    scratch: &mut [u8],
259    signing_hasher: H1,
260    address_hasher: H2,
261) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
262where
263    H1: Keccak256,
264    H2: Keccak256,
265{
266    validate_legacy_transaction_signature_with_backend(
267        expected_chain,
268        transaction,
269        expected_sender,
270        scratch,
271        signing_hasher,
272        K256Secp256k1Backend,
273        address_hasher,
274    )
275}
276
277/// Validates a decoded legacy EIP-155 transaction signature through a backend.
278pub fn validate_legacy_transaction_signature_with_backend<B, H1, H2>(
279    expected_chain: ChainId,
280    transaction: &UnvalidatedLegacyTransaction<'_>,
281    expected_sender: Option<Address>,
282    scratch: &mut [u8],
283    signing_hasher: H1,
284    secp256k1_backend: B,
285    address_hasher: H2,
286) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
287where
288    B: RecoverableSecp256k1,
289    H1: Keccak256,
290    H2: Keccak256,
291{
292    require_legacy_replay_domain(expected_chain, transaction)
293        .map_err(TransactionSignatureValidationError::ReplayDomain)?;
294    let signing_hash = legacy_eip155_transaction_signing_hash(transaction, scratch, signing_hasher)
295        .map_err(TransactionSignatureValidationError::SigningHash)?;
296    let signature = legacy_signature(transaction)?;
297    recover_and_check(
298        signing_hash,
299        signature,
300        expected_sender,
301        secp256k1_backend,
302        address_hasher,
303    )
304}
305
306/// Validates a decoded EIP-2930 transaction signature.
307#[cfg(feature = "secp256k1-k256")]
308pub fn validate_access_list_transaction_signature<H1, H2>(
309    expected_chain: ChainId,
310    transaction: &UnvalidatedAccessListTransaction<'_>,
311    expected_sender: Option<Address>,
312    scratch: &mut [u8],
313    signing_hasher: H1,
314    address_hasher: H2,
315) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
316where
317    H1: Keccak256,
318    H2: Keccak256,
319{
320    validate_access_list_transaction_signature_with_backend(
321        expected_chain,
322        transaction,
323        expected_sender,
324        scratch,
325        signing_hasher,
326        K256Secp256k1Backend,
327        address_hasher,
328    )
329}
330
331/// Validates a decoded EIP-2930 transaction signature through a backend.
332pub fn validate_access_list_transaction_signature_with_backend<B, H1, H2>(
333    expected_chain: ChainId,
334    transaction: &UnvalidatedAccessListTransaction<'_>,
335    expected_sender: Option<Address>,
336    scratch: &mut [u8],
337    signing_hasher: H1,
338    secp256k1_backend: B,
339    address_hasher: H2,
340) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
341where
342    B: RecoverableSecp256k1,
343    H1: Keccak256,
344    H2: Keccak256,
345{
346    require_access_list_replay_domain(expected_chain, transaction)
347        .map_err(TransactionSignatureValidationError::ReplayDomain)?;
348    let signing_hash = access_list_transaction_signing_hash(transaction, scratch, signing_hasher)
349        .map_err(TransactionSignatureValidationError::SigningHash)?;
350    let signature =
351        EthereumSignature::from_parts(transaction.r, transaction.s, transaction.y_parity);
352    recover_and_check(
353        signing_hash,
354        signature,
355        expected_sender,
356        secp256k1_backend,
357        address_hasher,
358    )
359}
360
361/// Validates a decoded EIP-1559 transaction signature.
362#[cfg(feature = "secp256k1-k256")]
363pub fn validate_dynamic_fee_transaction_signature<H1, H2>(
364    expected_chain: ChainId,
365    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
366    expected_sender: Option<Address>,
367    scratch: &mut [u8],
368    signing_hasher: H1,
369    address_hasher: H2,
370) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
371where
372    H1: Keccak256,
373    H2: Keccak256,
374{
375    validate_dynamic_fee_transaction_signature_with_backend(
376        expected_chain,
377        transaction,
378        expected_sender,
379        scratch,
380        signing_hasher,
381        K256Secp256k1Backend,
382        address_hasher,
383    )
384}
385
386/// Validates a decoded EIP-1559 transaction signature through a backend.
387pub fn validate_dynamic_fee_transaction_signature_with_backend<B, H1, H2>(
388    expected_chain: ChainId,
389    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
390    expected_sender: Option<Address>,
391    scratch: &mut [u8],
392    signing_hasher: H1,
393    secp256k1_backend: B,
394    address_hasher: H2,
395) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
396where
397    B: RecoverableSecp256k1,
398    H1: Keccak256,
399    H2: Keccak256,
400{
401    require_dynamic_fee_replay_domain(expected_chain, transaction)
402        .map_err(TransactionSignatureValidationError::ReplayDomain)?;
403    let signing_hash = dynamic_fee_transaction_signing_hash(transaction, scratch, signing_hasher)
404        .map_err(TransactionSignatureValidationError::SigningHash)?;
405    let signature =
406        EthereumSignature::from_parts(transaction.r, transaction.s, transaction.y_parity);
407    recover_and_check(
408        signing_hash,
409        signature,
410        expected_sender,
411        secp256k1_backend,
412        address_hasher,
413    )
414}
415
416/// Validates a decoded EIP-4844 transaction signature.
417#[cfg(feature = "secp256k1-k256")]
418pub fn validate_blob_transaction_signature<H1, H2>(
419    expected_chain: ChainId,
420    transaction: &UnvalidatedBlobTransaction<'_>,
421    expected_sender: Option<Address>,
422    scratch: &mut [u8],
423    signing_hasher: H1,
424    address_hasher: H2,
425) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
426where
427    H1: Keccak256,
428    H2: Keccak256,
429{
430    validate_blob_transaction_signature_with_backend(
431        expected_chain,
432        transaction,
433        expected_sender,
434        scratch,
435        signing_hasher,
436        K256Secp256k1Backend,
437        address_hasher,
438    )
439}
440
441/// Validates a decoded EIP-4844 transaction signature through a backend.
442pub fn validate_blob_transaction_signature_with_backend<B, H1, H2>(
443    expected_chain: ChainId,
444    transaction: &UnvalidatedBlobTransaction<'_>,
445    expected_sender: Option<Address>,
446    scratch: &mut [u8],
447    signing_hasher: H1,
448    secp256k1_backend: B,
449    address_hasher: H2,
450) -> Result<ValidatedTransactionSignature, TransactionSignatureValidationError>
451where
452    B: RecoverableSecp256k1,
453    H1: Keccak256,
454    H2: Keccak256,
455{
456    require_blob_replay_domain(expected_chain, transaction)
457        .map_err(TransactionSignatureValidationError::ReplayDomain)?;
458    let signing_hash = blob_transaction_signing_hash(transaction, scratch, signing_hasher)
459        .map_err(TransactionSignatureValidationError::SigningHash)?;
460    let signature =
461        EthereumSignature::from_parts(transaction.r, transaction.s, transaction.y_parity);
462    recover_and_check(
463        signing_hash,
464        signature,
465        expected_sender,
466        secp256k1_backend,
467        address_hasher,
468    )
469}