Skip to main content

eth_valkyoth_protocol/transaction/set_code/
validity.rs

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