Skip to main content

eth_valkyoth_protocol/transaction/
legacy.rs

1use core::fmt;
2
3use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpList, RlpScalar};
4use eth_valkyoth_primitives::{Address, ChainId, Gas, Nonce, Wei};
5
6use super::{TransactionEnvelope, TransactionEnvelopeError, decode_transaction_envelope};
7
8/// Number of fields in a canonical legacy transaction RLP list.
9pub const LEGACY_TRANSACTION_FIELD_COUNT: usize = 9;
10const ADDRESS_BYTES: usize = 20;
11
12/// Borrowed legacy transaction decoded only into field domains.
13///
14/// This type is intentionally unvalidated: no sender recovery, EIP-155 chain
15/// binding, intrinsic-gas check, nonce-state check, fork check, or signature
16/// validity is performed.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct UnvalidatedLegacyTransaction<'a> {
19    /// Account nonce.
20    pub nonce: Nonce,
21    /// Gas price in wei.
22    pub gas_price: Wei,
23    /// Gas limit.
24    pub gas_limit: Gas,
25    /// Call or contract-creation target.
26    pub to: LegacyTransactionTo,
27    /// Transferred value in wei.
28    pub value: Wei,
29    /// Borrowed transaction input data.
30    pub input: &'a [u8],
31    /// Raw canonical U256 signature recovery value.
32    ///
33    /// This is not checked for EIP-155, chain, or fork validity. Use
34    /// [`Self::eip155_chain_id`] instead of subtracting from this value
35    /// directly.
36    pub v: [u8; 32],
37    /// Raw canonical U256 signature `r` value.
38    ///
39    /// This is not checked for secp256k1 scalar validity.
40    pub r: [u8; 32],
41    /// Raw canonical U256 signature `s` value.
42    ///
43    /// This is not checked against the EIP-2 low-s bound or secp256k1 scalar
44    /// validity.
45    pub s: [u8; 32],
46}
47
48impl UnvalidatedLegacyTransaction<'_> {
49    /// Returns the nonzero EIP-155 chain ID encoded by `v`, if it fits this
50    /// crate's signed chain-domain width.
51    ///
52    /// Returns `None` for pre-EIP-155 `v` values such as `27` and `28`, and
53    /// for oversized `v` values that cannot fit in `u64`. It also returns
54    /// `None` when `v` maps to reserved `ChainId(0)`, matching
55    /// [`ChainId::try_from_signed_canonical_be_slice`]. This helper is
56    /// intentionally syntactic: it does not prove that the chain ID is
57    /// configured, active, or valid for any fork.
58    #[must_use]
59    pub fn eip155_chain_id(&self) -> Option<ChainId> {
60        const U64_TAIL_START: usize = 24;
61
62        let high_bytes = self.v.get(..U64_TAIL_START)?;
63        if high_bytes.iter().any(|byte| *byte != 0) {
64            return None;
65        }
66
67        let low_bytes = self.v.get(U64_TAIL_START..)?.try_into().ok()?;
68        let v = u64::from_be_bytes(low_bytes);
69        let chain_id = v.checked_sub(35)? / 2;
70        if chain_id == 0 {
71            return None;
72        }
73        Some(ChainId::new(chain_id))
74    }
75}
76
77/// Legacy transaction call/create target.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum LegacyTransactionTo {
80    /// Contract creation transaction with an empty `to` field.
81    Create,
82    /// Message call to an address.
83    Call(Address),
84}
85
86/// Legacy transaction field identifier.
87#[non_exhaustive]
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum LegacyTransactionField {
90    /// `nonce`.
91    Nonce,
92    /// `gas_price`.
93    GasPrice,
94    /// `gas_limit`.
95    GasLimit,
96    /// `to`.
97    To,
98    /// `value`.
99    Value,
100    /// `input`.
101    Input,
102    /// `v`.
103    V,
104    /// `r`.
105    R,
106    /// `s`.
107    S,
108}
109
110/// Legacy transaction decode failure.
111#[non_exhaustive]
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum LegacyTransactionDecodeError {
114    /// Envelope classification failed before legacy fields could be decoded.
115    Envelope(TransactionEnvelopeError),
116    /// A typed envelope was supplied to the legacy decoder.
117    TypedEnvelope {
118        /// Observed typed transaction prefix.
119        type_byte: u8,
120    },
121    /// Legacy transaction list did not contain exactly nine fields.
122    WrongFieldCount {
123        /// Expected field count.
124        expected: usize,
125        /// Actual field count.
126        found: usize,
127    },
128    /// A field failed RLP or primitive-domain decoding.
129    FieldDecode {
130        /// Field being decoded.
131        field: LegacyTransactionField,
132        /// Underlying decode error.
133        source: DecodeError,
134    },
135    /// The `to` field was neither empty nor a 20-byte address.
136    InvalidToLength {
137        /// Actual decoded scalar byte length.
138        found: usize,
139    },
140}
141
142impl LegacyTransactionDecodeError {
143    /// Stable machine-readable error code.
144    #[must_use]
145    pub const fn code(self) -> &'static str {
146        match self {
147            Self::Envelope(error) => error.code(),
148            Self::TypedEnvelope { .. } => "ETH_LEGACY_TX_TYPED_ENVELOPE",
149            Self::WrongFieldCount { .. } => "ETH_LEGACY_TX_WRONG_FIELD_COUNT",
150            Self::FieldDecode { source, .. } => source.code(),
151            Self::InvalidToLength { .. } => "ETH_LEGACY_TX_INVALID_TO_LENGTH",
152        }
153    }
154
155    /// Stable human-readable error message.
156    #[must_use]
157    pub const fn message(self) -> &'static str {
158        match self {
159            Self::Envelope(error) => error.message(),
160            Self::TypedEnvelope { .. } => "legacy transaction decoder received a typed envelope",
161            Self::WrongFieldCount { .. } => {
162                "legacy transaction must contain exactly nine RLP fields"
163            }
164            Self::FieldDecode { .. } => "legacy transaction field failed bounded decoding",
165            Self::InvalidToLength { .. } => {
166                "legacy transaction to field must be empty or a 20-byte address"
167            }
168        }
169    }
170
171    /// Stable high-level category for policy decisions.
172    #[must_use]
173    pub const fn category(self) -> LegacyTransactionDecodeErrorCategory {
174        match self {
175            Self::Envelope(error) => match error.category() {
176                super::TransactionEnvelopeErrorCategory::MalformedInput => {
177                    LegacyTransactionDecodeErrorCategory::MalformedInput
178                }
179                super::TransactionEnvelopeErrorCategory::Unsupported => {
180                    LegacyTransactionDecodeErrorCategory::Unsupported
181                }
182                super::TransactionEnvelopeErrorCategory::ResourceExhaustion => {
183                    LegacyTransactionDecodeErrorCategory::ResourceExhaustion
184                }
185            },
186            Self::TypedEnvelope { .. } => LegacyTransactionDecodeErrorCategory::WrongType,
187            Self::WrongFieldCount { .. } | Self::InvalidToLength { .. } => {
188                LegacyTransactionDecodeErrorCategory::MalformedInput
189            }
190            Self::FieldDecode { source, .. } => match source.category() {
191                eth_valkyoth_codec::DecodeErrorCategory::MalformedInput => {
192                    LegacyTransactionDecodeErrorCategory::MalformedInput
193                }
194                eth_valkyoth_codec::DecodeErrorCategory::ResourceExhaustion => {
195                    LegacyTransactionDecodeErrorCategory::ResourceExhaustion
196                }
197                _ => LegacyTransactionDecodeErrorCategory::MalformedInput,
198            },
199        }
200    }
201}
202
203impl fmt::Display for LegacyTransactionDecodeError {
204    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205        formatter.write_str(self.message())
206    }
207}
208
209#[cfg(feature = "std")]
210impl std::error::Error for LegacyTransactionDecodeError {}
211
212/// Stable high-level legacy transaction decode error categories.
213#[non_exhaustive]
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum LegacyTransactionDecodeErrorCategory {
216    /// Input is malformed for a legacy transaction.
217    MalformedInput,
218    /// A typed transaction envelope was supplied to a legacy-only decoder.
219    WrongType,
220    /// A future or unsupported transaction domain was encountered.
221    Unsupported,
222    /// The active decode policy rejected the input as too large or too deep.
223    ResourceExhaustion,
224}
225
226/// Decodes a canonical legacy transaction into unvalidated field domains.
227pub fn decode_legacy_transaction<'a>(
228    input: &'a [u8],
229    limits: DecodeLimits,
230) -> Result<UnvalidatedLegacyTransaction<'a>, LegacyTransactionDecodeError> {
231    match decode_transaction_envelope(input, limits)
232        .map_err(LegacyTransactionDecodeError::Envelope)?
233    {
234        TransactionEnvelope::Legacy(list) => decode_legacy_list(list, limits),
235        TransactionEnvelope::Typed(typed) => Err(LegacyTransactionDecodeError::TypedEnvelope {
236            type_byte: typed.transaction_type.get(),
237        }),
238    }
239}
240
241fn decode_legacy_list<'a>(
242    list: RlpList<'a>,
243    limits: DecodeLimits,
244) -> Result<UnvalidatedLegacyTransaction<'a>, LegacyTransactionDecodeError> {
245    if list.item_count() != LEGACY_TRANSACTION_FIELD_COUNT {
246        return Err(LegacyTransactionDecodeError::WrongFieldCount {
247            expected: LEGACY_TRANSACTION_FIELD_COUNT,
248            found: list.item_count(),
249        });
250    }
251
252    let mut fields = list.items();
253    let nonce = Nonce::new(decode_u64_field(
254        &mut fields,
255        LegacyTransactionField::Nonce,
256    )?);
257    let gas_price = Wei::from_be_bytes(decode_u256_field(
258        &mut fields,
259        LegacyTransactionField::GasPrice,
260    )?);
261    let gas_limit = Gas::new(decode_u64_field(
262        &mut fields,
263        LegacyTransactionField::GasLimit,
264    )?);
265    let to = decode_to(next_scalar(&mut fields, LegacyTransactionField::To)?)?;
266    let value = Wei::from_be_bytes(decode_u256_field(
267        &mut fields,
268        LegacyTransactionField::Value,
269    )?);
270    let input = next_scalar(&mut fields, LegacyTransactionField::Input)?.payload();
271    limits
272        .check_single_allocation_limit(input.len())
273        .map_err(|source| field_error(LegacyTransactionField::Input, source))?;
274    let v = decode_u256_field(&mut fields, LegacyTransactionField::V)?;
275    let r = decode_u256_field(&mut fields, LegacyTransactionField::R)?;
276    let s = decode_u256_field(&mut fields, LegacyTransactionField::S)?;
277
278    Ok(UnvalidatedLegacyTransaction {
279        nonce,
280        gas_price,
281        gas_limit,
282        to,
283        value,
284        input,
285        v,
286        r,
287        s,
288    })
289}
290
291fn next_scalar<'a>(
292    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
293    field: LegacyTransactionField,
294) -> Result<RlpScalar<'a>, LegacyTransactionDecodeError> {
295    let item = fields
296        .next()
297        .ok_or(field_error(field, DecodeError::Malformed))?
298        .map_err(|source| field_error(field, source))?;
299    match item {
300        RlpItem::Scalar(scalar) => Ok(scalar),
301        RlpItem::List(_) => Err(field_error(field, DecodeError::UnexpectedList)),
302    }
303}
304
305fn decode_to(scalar: RlpScalar<'_>) -> Result<LegacyTransactionTo, LegacyTransactionDecodeError> {
306    let payload = scalar.payload();
307    if payload.is_empty() {
308        return Ok(LegacyTransactionTo::Create);
309    }
310    let found = payload.len();
311    let bytes: [u8; ADDRESS_BYTES] = payload
312        .try_into()
313        .map_err(|_| LegacyTransactionDecodeError::InvalidToLength { found })?;
314    Ok(LegacyTransactionTo::Call(Address::from_bytes(bytes)))
315}
316
317fn decode_u64_field<'a>(
318    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
319    field: LegacyTransactionField,
320) -> Result<u64, LegacyTransactionDecodeError> {
321    RlpInteger::try_from_scalar(next_scalar(fields, field)?)
322        .and_then(RlpInteger::to_u64)
323        .map_err(|source| field_error(field, source))
324}
325
326fn decode_u256_field<'a>(
327    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
328    field: LegacyTransactionField,
329) -> Result<[u8; 32], LegacyTransactionDecodeError> {
330    RlpInteger::try_from_scalar(next_scalar(fields, field)?)
331        .and_then(RlpInteger::to_be_bytes32)
332        .map_err(|source| field_error(field, source))
333}
334
335const fn field_error(
336    field: LegacyTransactionField,
337    source: DecodeError,
338) -> LegacyTransactionDecodeError {
339    LegacyTransactionDecodeError::FieldDecode { field, source }
340}