eth_valkyoth_protocol/transaction/
legacy.rs1use 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
8pub const LEGACY_TRANSACTION_FIELD_COUNT: usize = 9;
10const ADDRESS_BYTES: usize = 20;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct UnvalidatedLegacyTransaction<'a> {
19 pub nonce: Nonce,
21 pub gas_price: Wei,
23 pub gas_limit: Gas,
25 pub to: LegacyTransactionTo,
27 pub value: Wei,
29 pub input: &'a [u8],
31 pub v: [u8; 32],
37 pub r: [u8; 32],
41 pub s: [u8; 32],
46}
47
48impl UnvalidatedLegacyTransaction<'_> {
49 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum LegacyTransactionTo {
80 Create,
82 Call(Address),
84}
85
86#[non_exhaustive]
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum LegacyTransactionField {
90 Nonce,
92 GasPrice,
94 GasLimit,
96 To,
98 Value,
100 Input,
102 V,
104 R,
106 S,
108}
109
110#[non_exhaustive]
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum LegacyTransactionDecodeError {
114 Envelope(TransactionEnvelopeError),
116 TypedEnvelope {
118 type_byte: u8,
120 },
121 WrongFieldCount {
123 expected: usize,
125 found: usize,
127 },
128 FieldDecode {
130 field: LegacyTransactionField,
132 source: DecodeError,
134 },
135 InvalidToLength {
137 found: usize,
139 },
140}
141
142impl LegacyTransactionDecodeError {
143 #[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 #[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 #[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#[non_exhaustive]
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum LegacyTransactionDecodeErrorCategory {
216 MalformedInput,
218 WrongType,
220 Unsupported,
222 ResourceExhaustion,
224}
225
226pub 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}