Skip to main content

eth_valkyoth_protocol/transaction/
encode.rs

1use eth_valkyoth_codec::{
2    DecodeError, checked_len_add, encode_rlp_integer, encode_rlp_list_header, encode_rlp_scalar,
3    encoded_rlp_integer_len, encoded_rlp_list_header_len, encoded_rlp_scalar_len,
4};
5use eth_valkyoth_primitives::Address;
6
7use super::access_list::ACCESS_LIST_TRANSACTION_TYPE;
8use super::blob::BLOB_TRANSACTION_TYPE;
9use super::dynamic_fee::DYNAMIC_FEE_TRANSACTION_TYPE;
10use super::{
11    AccessListTransactionTo, BlobVersionedHashes, UnvalidatedAccessListTransaction,
12    UnvalidatedBlobTransaction, UnvalidatedDynamicFeeTransaction, UnvalidatedLegacyTransaction,
13};
14use crate::transaction::LegacyTransactionTo;
15
16mod error;
17#[cfg(test)]
18mod tests;
19
20pub use error::{TransactionEncodeError, TransactionEncodeErrorCategory};
21
22const U64_BYTES: usize = 8;
23const U256_BYTES: usize = 32;
24
25/// Supported unvalidated transaction domains accepted by the encoder.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum UnvalidatedTransaction<'a> {
28    /// Legacy transaction.
29    Legacy(UnvalidatedLegacyTransaction<'a>),
30    /// EIP-2930 access-list transaction.
31    AccessList(UnvalidatedAccessListTransaction<'a>),
32    /// EIP-1559 dynamic-fee transaction.
33    DynamicFee(UnvalidatedDynamicFeeTransaction<'a>),
34    /// EIP-4844 blob transaction.
35    Blob(UnvalidatedBlobTransaction<'a>),
36}
37
38/// Returns the canonical encoded transaction envelope length.
39pub fn encoded_transaction_len(
40    transaction: UnvalidatedTransaction<'_>,
41) -> Result<usize, TransactionEncodeError> {
42    match transaction {
43        UnvalidatedTransaction::Legacy(tx) => encoded_legacy_transaction_len(&tx),
44        UnvalidatedTransaction::AccessList(tx) => encoded_access_list_transaction_len(&tx),
45        UnvalidatedTransaction::DynamicFee(tx) => encoded_dynamic_fee_transaction_len(&tx),
46        UnvalidatedTransaction::Blob(tx) => encoded_blob_transaction_len(&tx),
47    }
48}
49
50/// Canonically encodes an admitted unvalidated transaction envelope.
51///
52/// Unknown typed transaction payloads are intentionally not accepted here:
53/// callers that need lossless forwarding of unsupported types should carry the
54/// original bytes instead of constructing a typed field model this crate cannot
55/// validate or re-encode.
56pub fn encode_transaction(
57    transaction: UnvalidatedTransaction<'_>,
58    output: &mut [u8],
59) -> Result<usize, TransactionEncodeError> {
60    match transaction {
61        UnvalidatedTransaction::Legacy(tx) => encode_legacy_transaction(&tx, output),
62        UnvalidatedTransaction::AccessList(tx) => encode_access_list_transaction(&tx, output),
63        UnvalidatedTransaction::DynamicFee(tx) => encode_dynamic_fee_transaction(&tx, output),
64        UnvalidatedTransaction::Blob(tx) => encode_blob_transaction(&tx, output),
65    }
66}
67
68/// Returns the canonical encoded legacy transaction length.
69pub fn encoded_legacy_transaction_len(
70    transaction: &UnvalidatedLegacyTransaction<'_>,
71) -> Result<usize, TransactionEncodeError> {
72    encoded_list_envelope_len(legacy_payload_len(transaction)?).map_err(Into::into)
73}
74
75/// Canonically encodes a legacy transaction.
76pub fn encode_legacy_transaction(
77    transaction: &UnvalidatedLegacyTransaction<'_>,
78    output: &mut [u8],
79) -> Result<usize, TransactionEncodeError> {
80    let payload_len = legacy_payload_len(transaction)?;
81    encode_list_envelope(payload_len, output, |fields| {
82        write_u64(transaction.nonce.get(), fields)?;
83        write_wei(transaction.gas_price.to_be_bytes(), fields)?;
84        write_u64(transaction.gas_limit.get(), fields)?;
85        write_legacy_to(transaction.to, fields)?;
86        write_wei(transaction.value.to_be_bytes(), fields)?;
87        write_scalar(transaction.input, fields)?;
88        write_u256(transaction.v, fields)?;
89        write_u256(transaction.r, fields)?;
90        write_u256(transaction.s, fields)
91    })
92    .map_err(Into::into)
93}
94
95/// Returns the canonical encoded EIP-2930 transaction length.
96pub fn encoded_access_list_transaction_len(
97    transaction: &UnvalidatedAccessListTransaction<'_>,
98) -> Result<usize, TransactionEncodeError> {
99    encoded_typed_envelope_len(access_list_payload_len(transaction)?).map_err(Into::into)
100}
101
102/// Canonically encodes an EIP-2930 access-list transaction.
103pub fn encode_access_list_transaction(
104    transaction: &UnvalidatedAccessListTransaction<'_>,
105    output: &mut [u8],
106) -> Result<usize, TransactionEncodeError> {
107    let payload_len = access_list_payload_len(transaction)?;
108    encode_typed_envelope(
109        ACCESS_LIST_TRANSACTION_TYPE,
110        payload_len,
111        output,
112        |fields| {
113            write_u64(transaction.chain_id.get(), fields)?;
114            write_u64(transaction.nonce.get(), fields)?;
115            write_wei(transaction.gas_price.to_be_bytes(), fields)?;
116            write_u64(transaction.gas_limit.get(), fields)?;
117            write_transaction_to(transaction.to, fields)?;
118            write_wei(transaction.value.to_be_bytes(), fields)?;
119            write_scalar(transaction.input, fields)?;
120            write_access_list(transaction.access_list, fields)?;
121            write_u64(u64::from(transaction.y_parity.get()), fields)?;
122            write_u256(transaction.r, fields)?;
123            write_u256(transaction.s, fields)
124        },
125    )
126    .map_err(Into::into)
127}
128
129/// Returns the canonical encoded EIP-1559 transaction length.
130pub fn encoded_dynamic_fee_transaction_len(
131    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
132) -> Result<usize, TransactionEncodeError> {
133    encoded_typed_envelope_len(dynamic_fee_payload_len(transaction)?).map_err(Into::into)
134}
135
136/// Canonically encodes an EIP-1559 dynamic-fee transaction.
137pub fn encode_dynamic_fee_transaction(
138    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
139    output: &mut [u8],
140) -> Result<usize, TransactionEncodeError> {
141    let payload_len = dynamic_fee_payload_len(transaction)?;
142    encode_typed_envelope(
143        DYNAMIC_FEE_TRANSACTION_TYPE,
144        payload_len,
145        output,
146        |fields| {
147            write_u64(transaction.chain_id.get(), fields)?;
148            write_u64(transaction.nonce.get(), fields)?;
149            write_wei(transaction.max_priority_fee_per_gas.to_be_bytes(), fields)?;
150            write_wei(transaction.max_fee_per_gas.to_be_bytes(), fields)?;
151            write_u64(transaction.gas_limit.get(), fields)?;
152            write_transaction_to(transaction.to, fields)?;
153            write_wei(transaction.value.to_be_bytes(), fields)?;
154            write_scalar(transaction.input, fields)?;
155            write_access_list(transaction.access_list, fields)?;
156            write_u64(u64::from(transaction.y_parity.get()), fields)?;
157            write_u256(transaction.r, fields)?;
158            write_u256(transaction.s, fields)
159        },
160    )
161    .map_err(Into::into)
162}
163
164/// Returns the canonical encoded EIP-4844 transaction length.
165pub fn encoded_blob_transaction_len(
166    transaction: &UnvalidatedBlobTransaction<'_>,
167) -> Result<usize, TransactionEncodeError> {
168    encoded_typed_envelope_len(blob_payload_len(transaction)?).map_err(Into::into)
169}
170
171/// Canonically encodes an EIP-4844 blob transaction.
172pub fn encode_blob_transaction(
173    transaction: &UnvalidatedBlobTransaction<'_>,
174    output: &mut [u8],
175) -> Result<usize, TransactionEncodeError> {
176    let payload_len = blob_payload_len(transaction)?;
177    encode_typed_envelope(BLOB_TRANSACTION_TYPE, payload_len, output, |fields| {
178        write_u64(transaction.chain_id.get(), fields)?;
179        write_u64(transaction.nonce.get(), fields)?;
180        write_wei(transaction.max_priority_fee_per_gas.to_be_bytes(), fields)?;
181        write_wei(transaction.max_fee_per_gas.to_be_bytes(), fields)?;
182        write_u64(transaction.gas_limit.get(), fields)?;
183        write_address(transaction.to, fields)?;
184        write_wei(transaction.value.to_be_bytes(), fields)?;
185        write_scalar(transaction.input, fields)?;
186        write_access_list(transaction.access_list, fields)?;
187        write_wei(transaction.max_fee_per_blob_gas.to_be_bytes(), fields)?;
188        write_blob_hashes(transaction.blob_versioned_hashes, fields)?;
189        write_u64(u64::from(transaction.y_parity.get()), fields)?;
190        write_u256(transaction.r, fields)?;
191        write_u256(transaction.s, fields)
192    })
193    .map_err(Into::into)
194}
195
196fn legacy_payload_len(tx: &UnvalidatedLegacyTransaction<'_>) -> Result<usize, DecodeError> {
197    sum_lengths(&[
198        encoded_u64_len(tx.nonce.get())?,
199        encoded_u256_len(tx.gas_price.to_be_bytes())?,
200        encoded_u64_len(tx.gas_limit.get())?,
201        encoded_legacy_to_len(tx.to)?,
202        encoded_u256_len(tx.value.to_be_bytes())?,
203        encoded_rlp_scalar_len(tx.input)?,
204        encoded_u256_len(tx.v)?,
205        encoded_u256_len(tx.r)?,
206        encoded_u256_len(tx.s)?,
207    ])
208}
209
210fn access_list_payload_len(
211    tx: &UnvalidatedAccessListTransaction<'_>,
212) -> Result<usize, DecodeError> {
213    sum_lengths(&[
214        encoded_u64_len(tx.chain_id.get())?,
215        encoded_u64_len(tx.nonce.get())?,
216        encoded_u256_len(tx.gas_price.to_be_bytes())?,
217        encoded_u64_len(tx.gas_limit.get())?,
218        encoded_access_list_to_len(tx.to)?,
219        encoded_u256_len(tx.value.to_be_bytes())?,
220        encoded_rlp_scalar_len(tx.input)?,
221        tx.access_list.encoded_rlp_len(),
222        encoded_u64_len(u64::from(tx.y_parity.get()))?,
223        encoded_u256_len(tx.r)?,
224        encoded_u256_len(tx.s)?,
225    ])
226}
227
228fn dynamic_fee_payload_len(
229    tx: &UnvalidatedDynamicFeeTransaction<'_>,
230) -> Result<usize, DecodeError> {
231    sum_lengths(&[
232        encoded_u64_len(tx.chain_id.get())?,
233        encoded_u64_len(tx.nonce.get())?,
234        encoded_u256_len(tx.max_priority_fee_per_gas.to_be_bytes())?,
235        encoded_u256_len(tx.max_fee_per_gas.to_be_bytes())?,
236        encoded_u64_len(tx.gas_limit.get())?,
237        encoded_access_list_to_len(tx.to)?,
238        encoded_u256_len(tx.value.to_be_bytes())?,
239        encoded_rlp_scalar_len(tx.input)?,
240        tx.access_list.encoded_rlp_len(),
241        encoded_u64_len(u64::from(tx.y_parity.get()))?,
242        encoded_u256_len(tx.r)?,
243        encoded_u256_len(tx.s)?,
244    ])
245}
246
247fn blob_payload_len(tx: &UnvalidatedBlobTransaction<'_>) -> Result<usize, DecodeError> {
248    sum_lengths(&[
249        encoded_u64_len(tx.chain_id.get())?,
250        encoded_u64_len(tx.nonce.get())?,
251        encoded_u256_len(tx.max_priority_fee_per_gas.to_be_bytes())?,
252        encoded_u256_len(tx.max_fee_per_gas.to_be_bytes())?,
253        encoded_u64_len(tx.gas_limit.get())?,
254        encoded_rlp_scalar_len(&tx.to.to_bytes())?,
255        encoded_u256_len(tx.value.to_be_bytes())?,
256        encoded_rlp_scalar_len(tx.input)?,
257        tx.access_list.encoded_rlp_len(),
258        encoded_u256_len(tx.max_fee_per_blob_gas.to_be_bytes())?,
259        tx.blob_versioned_hashes.encoded_rlp_len(),
260        encoded_u64_len(u64::from(tx.y_parity.get()))?,
261        encoded_u256_len(tx.r)?,
262        encoded_u256_len(tx.s)?,
263    ])
264}
265
266pub(super) fn encoded_list_envelope_len(payload_len: usize) -> Result<usize, DecodeError> {
267    checked_len_add(encoded_rlp_list_header_len(payload_len)?, payload_len)
268}
269
270pub(super) fn encoded_typed_envelope_len(payload_len: usize) -> Result<usize, DecodeError> {
271    checked_len_add(1, encoded_list_envelope_len(payload_len)?)
272}
273
274pub(super) fn encode_list_envelope(
275    payload_len: usize,
276    output: &mut [u8],
277    write_fields: impl FnOnce(&mut FieldWriter<'_>) -> Result<(), DecodeError>,
278) -> Result<usize, DecodeError> {
279    let total_len = encoded_list_envelope_len(payload_len)?;
280    if output.len() < total_len {
281        return Err(DecodeError::OffsetOutOfBounds);
282    }
283    let header_len = encoded_rlp_list_header_len(payload_len)?;
284    let fields = output
285        .get_mut(header_len..total_len)
286        .ok_or(DecodeError::OffsetOutOfBounds)?;
287    let mut writer = FieldWriter::new(fields);
288    write_fields(&mut writer)?;
289    writer.finish(payload_len)?;
290    encode_rlp_list_header(payload_len, output)?;
291    Ok(total_len)
292}
293
294pub(super) fn encode_typed_envelope(
295    transaction_type: u8,
296    payload_len: usize,
297    output: &mut [u8],
298    write_fields: impl FnOnce(&mut FieldWriter<'_>) -> Result<(), DecodeError>,
299) -> Result<usize, DecodeError> {
300    let payload_total_len = encoded_list_envelope_len(payload_len)?;
301    let total_len = checked_len_add(1, payload_total_len)?;
302    if output.len() < total_len {
303        return Err(DecodeError::OffsetOutOfBounds);
304    }
305    let payload_output = output
306        .get_mut(1..total_len)
307        .ok_or(DecodeError::OffsetOutOfBounds)?;
308    encode_list_envelope(payload_len, payload_output, write_fields)?;
309    let type_byte = output.first_mut().ok_or(DecodeError::OffsetOutOfBounds)?;
310    *type_byte = transaction_type;
311    Ok(total_len)
312}
313
314pub(super) fn encoded_u64_len(value: u64) -> Result<usize, DecodeError> {
315    let bytes = value.to_be_bytes();
316    encoded_rlp_integer_len(trim_u64_payload(&bytes))
317}
318
319pub(super) fn encoded_u256_len(bytes: [u8; U256_BYTES]) -> Result<usize, DecodeError> {
320    encoded_rlp_integer_len(trim_u256_payload(&bytes))
321}
322
323pub(super) fn encoded_legacy_to_len(to: LegacyTransactionTo) -> Result<usize, DecodeError> {
324    match to {
325        LegacyTransactionTo::Create => encoded_rlp_scalar_len(&[]),
326        LegacyTransactionTo::Call(address) => encoded_rlp_scalar_len(&address.to_bytes()),
327    }
328}
329
330pub(super) fn encoded_access_list_to_len(
331    to: AccessListTransactionTo,
332) -> Result<usize, DecodeError> {
333    match to {
334        AccessListTransactionTo::Create => encoded_rlp_scalar_len(&[]),
335        AccessListTransactionTo::Call(address) => encoded_rlp_scalar_len(&address.to_bytes()),
336    }
337}
338
339pub(super) fn sum_lengths(lengths: &[usize]) -> Result<usize, DecodeError> {
340    let mut total = 0usize;
341    for length in lengths {
342        total = checked_len_add(total, *length)?;
343    }
344    Ok(total)
345}
346
347pub(super) struct FieldWriter<'a> {
348    output: &'a mut [u8],
349    cursor: usize,
350}
351
352impl<'a> FieldWriter<'a> {
353    fn new(output: &'a mut [u8]) -> Self {
354        Self { output, cursor: 0 }
355    }
356
357    pub(super) fn write_with(
358        &mut self,
359        write: impl FnOnce(&mut [u8]) -> Result<usize, DecodeError>,
360    ) -> Result<(), DecodeError> {
361        let target = self
362            .output
363            .get_mut(self.cursor..)
364            .ok_or(DecodeError::OffsetOutOfBounds)?;
365        let written = write(target)?;
366        self.cursor = checked_len_add(self.cursor, written)?;
367        Ok(())
368    }
369
370    fn finish(self, expected_len: usize) -> Result<(), DecodeError> {
371        if self.cursor == expected_len {
372            Ok(())
373        } else {
374            Err(DecodeError::DecoderOverread)
375        }
376    }
377}
378
379pub(super) fn write_u64(value: u64, writer: &mut FieldWriter<'_>) -> Result<(), DecodeError> {
380    let bytes = value.to_be_bytes();
381    writer.write_with(|output| encode_rlp_integer(trim_u64_payload(&bytes), output))
382}
383
384pub(super) fn write_u256(
385    bytes: [u8; U256_BYTES],
386    writer: &mut FieldWriter<'_>,
387) -> Result<(), DecodeError> {
388    writer.write_with(|output| encode_rlp_integer(trim_u256_payload(&bytes), output))
389}
390
391pub(super) fn write_wei(
392    bytes: [u8; U256_BYTES],
393    writer: &mut FieldWriter<'_>,
394) -> Result<(), DecodeError> {
395    write_u256(bytes, writer)
396}
397
398pub(super) fn write_scalar(
399    payload: &[u8],
400    writer: &mut FieldWriter<'_>,
401) -> Result<(), DecodeError> {
402    writer.write_with(|output| encode_rlp_scalar(payload, output))
403}
404
405pub(super) fn write_address(
406    address: Address,
407    writer: &mut FieldWriter<'_>,
408) -> Result<(), DecodeError> {
409    write_scalar(&address.to_bytes(), writer)
410}
411
412pub(super) fn write_legacy_to(
413    to: LegacyTransactionTo,
414    writer: &mut FieldWriter<'_>,
415) -> Result<(), DecodeError> {
416    match to {
417        LegacyTransactionTo::Create => write_scalar(&[], writer),
418        LegacyTransactionTo::Call(address) => write_address(address, writer),
419    }
420}
421
422pub(super) fn write_transaction_to(
423    to: AccessListTransactionTo,
424    writer: &mut FieldWriter<'_>,
425) -> Result<(), DecodeError> {
426    match to {
427        AccessListTransactionTo::Create => write_scalar(&[], writer),
428        AccessListTransactionTo::Call(address) => write_address(address, writer),
429    }
430}
431
432pub(super) fn write_access_list(
433    access_list: super::AccessList<'_>,
434    writer: &mut FieldWriter<'_>,
435) -> Result<(), DecodeError> {
436    writer.write_with(|output| access_list.encode_rlp(output))
437}
438
439pub(super) fn write_blob_hashes(
440    hashes: BlobVersionedHashes<'_>,
441    writer: &mut FieldWriter<'_>,
442) -> Result<(), DecodeError> {
443    writer.write_with(|output| hashes.encode_rlp(output))
444}
445
446fn trim_u64_payload(bytes: &[u8; U64_BYTES]) -> &[u8] {
447    let start = bytes
448        .iter()
449        .position(|byte| *byte != 0)
450        .unwrap_or(U64_BYTES);
451    bytes.get(start..).unwrap_or(&[])
452}
453
454fn trim_u256_payload(bytes: &[u8; U256_BYTES]) -> &[u8] {
455    let start = bytes
456        .iter()
457        .position(|byte| *byte != 0)
458        .unwrap_or(U256_BYTES);
459    bytes.get(start..).unwrap_or(&[])
460}