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