Skip to main content

eth_valkyoth_protocol/transaction/
access_list.rs

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