Skip to main content

eth_valkyoth_protocol/transaction/
set_code.rs

1use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpList, RlpScalar};
2use eth_valkyoth_primitives::{Address, ChainId, Gas, Nonce, Wei};
3
4use super::access_list::{AccessListDecodeError, decode_access_list};
5use super::{AccessList, SignatureYParity, TransactionEnvelope, decode_transaction_envelope};
6use crate::transaction::fields::{
7    ADDRESS_BYTES, decode_chain_id as decode_shared_chain_id, decode_required_address,
8    decode_u64_field as decode_shared_u64_field, decode_u256_field as decode_shared_u256_field,
9    next_list as next_shared_list, next_scalar as next_shared_scalar,
10};
11
12mod error;
13mod validity;
14
15pub use error::{
16    SetCodeAuthorizationField, SetCodeTransactionDecodeError, SetCodeTransactionDecodeErrorCategory,
17};
18pub use validity::*;
19
20/// EIP-7702 set-code transaction type byte.
21pub const SET_CODE_TRANSACTION_TYPE: u8 = 0x04;
22/// Number of fields in an EIP-7702 set-code transaction payload.
23pub const SET_CODE_TRANSACTION_FIELD_COUNT: usize = 13;
24/// Number of fields in an EIP-7702 authorization tuple.
25pub const SET_CODE_AUTHORIZATION_FIELD_COUNT: usize = 6;
26
27/// Borrowed EIP-7702 set-code transaction decoded only into field domains.
28///
29/// This type is intentionally unvalidated: no sender recovery, signature
30/// validity, fee-order check, authorization signature validation, non-empty
31/// authorization-list policy, account-state check, delegation-indicator policy,
32/// or fork validity is performed.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct UnvalidatedSetCodeTransaction<'a> {
35    /// Chain ID encoded in the signed transaction domain.
36    pub chain_id: ChainId,
37    /// Account nonce.
38    pub nonce: Nonce,
39    /// Maximum priority fee per gas in wei.
40    pub max_priority_fee_per_gas: Wei,
41    /// Maximum total fee per gas in wei.
42    pub max_fee_per_gas: Wei,
43    /// Gas limit.
44    pub gas_limit: Gas,
45    /// Call target. EIP-7702 set-code transactions cannot be contract creation.
46    pub to: Address,
47    /// Transferred value in wei.
48    pub value: Wei,
49    /// Borrowed transaction input data.
50    pub input: &'a [u8],
51    /// Borrowed access list.
52    pub access_list: AccessList<'a>,
53    /// Borrowed authorization list.
54    pub authorization_list: SetCodeAuthorizationList<'a>,
55    /// Signature y parity.
56    pub y_parity: SignatureYParity,
57    /// Raw canonical U256 signature `r` value.
58    ///
59    /// This is not checked for secp256k1 scalar validity.
60    pub r: [u8; 32],
61    /// Raw canonical U256 signature `s` value.
62    ///
63    /// This is not checked against the EIP-2 low-s bound or secp256k1 scalar
64    /// validity.
65    pub s: [u8; 32],
66}
67
68/// EIP-7702 authorization chain ID.
69///
70/// Authorization chain IDs may be zero to allow a chain-agnostic
71/// authorization. The outer transaction chain ID still uses [`ChainId`] and
72/// remains nonzero.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct SetCodeAuthorizationChainId {
75    bytes: [u8; 32],
76}
77
78impl SetCodeAuthorizationChainId {
79    /// Creates an authorization chain ID from canonical 256-bit bytes.
80    #[must_use]
81    pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
82        Self { bytes }
83    }
84
85    /// Returns the canonical 256-bit big-endian bytes.
86    #[must_use]
87    pub const fn to_be_bytes(self) -> [u8; 32] {
88        self.bytes
89    }
90
91    /// Returns true when the authorization is chain-agnostic.
92    #[must_use]
93    pub fn is_universal(self) -> bool {
94        self.bytes.iter().all(|byte| *byte == 0)
95    }
96}
97
98/// Borrowed EIP-7702 authorization list.
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub struct SetCodeAuthorizationList<'a> {
101    list: RlpList<'a>,
102}
103
104impl<'a> SetCodeAuthorizationList<'a> {
105    /// Returns the number of authorization tuples.
106    #[must_use]
107    pub const fn len(self) -> usize {
108        self.list.item_count()
109    }
110
111    /// Returns true when the list is empty.
112    ///
113    /// EIP-7702 execution validation requires at least one authorization
114    /// tuple. This syntactic decoder intentionally leaves that fork-validity
115    /// check to a later validation state.
116    #[must_use]
117    pub const fn is_empty(self) -> bool {
118        self.list.is_empty()
119    }
120
121    /// Returns an iterator over authorization tuples.
122    ///
123    /// The transaction decoder validates every tuple before returning this
124    /// borrowed model. Iterating re-parses the same bounded RLP bytes so callers
125    /// can use zero-copy access without storing decoded authorizations.
126    #[must_use]
127    pub const fn authorizations(self) -> SetCodeAuthorizationItems<'a> {
128        SetCodeAuthorizationItems {
129            items: self.list.items(),
130        }
131    }
132
133    /// Returns the encoded RLP list length of the borrowed authorization list.
134    pub(crate) const fn encoded_rlp_len(self) -> usize {
135        self.list.encoded_len()
136    }
137
138    /// Re-encodes the already validated borrowed authorization list.
139    pub(crate) fn encode_rlp(self, output: &mut [u8]) -> Result<usize, DecodeError> {
140        eth_valkyoth_codec::encode_decoded_list(self.list, output)
141    }
142}
143
144/// Borrowed EIP-7702 authorization tuple.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub struct SetCodeAuthorization {
147    /// Chain ID for this authorization. Zero means chain-agnostic.
148    pub chain_id: SetCodeAuthorizationChainId,
149    /// Delegation target address.
150    pub address: Address,
151    /// Authorizing account nonce.
152    pub nonce: Nonce,
153    /// Authorization signature y parity.
154    pub y_parity: SignatureYParity,
155    /// Raw canonical U256 authorization signature `r` value.
156    pub r: [u8; 32],
157    /// Raw canonical U256 authorization signature `s` value.
158    pub s: [u8; 32],
159}
160
161/// Iterator over borrowed EIP-7702 authorization tuples.
162#[derive(Clone, Debug)]
163pub struct SetCodeAuthorizationItems<'a> {
164    items: eth_valkyoth_codec::RlpListItems<'a>,
165}
166
167impl Iterator for SetCodeAuthorizationItems<'_> {
168    type Item = Result<SetCodeAuthorization, SetCodeTransactionDecodeError>;
169
170    fn next(&mut self) -> Option<Self::Item> {
171        self.items
172            .next()
173            .map(|item| decode_authorization_item(item).map_err(map_authorization_error))
174    }
175}
176
177impl core::iter::FusedIterator for SetCodeAuthorizationItems<'_> {}
178
179/// EIP-7702 transaction field identifier.
180#[non_exhaustive]
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum SetCodeTransactionField {
183    /// Whole typed-transaction payload.
184    Payload,
185    /// `chain_id`.
186    ChainId,
187    /// `nonce`.
188    Nonce,
189    /// `max_priority_fee_per_gas`.
190    MaxPriorityFeePerGas,
191    /// `max_fee_per_gas`.
192    MaxFeePerGas,
193    /// `gas_limit`.
194    GasLimit,
195    /// `destination`.
196    To,
197    /// `value`.
198    Value,
199    /// `data`.
200    Data,
201    /// `access_list`.
202    AccessList,
203    /// `authorization_list`.
204    AuthorizationList,
205    /// `signature_y_parity`.
206    SignatureYParity,
207    /// `signature_r`.
208    SignatureR,
209    /// `signature_s`.
210    SignatureS,
211}
212
213/// Decodes an EIP-7702 set-code transaction into unvalidated field domains.
214pub fn decode_set_code_transaction<'a>(
215    input: &'a [u8],
216    limits: DecodeLimits,
217) -> Result<UnvalidatedSetCodeTransaction<'a>, SetCodeTransactionDecodeError> {
218    match decode_transaction_envelope(input, limits)
219        .map_err(SetCodeTransactionDecodeError::Envelope)?
220    {
221        TransactionEnvelope::Typed(typed)
222            if typed.transaction_type.get() == SET_CODE_TRANSACTION_TYPE =>
223        {
224            decode_set_code_payload(typed.payload, limits)
225        }
226        TransactionEnvelope::Typed(typed) => {
227            Err(SetCodeTransactionDecodeError::WrongTransactionType {
228                type_byte: typed.transaction_type.get(),
229            })
230        }
231        TransactionEnvelope::Legacy(_) => {
232            Err(SetCodeTransactionDecodeError::WrongTransactionType { type_byte: 0 })
233        }
234    }
235}
236
237fn decode_set_code_payload<'a>(
238    payload: &'a [u8],
239    limits: DecodeLimits,
240) -> Result<UnvalidatedSetCodeTransaction<'a>, SetCodeTransactionDecodeError> {
241    let list = eth_valkyoth_codec::decode_rlp_list(payload, limits)
242        .map_err(|source| field_error(SetCodeTransactionField::Payload, source))?;
243    if list.item_count() != SET_CODE_TRANSACTION_FIELD_COUNT {
244        return Err(SetCodeTransactionDecodeError::WrongFieldCount {
245            expected: SET_CODE_TRANSACTION_FIELD_COUNT,
246            found: list.item_count(),
247        });
248    }
249
250    let mut fields = list.items();
251    let chain_id =
252        decode_shared_chain_id(&mut fields, SetCodeTransactionField::ChainId, field_error)?;
253    let nonce = Nonce::new(decode_shared_u64_field(
254        &mut fields,
255        SetCodeTransactionField::Nonce,
256        field_error,
257    )?);
258    let max_priority_fee_per_gas = Wei::from_be_bytes(decode_shared_u256_field(
259        &mut fields,
260        SetCodeTransactionField::MaxPriorityFeePerGas,
261        field_error,
262    )?);
263    let max_fee_per_gas = Wei::from_be_bytes(decode_shared_u256_field(
264        &mut fields,
265        SetCodeTransactionField::MaxFeePerGas,
266        field_error,
267    )?);
268    let gas_limit = Gas::new(decode_shared_u64_field(
269        &mut fields,
270        SetCodeTransactionField::GasLimit,
271        field_error,
272    )?);
273    let to = decode_required_address(
274        next_shared_scalar(&mut fields, SetCodeTransactionField::To, field_error)?,
275        |found| SetCodeTransactionDecodeError::InvalidToLength { found },
276    )?;
277    let value = Wei::from_be_bytes(decode_shared_u256_field(
278        &mut fields,
279        SetCodeTransactionField::Value,
280        field_error,
281    )?);
282    let input =
283        next_shared_scalar(&mut fields, SetCodeTransactionField::Data, field_error)?.payload();
284    limits
285        .check_single_allocation_limit(input.len())
286        .map_err(|source| field_error(SetCodeTransactionField::Data, source))?;
287    let access_list = decode_access_list(next_shared_list(
288        &mut fields,
289        SetCodeTransactionField::AccessList,
290        field_error,
291    )?)
292    .map_err(map_access_list_error)?;
293    let authorization_list = decode_authorization_list(next_shared_list(
294        &mut fields,
295        SetCodeTransactionField::AuthorizationList,
296        field_error,
297    )?)
298    .map_err(map_authorization_error)?;
299    let y_parity = SignatureYParity::try_new(decode_shared_u64_field(
300        &mut fields,
301        SetCodeTransactionField::SignatureYParity,
302        field_error,
303    )?)
304    .map_err(|error| SetCodeTransactionDecodeError::InvalidYParity {
305        value: error.value(),
306    })?;
307    let r = decode_shared_u256_field(
308        &mut fields,
309        SetCodeTransactionField::SignatureR,
310        field_error,
311    )?;
312    let s = decode_shared_u256_field(
313        &mut fields,
314        SetCodeTransactionField::SignatureS,
315        field_error,
316    )?;
317
318    Ok(UnvalidatedSetCodeTransaction {
319        chain_id,
320        nonce,
321        max_priority_fee_per_gas,
322        max_fee_per_gas,
323        gas_limit,
324        to,
325        value,
326        input,
327        access_list,
328        authorization_list,
329        y_parity,
330        r,
331        s,
332    })
333}
334
335fn decode_authorization_list(
336    list: RlpList<'_>,
337) -> Result<SetCodeAuthorizationList<'_>, SetCodeAuthorizationDecodeError> {
338    for item in list.items() {
339        let _ = decode_authorization_item(item)?;
340    }
341    Ok(SetCodeAuthorizationList { list })
342}
343
344fn decode_authorization_item(
345    item: Result<RlpItem<'_>, DecodeError>,
346) -> Result<SetCodeAuthorization, SetCodeAuthorizationDecodeError> {
347    let item = item.map_err(SetCodeAuthorizationDecodeError::TupleDecode)?;
348    let RlpItem::List(list) = item else {
349        return Err(SetCodeAuthorizationDecodeError::TupleDecode(
350            DecodeError::UnexpectedScalar,
351        ));
352    };
353    if list.item_count() != SET_CODE_AUTHORIZATION_FIELD_COUNT {
354        return Err(
355            SetCodeAuthorizationDecodeError::InvalidAuthorizationFieldCount {
356                found: list.item_count(),
357            },
358        );
359    }
360
361    let mut fields = list.items();
362    let chain_id = decode_authorization_chain_id(next_shared_scalar(
363        &mut fields,
364        SetCodeTransactionField::AuthorizationList,
365        |_, source| auth_field_error(SetCodeAuthorizationField::ChainId, source),
366    )?)?;
367    let address = decode_authorization_address(next_shared_scalar(
368        &mut fields,
369        SetCodeTransactionField::AuthorizationList,
370        |_, source| auth_field_error(SetCodeAuthorizationField::Address, source),
371    )?)?;
372    let nonce = Nonce::new(decode_shared_u64_field(
373        &mut fields,
374        SetCodeTransactionField::AuthorizationList,
375        |_, source| auth_field_error(SetCodeAuthorizationField::Nonce, source),
376    )?);
377    let y_parity = SignatureYParity::try_new(decode_shared_u64_field(
378        &mut fields,
379        SetCodeTransactionField::AuthorizationList,
380        |_, source| auth_field_error(SetCodeAuthorizationField::YParity, source),
381    )?)
382    .map_err(
383        |error| SetCodeAuthorizationDecodeError::InvalidAuthorizationYParity {
384            value: error.value(),
385        },
386    )?;
387    let r = decode_shared_u256_field(
388        &mut fields,
389        SetCodeTransactionField::AuthorizationList,
390        |_, source| auth_field_error(SetCodeAuthorizationField::R, source),
391    )?;
392    let s = decode_shared_u256_field(
393        &mut fields,
394        SetCodeTransactionField::AuthorizationList,
395        |_, source| auth_field_error(SetCodeAuthorizationField::S, source),
396    )?;
397
398    Ok(SetCodeAuthorization {
399        chain_id,
400        address,
401        nonce,
402        y_parity,
403        r,
404        s,
405    })
406}
407
408fn decode_authorization_chain_id(
409    scalar: RlpScalar<'_>,
410) -> Result<SetCodeAuthorizationChainId, SetCodeAuthorizationDecodeError> {
411    let bytes = RlpInteger::try_from_scalar(scalar)
412        .and_then(RlpInteger::to_be_bytes32)
413        .map_err(|source| auth_field_error(SetCodeAuthorizationField::ChainId, source))?;
414    Ok(SetCodeAuthorizationChainId::from_be_bytes(bytes))
415}
416
417fn decode_authorization_address(
418    scalar: RlpScalar<'_>,
419) -> Result<Address, SetCodeAuthorizationDecodeError> {
420    let found = scalar.payload().len();
421    let bytes: [u8; ADDRESS_BYTES] = scalar.payload().try_into().map_err(|_| {
422        SetCodeAuthorizationDecodeError::InvalidAuthorizationAddressLength { found }
423    })?;
424    Ok(Address::from_bytes(bytes))
425}
426
427const fn auth_field_error(
428    field: SetCodeAuthorizationField,
429    source: DecodeError,
430) -> SetCodeAuthorizationDecodeError {
431    SetCodeAuthorizationDecodeError::FieldDecode { field, source }
432}
433
434const fn field_error(
435    field: SetCodeTransactionField,
436    source: DecodeError,
437) -> SetCodeTransactionDecodeError {
438    SetCodeTransactionDecodeError::FieldDecode { field, source }
439}
440
441const fn map_access_list_error(error: AccessListDecodeError) -> SetCodeTransactionDecodeError {
442    match error {
443        AccessListDecodeError::FieldDecode(source) => {
444            field_error(SetCodeTransactionField::AccessList, source)
445        }
446        AccessListDecodeError::InvalidAccessListEntryFieldCount { found } => {
447            SetCodeTransactionDecodeError::InvalidAccessListEntryFieldCount { found }
448        }
449        AccessListDecodeError::InvalidAccessListAddressLength { found } => {
450            SetCodeTransactionDecodeError::InvalidAccessListAddressLength { found }
451        }
452        AccessListDecodeError::InvalidStorageKeyLength { found } => {
453            SetCodeTransactionDecodeError::InvalidStorageKeyLength { found }
454        }
455    }
456}
457
458#[derive(Clone, Copy, Debug, Eq, PartialEq)]
459enum SetCodeAuthorizationDecodeError {
460    TupleDecode(DecodeError),
461    FieldDecode {
462        field: SetCodeAuthorizationField,
463        source: DecodeError,
464    },
465    InvalidAuthorizationFieldCount {
466        found: usize,
467    },
468    InvalidAuthorizationAddressLength {
469        found: usize,
470    },
471    InvalidAuthorizationYParity {
472        value: u64,
473    },
474}
475
476const fn map_authorization_error(
477    error: SetCodeAuthorizationDecodeError,
478) -> SetCodeTransactionDecodeError {
479    match error {
480        SetCodeAuthorizationDecodeError::TupleDecode(source) => {
481            field_error(SetCodeTransactionField::AuthorizationList, source)
482        }
483        SetCodeAuthorizationDecodeError::FieldDecode { field, source } => {
484            SetCodeTransactionDecodeError::AuthorizationFieldDecode { field, source }
485        }
486        SetCodeAuthorizationDecodeError::InvalidAuthorizationFieldCount { found } => {
487            SetCodeTransactionDecodeError::InvalidAuthorizationFieldCount { found }
488        }
489        SetCodeAuthorizationDecodeError::InvalidAuthorizationAddressLength { found } => {
490            SetCodeTransactionDecodeError::InvalidAuthorizationAddressLength { found }
491        }
492        SetCodeAuthorizationDecodeError::InvalidAuthorizationYParity { value } => {
493            SetCodeTransactionDecodeError::InvalidAuthorizationYParity { value }
494        }
495    }
496}