Skip to main content

eth_valkyoth_protocol/transaction/
blob.rs

1use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpItem, RlpList};
2use eth_valkyoth_primitives::{Address, B256, 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    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::{BlobTransactionDecodeError, BlobTransactionDecodeErrorCategory};
15
16/// EIP-4844 blob transaction type byte.
17pub const BLOB_TRANSACTION_TYPE: u8 = 0x03;
18/// Number of fields in an EIP-4844 blob transaction payload.
19pub const BLOB_TRANSACTION_FIELD_COUNT: usize = 14;
20
21const B256_BYTES: usize = 32;
22
23/// Borrowed EIP-4844 transaction decoded only into field domains.
24///
25/// This type is intentionally unvalidated: no sender recovery, signature
26/// validity, fee-order check, blob fee adequacy, KZG proof validation,
27/// data-availability validation, blob-hash version policy, account-state check,
28/// block blob-gas accounting, or fork validity is performed.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct UnvalidatedBlobTransaction<'a> {
31    /// Chain ID encoded in the signed transaction domain.
32    pub chain_id: ChainId,
33    /// Account nonce.
34    pub nonce: Nonce,
35    /// Maximum priority fee per gas in wei.
36    pub max_priority_fee_per_gas: Wei,
37    /// Maximum total fee per gas in wei.
38    pub max_fee_per_gas: Wei,
39    /// Gas limit.
40    pub gas_limit: Gas,
41    /// Call target. EIP-4844 blob transactions cannot be contract creation.
42    pub to: Address,
43    /// Transferred value in wei.
44    pub value: Wei,
45    /// Borrowed transaction input data.
46    pub input: &'a [u8],
47    /// Borrowed access list.
48    pub access_list: AccessList<'a>,
49    /// Maximum fee per blob gas in wei.
50    pub max_fee_per_blob_gas: Wei,
51    /// Borrowed list of blob versioned hashes.
52    pub blob_versioned_hashes: BlobVersionedHashes<'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/// Borrowed EIP-4844 blob versioned hash list.
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct BlobVersionedHashes<'a> {
69    list: RlpList<'a>,
70}
71
72impl<'a> BlobVersionedHashes<'a> {
73    /// Returns the number of blob versioned hashes.
74    #[must_use]
75    pub const fn len(self) -> usize {
76        self.list.item_count()
77    }
78
79    /// Returns true when the list is empty.
80    ///
81    /// EIP-4844 execution validation requires at least one blob hash. This
82    /// syntactic decoder intentionally leaves that fork-validity check to a
83    /// later validation state.
84    #[must_use]
85    pub const fn is_empty(self) -> bool {
86        self.list.is_empty()
87    }
88
89    /// Returns an iterator over blob versioned hashes.
90    ///
91    /// The transaction decoder validates every hash length before returning
92    /// this borrowed model. Iterating re-parses the same bounded RLP bytes so
93    /// callers can use zero-copy access without storing decoded hashes.
94    #[must_use]
95    pub const fn hashes(self) -> BlobVersionedHashItems<'a> {
96        BlobVersionedHashItems {
97            items: self.list.items(),
98        }
99    }
100
101    /// Returns the encoded RLP list length of the borrowed hash list.
102    pub(crate) const fn encoded_rlp_len(self) -> usize {
103        self.list.encoded_len()
104    }
105
106    /// Re-encodes the already validated borrowed hash list.
107    pub(crate) fn encode_rlp(self, output: &mut [u8]) -> Result<usize, DecodeError> {
108        eth_valkyoth_codec::encode_decoded_list(self.list, output)
109    }
110}
111
112/// Iterator over borrowed blob versioned hashes.
113#[derive(Clone, Debug)]
114pub struct BlobVersionedHashItems<'a> {
115    items: eth_valkyoth_codec::RlpListItems<'a>,
116}
117
118impl Iterator for BlobVersionedHashItems<'_> {
119    type Item = Result<B256, BlobTransactionDecodeError>;
120
121    fn next(&mut self) -> Option<Self::Item> {
122        self.items.next().map(decode_blob_versioned_hash_item)
123    }
124}
125
126impl core::iter::FusedIterator for BlobVersionedHashItems<'_> {}
127
128/// EIP-4844 transaction field identifier.
129#[non_exhaustive]
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum BlobTransactionField {
132    /// Whole typed-transaction payload.
133    Payload,
134    /// `chain_id`.
135    ChainId,
136    /// `nonce`.
137    Nonce,
138    /// `max_priority_fee_per_gas`.
139    MaxPriorityFeePerGas,
140    /// `max_fee_per_gas`.
141    MaxFeePerGas,
142    /// `gas_limit`.
143    GasLimit,
144    /// `to`.
145    To,
146    /// `value`.
147    Value,
148    /// `data`.
149    Data,
150    /// `access_list`.
151    AccessList,
152    /// `max_fee_per_blob_gas`.
153    MaxFeePerBlobGas,
154    /// `blob_versioned_hashes`.
155    BlobVersionedHashes,
156    /// `y_parity`.
157    SignatureYParity,
158    /// `r`.
159    SignatureR,
160    /// `s`.
161    SignatureS,
162}
163
164/// Decodes an EIP-4844 blob transaction into unvalidated field domains.
165pub fn decode_blob_transaction<'a>(
166    input: &'a [u8],
167    limits: DecodeLimits,
168) -> Result<UnvalidatedBlobTransaction<'a>, BlobTransactionDecodeError> {
169    match decode_transaction_envelope(input, limits)
170        .map_err(BlobTransactionDecodeError::Envelope)?
171    {
172        TransactionEnvelope::Typed(typed)
173            if typed.transaction_type.get() == BLOB_TRANSACTION_TYPE =>
174        {
175            decode_blob_payload(typed.payload, limits)
176        }
177        TransactionEnvelope::Typed(typed) => {
178            Err(BlobTransactionDecodeError::WrongTransactionType {
179                type_byte: typed.transaction_type.get(),
180            })
181        }
182        TransactionEnvelope::Legacy(_) => {
183            Err(BlobTransactionDecodeError::WrongTransactionType { type_byte: 0 })
184        }
185    }
186}
187
188fn decode_blob_payload<'a>(
189    payload: &'a [u8],
190    limits: DecodeLimits,
191) -> Result<UnvalidatedBlobTransaction<'a>, BlobTransactionDecodeError> {
192    let list = eth_valkyoth_codec::decode_rlp_list(payload, limits)
193        .map_err(|source| field_error(BlobTransactionField::Payload, source))?;
194    if list.item_count() != BLOB_TRANSACTION_FIELD_COUNT {
195        return Err(BlobTransactionDecodeError::WrongFieldCount {
196            expected: BLOB_TRANSACTION_FIELD_COUNT,
197            found: list.item_count(),
198        });
199    }
200
201    let mut fields = list.items();
202    let chain_id = decode_shared_chain_id(&mut fields, BlobTransactionField::ChainId, field_error)?;
203    let nonce = Nonce::new(decode_shared_u64_field(
204        &mut fields,
205        BlobTransactionField::Nonce,
206        field_error,
207    )?);
208    let max_priority_fee_per_gas = Wei::from_be_bytes(decode_shared_u256_field(
209        &mut fields,
210        BlobTransactionField::MaxPriorityFeePerGas,
211        field_error,
212    )?);
213    let max_fee_per_gas = Wei::from_be_bytes(decode_shared_u256_field(
214        &mut fields,
215        BlobTransactionField::MaxFeePerGas,
216        field_error,
217    )?);
218    let gas_limit = Gas::new(decode_shared_u64_field(
219        &mut fields,
220        BlobTransactionField::GasLimit,
221        field_error,
222    )?);
223    let to = decode_required_address(
224        next_shared_scalar(&mut fields, BlobTransactionField::To, field_error)?,
225        |found| BlobTransactionDecodeError::InvalidToLength { found },
226    )?;
227    let value = Wei::from_be_bytes(decode_shared_u256_field(
228        &mut fields,
229        BlobTransactionField::Value,
230        field_error,
231    )?);
232    let input = next_shared_scalar(&mut fields, BlobTransactionField::Data, field_error)?.payload();
233    limits
234        .check_single_allocation_limit(input.len())
235        .map_err(|source| field_error(BlobTransactionField::Data, source))?;
236    let access_list = decode_access_list(next_shared_list(
237        &mut fields,
238        BlobTransactionField::AccessList,
239        field_error,
240    )?)
241    .map_err(map_access_list_error)?;
242    let max_fee_per_blob_gas = Wei::from_be_bytes(decode_shared_u256_field(
243        &mut fields,
244        BlobTransactionField::MaxFeePerBlobGas,
245        field_error,
246    )?);
247    let blob_versioned_hashes = decode_blob_versioned_hashes(next_shared_list(
248        &mut fields,
249        BlobTransactionField::BlobVersionedHashes,
250        field_error,
251    )?)?;
252    let y_parity = SignatureYParity::try_new(decode_shared_u64_field(
253        &mut fields,
254        BlobTransactionField::SignatureYParity,
255        field_error,
256    )?)
257    .map_err(|error| BlobTransactionDecodeError::InvalidYParity {
258        value: error.value(),
259    })?;
260    let r = decode_shared_u256_field(&mut fields, BlobTransactionField::SignatureR, field_error)?;
261    let s = decode_shared_u256_field(&mut fields, BlobTransactionField::SignatureS, field_error)?;
262
263    Ok(UnvalidatedBlobTransaction {
264        chain_id,
265        nonce,
266        max_priority_fee_per_gas,
267        max_fee_per_gas,
268        gas_limit,
269        to,
270        value,
271        input,
272        access_list,
273        max_fee_per_blob_gas,
274        blob_versioned_hashes,
275        y_parity,
276        r,
277        s,
278    })
279}
280
281fn decode_blob_versioned_hashes(
282    list: RlpList<'_>,
283) -> Result<BlobVersionedHashes<'_>, BlobTransactionDecodeError> {
284    for item in list.items() {
285        let _ = decode_blob_versioned_hash_item(item)?;
286    }
287    Ok(BlobVersionedHashes { list })
288}
289
290fn decode_blob_versioned_hash_item(
291    item: Result<RlpItem<'_>, DecodeError>,
292) -> Result<B256, BlobTransactionDecodeError> {
293    let item =
294        item.map_err(|source| field_error(BlobTransactionField::BlobVersionedHashes, source))?;
295    let RlpItem::Scalar(scalar) = item else {
296        return Err(field_error(
297            BlobTransactionField::BlobVersionedHashes,
298            DecodeError::UnexpectedList,
299        ));
300    };
301    let found = scalar.payload().len();
302    let bytes: [u8; B256_BYTES] = scalar
303        .payload()
304        .try_into()
305        .map_err(|_| BlobTransactionDecodeError::InvalidBlobVersionedHashLength { found })?;
306    Ok(B256::from_bytes(bytes))
307}
308
309const fn field_error(
310    field: BlobTransactionField,
311    source: DecodeError,
312) -> BlobTransactionDecodeError {
313    BlobTransactionDecodeError::FieldDecode { field, source }
314}
315
316const fn map_access_list_error(error: AccessListDecodeError) -> BlobTransactionDecodeError {
317    match error {
318        AccessListDecodeError::FieldDecode(source) => {
319            field_error(BlobTransactionField::AccessList, source)
320        }
321        AccessListDecodeError::InvalidAccessListEntryFieldCount { found } => {
322            BlobTransactionDecodeError::InvalidAccessListEntryFieldCount { found }
323        }
324        AccessListDecodeError::InvalidAccessListAddressLength { found } => {
325            BlobTransactionDecodeError::InvalidAccessListAddressLength { found }
326        }
327        AccessListDecodeError::InvalidStorageKeyLength { found } => {
328            BlobTransactionDecodeError::InvalidStorageKeyLength { found }
329        }
330    }
331}