Skip to main content

eth_valkyoth_protocol/transaction/set_code/
validity.rs

1use core::fmt;
2
3use eth_valkyoth_primitives::{Address, ChainId, Gas, Nonce};
4
5use super::{SetCodeAuthorization, SetCodeTransactionDecodeError, UnvalidatedSetCodeTransaction};
6use crate::{Hardfork, ValidationContext};
7
8/// EIP-7702 delegation indicator prefix bytes.
9pub const EIP_7702_DELEGATION_INDICATOR_PREFIX: [u8; 3] = [0xef, 0x01, 0x00];
10
11/// Validation context for EIP-7702 set-code transaction validity.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct SetCodeTransactionValidationContext {
14    /// Active fork context for the block that would include this transaction.
15    pub fork: ValidationContext,
16    /// Caller-computed minimum gas limit, usually intrinsic gas.
17    ///
18    /// This crate does not bundle execution-state gas accounting. Supplying a
19    /// value here lets callers bind their own gas calculation to the same
20    /// transaction-validity gate.
21    pub minimum_gas_limit: Option<Gas>,
22}
23
24/// Authority recovered for one authorization tuple.
25///
26/// Callers should create this value only from a successful
27/// `eth-valkyoth-verify` authorization signature validation result.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct SetCodeAuthorizationAuthority {
30    /// Index of the authorization tuple in the transaction authorization list.
31    pub authorization_index: usize,
32    /// Recovered authorizing account.
33    pub authority: Address,
34}
35
36/// Caller-provided source of recovered authorization authorities.
37///
38/// The slice implementation is intended for tests and small fixtures. Production
39/// callers with large authorization lists should use an indexed implementation
40/// to avoid repeated linear scans.
41pub trait SetCodeAuthorizationAuthorityView {
42    /// Returns the recovered authority for `authorization` at
43    /// `authorization_index`.
44    fn authority_for(
45        &self,
46        authorization_index: usize,
47        authorization: SetCodeAuthorization,
48    ) -> Option<Address>;
49}
50
51impl SetCodeAuthorizationAuthorityView for [SetCodeAuthorizationAuthority] {
52    fn authority_for(
53        &self,
54        authorization_index: usize,
55        _authorization: SetCodeAuthorization,
56    ) -> Option<Address> {
57        self.iter()
58            .find(|entry| entry.authorization_index == authorization_index)
59            .map(|entry| entry.authority)
60    }
61}
62
63/// Caller-reviewed authority account code state.
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum SetCodeAuthorityCode {
66    /// The authority account has no code.
67    Empty,
68    /// The authority account already contains an EIP-7702 delegation indicator.
69    ///
70    /// This crate does not inspect account bytecode or verify the
71    /// [`EIP_7702_DELEGATION_INDICATOR_PREFIX`] itself. The `Delegation`
72    /// classification is caller-trusted account-state input.
73    Delegation {
74        /// Delegation target encoded after `0xef0100`.
75        target: Address,
76    },
77    /// The authority account contains non-delegation code.
78    Other,
79}
80
81/// Caller-provided authority account state.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct SetCodeAuthorityAccount {
84    /// Authority account address.
85    pub authority: Address,
86    /// Current authority account nonce.
87    pub nonce: Nonce,
88    /// Current authority account code classification.
89    pub code: SetCodeAuthorityCode,
90}
91
92/// Caller-provided authority account-state view.
93///
94/// EIP-7702 treats an authority account that is absent from the state trie as an
95/// empty account with nonce `0`. Implementations must synthesize
96/// `SetCodeAuthorityAccount { nonce: Nonce::new(0), code: Empty, .. }` for that
97/// case. Return `None` only when state is genuinely unavailable.
98///
99/// The slice implementation is intended for tests and small fixtures. Production
100/// callers with large authorization lists should use an indexed implementation
101/// to avoid repeated linear scans.
102pub trait SetCodeAuthorityStateView {
103    /// Returns account state for `authority`.
104    fn authority_account(&self, authority: Address) -> Option<SetCodeAuthorityAccount>;
105}
106
107impl SetCodeAuthorityStateView for [SetCodeAuthorityAccount] {
108    fn authority_account(&self, authority: Address) -> Option<SetCodeAuthorityAccount> {
109        self.iter()
110            .find(|entry| entry.authority == authority)
111            .copied()
112    }
113}
114
115/// EIP-7702 set-code transaction that passed context validity checks.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct ValidSetCodeTransaction<'a> {
118    transaction: UnvalidatedSetCodeTransaction<'a>,
119    authorization_count: usize,
120    applied_authorization_count: usize,
121    skipped_authorization_count: usize,
122}
123
124impl<'a> ValidSetCodeTransaction<'a> {
125    const fn new(
126        transaction: UnvalidatedSetCodeTransaction<'a>,
127        authorization_count: usize,
128        applied_authorization_count: usize,
129        skipped_authorization_count: usize,
130    ) -> Self {
131        Self {
132            transaction,
133            authorization_count,
134            applied_authorization_count,
135            skipped_authorization_count,
136        }
137    }
138
139    /// Returns the underlying decoded transaction.
140    #[must_use]
141    pub const fn transaction(self) -> UnvalidatedSetCodeTransaction<'a> {
142        self.transaction
143    }
144
145    /// Returns the number of checked authorization tuples.
146    #[must_use]
147    pub const fn authorization_count(self) -> usize {
148        self.authorization_count
149    }
150
151    /// Returns the number of authorization tuples that passed per-tuple checks.
152    #[must_use]
153    pub const fn applied_authorization_count(self) -> usize {
154        self.applied_authorization_count
155    }
156
157    /// Returns the number of authorization tuples skipped by EIP-7702 rules.
158    #[must_use]
159    pub const fn skipped_authorization_count(self) -> usize {
160        self.skipped_authorization_count
161    }
162}
163
164/// EIP-7702 set-code transaction validity failure.
165///
166/// Per-authorization-tuple failures are exposed for diagnostics, but the
167/// transaction validity gate does not return them as fatal errors. EIP-7702
168/// skips invalid authorization tuples and continues processing later tuples.
169#[non_exhaustive]
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum SetCodeTransactionValidityError {
172    /// The selected fork is inactive for this validation context.
173    InactiveFork,
174    /// Set-code transactions are not admitted before Prague/Pectra.
175    ForkBeforePrague,
176    /// The transaction chain ID does not match the validation context.
177    WrongTransactionChain,
178    /// The priority fee exceeds the max fee.
179    PriorityFeeExceedsMaxFee,
180    /// The gas limit is below the caller-computed minimum.
181    GasLimitTooLow,
182    /// EIP-7702 transactions must have at least one authorization tuple.
183    EmptyAuthorizationList,
184    /// A previously decoded authorization tuple failed to iterate.
185    AuthorizationDecode(SetCodeTransactionDecodeError),
186    /// No verified authority was supplied for this authorization tuple.
187    MissingAuthorizationAuthority {
188        /// Authorization tuple index.
189        authorization_index: usize,
190    },
191    /// Authorization chain ID is neither universal nor the expected chain.
192    WrongAuthorizationChain {
193        /// Authorization tuple index.
194        authorization_index: usize,
195    },
196    /// Authorization nonce is `u64::MAX` and cannot be incremented.
197    AuthorizationNonceTooHigh {
198        /// Authorization tuple index.
199        authorization_index: usize,
200    },
201    /// No account state was supplied for the recovered authority.
202    MissingAuthorityState {
203        /// Authorization tuple index.
204        authorization_index: usize,
205    },
206    /// Recovered authority nonce does not match the authorization nonce.
207    AuthorityNonceMismatch {
208        /// Authorization tuple index.
209        authorization_index: usize,
210    },
211    /// Authority account code is neither empty nor an EIP-7702 delegation.
212    InvalidAuthorityCode {
213        /// Authorization tuple index.
214        authorization_index: usize,
215    },
216}
217
218impl SetCodeTransactionValidityError {
219    /// Stable machine-readable error code.
220    #[must_use]
221    pub const fn code(self) -> &'static str {
222        match self {
223            Self::InactiveFork => "ETH_SET_CODE_VALIDITY_INACTIVE_FORK",
224            Self::ForkBeforePrague => "ETH_SET_CODE_VALIDITY_PRE_PRAGUE",
225            Self::WrongTransactionChain => "ETH_SET_CODE_VALIDITY_WRONG_TX_CHAIN",
226            Self::PriorityFeeExceedsMaxFee => "ETH_SET_CODE_VALIDITY_FEE_ORDER",
227            Self::GasLimitTooLow => "ETH_SET_CODE_VALIDITY_GAS_LIMIT",
228            Self::EmptyAuthorizationList => "ETH_SET_CODE_VALIDITY_EMPTY_AUTH_LIST",
229            Self::AuthorizationDecode(error) => error.code(),
230            Self::MissingAuthorizationAuthority { .. } => "ETH_SET_CODE_VALIDITY_MISSING_AUTHORITY",
231            Self::WrongAuthorizationChain { .. } => "ETH_SET_CODE_VALIDITY_WRONG_AUTH_CHAIN",
232            Self::AuthorizationNonceTooHigh { .. } => "ETH_SET_CODE_VALIDITY_AUTH_NONCE_TOO_HIGH",
233            Self::MissingAuthorityState { .. } => "ETH_SET_CODE_VALIDITY_MISSING_AUTHORITY_STATE",
234            Self::AuthorityNonceMismatch { .. } => "ETH_SET_CODE_VALIDITY_AUTHORITY_NONCE_MISMATCH",
235            Self::InvalidAuthorityCode { .. } => "ETH_SET_CODE_VALIDITY_INVALID_AUTHORITY_CODE",
236        }
237    }
238
239    /// Stable human-readable error message.
240    #[must_use]
241    pub const fn message(self) -> &'static str {
242        match self {
243            Self::InactiveFork => "set-code transaction fork is not active",
244            Self::ForkBeforePrague => "set-code transaction requires Prague/Pectra or later",
245            Self::WrongTransactionChain => {
246                "set-code transaction chain id does not match validation context"
247            }
248            Self::PriorityFeeExceedsMaxFee => "set-code transaction priority fee exceeds max fee",
249            Self::GasLimitTooLow => "set-code transaction gas limit is below required minimum",
250            Self::EmptyAuthorizationList => {
251                "set-code transaction authorization list must not be empty"
252            }
253            Self::AuthorizationDecode(error) => error.message(),
254            Self::MissingAuthorizationAuthority { .. } => {
255                "set-code authorization has no verified recovered authority"
256            }
257            Self::WrongAuthorizationChain { .. } => {
258                "set-code authorization chain id is not valid for this chain"
259            }
260            Self::AuthorizationNonceTooHigh { .. } => {
261                "set-code authorization nonce is too high to increment"
262            }
263            Self::MissingAuthorityState { .. } => "set-code authority account state is unavailable",
264            Self::AuthorityNonceMismatch { .. } => {
265                "set-code authority nonce does not match authorization nonce"
266            }
267            Self::InvalidAuthorityCode { .. } => {
268                "set-code authority code is neither empty nor delegated"
269            }
270        }
271    }
272
273    /// Stable high-level category for policy decisions.
274    #[must_use]
275    pub const fn category(self) -> SetCodeTransactionValidityErrorCategory {
276        match self {
277            Self::InactiveFork | Self::ForkBeforePrague | Self::WrongTransactionChain => {
278                SetCodeTransactionValidityErrorCategory::Fork
279            }
280            Self::PriorityFeeExceedsMaxFee => SetCodeTransactionValidityErrorCategory::Fee,
281            Self::GasLimitTooLow => SetCodeTransactionValidityErrorCategory::Gas,
282            Self::EmptyAuthorizationList
283            | Self::AuthorizationDecode(_)
284            | Self::MissingAuthorizationAuthority { .. }
285            | Self::WrongAuthorizationChain { .. }
286            | Self::AuthorizationNonceTooHigh { .. } => {
287                SetCodeTransactionValidityErrorCategory::Authorization
288            }
289            Self::MissingAuthorityState { .. }
290            | Self::AuthorityNonceMismatch { .. }
291            | Self::InvalidAuthorityCode { .. } => {
292                SetCodeTransactionValidityErrorCategory::AccountState
293            }
294        }
295    }
296}
297
298impl fmt::Display for SetCodeTransactionValidityError {
299    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300        formatter.write_str(self.message())
301    }
302}
303
304#[cfg(feature = "std")]
305impl std::error::Error for SetCodeTransactionValidityError {}
306
307/// Stable high-level set-code transaction validity categories.
308#[non_exhaustive]
309#[derive(Clone, Copy, Debug, Eq, PartialEq)]
310pub enum SetCodeTransactionValidityErrorCategory {
311    /// Fork or chain context rejected the transaction.
312    Fork,
313    /// Fee policy rejected the transaction.
314    Fee,
315    /// Gas policy rejected the transaction.
316    Gas,
317    /// Authorization-list policy rejected the transaction.
318    Authorization,
319    /// Authority account-state policy rejected the transaction.
320    AccountState,
321}
322
323/// Validates EIP-7702 context rules for a decoded set-code transaction.
324///
325/// This is the non-cryptographic validity gate. It expects `authorities` to be
326/// produced by validating every authorization tuple signature through the
327/// verify crate. It does not recover signatures itself and does not execute the
328/// transaction. Invalid authorization tuples are counted as skipped instead of
329/// rejecting the whole transaction, matching EIP-7702 tuple processing.
330pub fn validate_set_code_transaction_context<'a, A, S>(
331    transaction: UnvalidatedSetCodeTransaction<'a>,
332    context: SetCodeTransactionValidationContext,
333    authorities: &A,
334    accounts: &S,
335) -> Result<ValidSetCodeTransaction<'a>, SetCodeTransactionValidityError>
336where
337    A: SetCodeAuthorizationAuthorityView + ?Sized,
338    S: SetCodeAuthorityStateView + ?Sized,
339{
340    validate_outer_context(&transaction, context)?;
341    let summary = validate_authorizations(transaction, authorities, accounts);
342    Ok(ValidSetCodeTransaction::new(
343        transaction,
344        transaction.authorization_list.len(),
345        summary.applied,
346        summary.skipped,
347    ))
348}
349
350fn validate_outer_context(
351    transaction: &UnvalidatedSetCodeTransaction<'_>,
352    context: SetCodeTransactionValidationContext,
353) -> Result<(), SetCodeTransactionValidityError> {
354    if !context.fork.fork_is_active() {
355        return Err(SetCodeTransactionValidityError::InactiveFork);
356    }
357    if context.fork.fork.hardfork < Hardfork::Prague {
358        return Err(SetCodeTransactionValidityError::ForkBeforePrague);
359    }
360    if context.fork.fork.chain_id != transaction.chain_id {
361        return Err(SetCodeTransactionValidityError::WrongTransactionChain);
362    }
363    if wei_exceeds(
364        transaction.max_priority_fee_per_gas,
365        transaction.max_fee_per_gas,
366    ) {
367        return Err(SetCodeTransactionValidityError::PriorityFeeExceedsMaxFee);
368    }
369    if let Some(minimum_gas_limit) = context.minimum_gas_limit
370        && transaction.gas_limit < minimum_gas_limit
371    {
372        return Err(SetCodeTransactionValidityError::GasLimitTooLow);
373    }
374    if transaction.authorization_list.is_empty() {
375        return Err(SetCodeTransactionValidityError::EmptyAuthorizationList);
376    }
377    Ok(())
378}
379
380fn wei_exceeds(left: eth_valkyoth_primitives::Wei, right: eth_valkyoth_primitives::Wei) -> bool {
381    left.to_be_bytes() > right.to_be_bytes()
382}
383
384fn validate_authorizations<A, S>(
385    transaction: UnvalidatedSetCodeTransaction<'_>,
386    authorities: &A,
387    accounts: &S,
388) -> AuthorizationValidationSummary
389where
390    A: SetCodeAuthorizationAuthorityView + ?Sized,
391    S: SetCodeAuthorityStateView + ?Sized,
392{
393    let mut summary = AuthorizationValidationSummary::default();
394    for (authorization_index, authorization) in
395        transaction.authorization_list.authorizations().enumerate()
396    {
397        let Ok(authorization) = authorization else {
398            summary.skipped = summary.skipped.saturating_add(1);
399            continue;
400        };
401        if validate_authorization(
402            transaction,
403            authorization_index,
404            authorization,
405            authorities,
406            accounts,
407        )
408        .is_ok()
409        {
410            summary.applied = summary.applied.saturating_add(1);
411        } else {
412            summary.skipped = summary.skipped.saturating_add(1);
413        }
414    }
415    summary
416}
417
418#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
419struct AuthorizationValidationSummary {
420    applied: usize,
421    skipped: usize,
422}
423
424fn validate_authorization<A, S>(
425    transaction: UnvalidatedSetCodeTransaction<'_>,
426    authorization_index: usize,
427    authorization: SetCodeAuthorization,
428    authorities: &A,
429    accounts: &S,
430) -> Result<(), SetCodeTransactionValidityError>
431where
432    A: SetCodeAuthorizationAuthorityView + ?Sized,
433    S: SetCodeAuthorityStateView + ?Sized,
434{
435    if !authorization_chain_permits(authorization, transaction.chain_id) {
436        return Err(SetCodeTransactionValidityError::WrongAuthorizationChain {
437            authorization_index,
438        });
439    }
440    if authorization.nonce.get() == u64::MAX {
441        return Err(SetCodeTransactionValidityError::AuthorizationNonceTooHigh {
442            authorization_index,
443        });
444    }
445    let authority = authorities
446        .authority_for(authorization_index, authorization)
447        .ok_or(
448            SetCodeTransactionValidityError::MissingAuthorizationAuthority {
449                authorization_index,
450            },
451        )?;
452    let account = accounts.authority_account(authority).ok_or(
453        SetCodeTransactionValidityError::MissingAuthorityState {
454            authorization_index,
455        },
456    )?;
457    if account.nonce != authorization.nonce {
458        return Err(SetCodeTransactionValidityError::AuthorityNonceMismatch {
459            authorization_index,
460        });
461    }
462    match account.code {
463        SetCodeAuthorityCode::Empty | SetCodeAuthorityCode::Delegation { .. } => Ok(()),
464        SetCodeAuthorityCode::Other => Err(SetCodeTransactionValidityError::InvalidAuthorityCode {
465            authorization_index,
466        }),
467    }
468}
469
470fn authorization_chain_permits(authorization: SetCodeAuthorization, chain_id: ChainId) -> bool {
471    if authorization.chain_id.is_universal() {
472        return true;
473    }
474    let bytes = authorization.chain_id.to_be_bytes();
475    let Some(prefix) = bytes.get(..24) else {
476        return false;
477    };
478    let Some(suffix) = bytes.get(24..) else {
479        return false;
480    };
481    prefix.iter().all(|byte| *byte == 0) && suffix == chain_id.get().to_be_bytes()
482}
483
484#[cfg(test)]
485#[path = "validity_tests.rs"]
486mod tests;