1use core::fmt;
2
3use eth_valkyoth_primitives::{Address, ChainId, Gas, Nonce};
4
5use super::{SetCodeAuthorization, SetCodeTransactionDecodeError, UnvalidatedSetCodeTransaction};
6use crate::{Hardfork, ValidationContext};
7
8pub const EIP_7702_DELEGATION_INDICATOR_PREFIX: [u8; 3] = [0xef, 0x01, 0x00];
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct SetCodeTransactionValidationContext {
14 pub fork: ValidationContext,
16 pub minimum_gas_limit: Option<Gas>,
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct SetCodeAuthorizationAuthority {
30 pub authorization_index: usize,
32 pub authority: Address,
34}
35
36pub trait SetCodeAuthorizationAuthorityView {
42 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum SetCodeAuthorityCode {
66 Empty,
68 Delegation {
74 target: Address,
76 },
77 Other,
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct SetCodeAuthorityAccount {
84 pub authority: Address,
86 pub nonce: Nonce,
88 pub code: SetCodeAuthorityCode,
90}
91
92pub trait SetCodeAuthorityStateView {
103 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#[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 #[must_use]
141 pub const fn transaction(self) -> UnvalidatedSetCodeTransaction<'a> {
142 self.transaction
143 }
144
145 #[must_use]
147 pub const fn authorization_count(self) -> usize {
148 self.authorization_count
149 }
150
151 #[must_use]
153 pub const fn applied_authorization_count(self) -> usize {
154 self.applied_authorization_count
155 }
156
157 #[must_use]
159 pub const fn skipped_authorization_count(self) -> usize {
160 self.skipped_authorization_count
161 }
162}
163
164#[non_exhaustive]
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum SetCodeTransactionValidityError {
172 InactiveFork,
174 ForkBeforePrague,
176 WrongTransactionChain,
178 PriorityFeeExceedsMaxFee,
180 GasLimitTooLow,
182 EmptyAuthorizationList,
184 AuthorizationDecode(SetCodeTransactionDecodeError),
186 MissingAuthorizationAuthority {
188 authorization_index: usize,
190 },
191 WrongAuthorizationChain {
193 authorization_index: usize,
195 },
196 AuthorizationNonceTooHigh {
198 authorization_index: usize,
200 },
201 MissingAuthorityState {
203 authorization_index: usize,
205 },
206 AuthorityNonceMismatch {
208 authorization_index: usize,
210 },
211 InvalidAuthorityCode {
213 authorization_index: usize,
215 },
216}
217
218impl SetCodeTransactionValidityError {
219 #[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 #[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 #[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#[non_exhaustive]
309#[derive(Clone, Copy, Debug, Eq, PartialEq)]
310pub enum SetCodeTransactionValidityErrorCategory {
311 Fork,
313 Fee,
315 Gas,
317 Authorization,
319 AccountState,
321}
322
323pub 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;