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