1use alloy_consensus::{
2 transaction::{from_eip155_value, RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
3 Header, TxEip1559, TxEip2930, TxEip7702, TxLegacy,
4};
5use alloy_eips::{
6 eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
7 eip4895::Withdrawals,
8 Typed2718,
9};
10use alloy_primitives::{
11 bytes::{Buf, BytesMut},
12 keccak256, Signature, TxHash, B256, U256,
13};
14use alloy_rlp::{Decodable, Error as RlpError, RlpDecodable};
15use derive_more::{AsRef, Deref};
16use op_alloy_consensus::{OpTxType, OpTypedTransaction, TxDeposit};
17use reth_downloaders::file_client::FileClientError;
18use serde::{Deserialize, Serialize};
19use tokio_util::codec::Decoder;
20
21#[expect(dead_code)]
22pub(crate) struct OvmBlockFileCodec;
25
26impl Decoder for OvmBlockFileCodec {
27 type Item = OvmBlock;
28 type Error = FileClientError;
29
30 fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
31 if src.is_empty() {
32 return Ok(None);
33 }
34
35 let buf_slice = &mut src.as_ref();
36 let body =
37 OvmBlock::decode(buf_slice).map_err(|err| FileClientError::Rlp(err, src.to_vec()))?;
38 src.advance(src.len() - buf_slice.len());
39
40 Ok(Some(body))
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, RlpDecodable)]
48pub struct OvmBlock {
49 pub header: Header,
51 pub body: OvmBlockBody,
53}
54
55impl OvmBlock {
56 pub fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
58 let header = Header::decode(buf)?;
59 let body = OvmBlockBody::decode(buf)?;
60 Ok(Self { header, body })
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Default, RlpDecodable)]
66#[rlp(trailing)]
67pub struct OvmBlockBody {
68 pub transactions: Vec<OvmTransactionSigned>,
70 pub ommers: Vec<Header>,
72 pub withdrawals: Option<Withdrawals>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref, Serialize, Deserialize)]
78pub struct OvmTransactionSigned {
79 pub hash: TxHash,
81 pub signature: Signature,
83 #[deref]
85 #[as_ref]
86 pub transaction: OpTypedTransaction,
87}
88
89impl AsRef<Self> for OvmTransactionSigned {
90 fn as_ref(&self) -> &Self {
91 self
92 }
93}
94
95impl OvmTransactionSigned {
96 pub fn recalculate_hash(&self) -> B256 {
99 keccak256(self.encoded_2718())
100 }
101
102 pub fn from_transaction_and_signature(
106 transaction: OpTypedTransaction,
107 signature: Signature,
108 ) -> Self {
109 let mut initial_tx = Self { transaction, hash: Default::default(), signature };
110 initial_tx.hash = initial_tx.recalculate_hash();
111 initial_tx
112 }
113
114 pub(crate) fn decode_rlp_legacy_transaction_tuple(
121 data: &mut &[u8],
122 ) -> alloy_rlp::Result<(TxLegacy, TxHash, Signature)> {
123 let original_encoding = *data;
124
125 let header = alloy_rlp::Header::decode(data)?;
126 let remaining_len = data.len();
127
128 let transaction_payload_len = header.payload_length;
129
130 if transaction_payload_len > remaining_len {
131 return Err(RlpError::InputTooShort);
132 }
133
134 let mut transaction = TxLegacy {
135 nonce: Decodable::decode(data)?,
136 gas_price: Decodable::decode(data)?,
137 gas_limit: Decodable::decode(data)?,
138 to: Decodable::decode(data)?,
139 value: Decodable::decode(data)?,
140 input: Decodable::decode(data)?,
141 chain_id: None,
142 };
143
144 let v = Decodable::decode(data)?;
145 let r: U256 = Decodable::decode(data)?;
146 let s: U256 = Decodable::decode(data)?;
147
148 let tx_length = header.payload_length + header.length();
149 let hash = keccak256(&original_encoding[..tx_length]);
150
151 let (signature, chain_id) = if v == 0 && r.is_zero() && s.is_zero() {
153 (Signature::new(r, s, false), None)
155 } else {
156 let (parity, chain_id) = from_eip155_value(v)
158 .ok_or(alloy_rlp::Error::Custom("invalid parity for legacy transaction"))?;
159 (Signature::new(r, s, parity), chain_id)
160 };
161
162 transaction.chain_id = chain_id;
164 let decoded = remaining_len - data.len();
165 if decoded != transaction_payload_len {
166 return Err(RlpError::UnexpectedLength);
167 }
168
169 Ok((transaction, hash, signature))
170 }
171
172 pub fn decode_rlp_legacy_transaction(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
184 let (transaction, hash, signature) = Self::decode_rlp_legacy_transaction_tuple(data)?;
185 let signed = Self { transaction: OpTypedTransaction::Legacy(transaction), hash, signature };
186 Ok(signed)
187 }
188}
189
190impl Decodable for OvmTransactionSigned {
191 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
217 Self::network_decode(buf).map_err(Into::into)
218 }
219}
220
221impl Typed2718 for OvmTransactionSigned {
222 fn ty(&self) -> u8 {
223 self.transaction.tx_type() as u8
224 }
225}
226
227impl Encodable2718 for OvmTransactionSigned {
228 fn type_flag(&self) -> Option<u8> {
229 match self.transaction.tx_type() {
230 OpTxType::Legacy => None,
231 tx_type => Some(tx_type as u8),
232 }
233 }
234
235 fn encode_2718_len(&self) -> usize {
236 match &self.transaction {
237 OpTypedTransaction::Legacy(legacy_tx) => {
238 legacy_tx.eip2718_encoded_length(&self.signature)
239 }
240 OpTypedTransaction::Eip2930(access_list_tx) => {
241 access_list_tx.eip2718_encoded_length(&self.signature)
242 }
243 OpTypedTransaction::Eip1559(dynamic_fee_tx) => {
244 dynamic_fee_tx.eip2718_encoded_length(&self.signature)
245 }
246 OpTypedTransaction::Eip7702(set_code_tx) => {
247 set_code_tx.eip2718_encoded_length(&self.signature)
248 }
249 OpTypedTransaction::Deposit(deposit_tx) => deposit_tx.eip2718_encoded_length(),
250 }
251 }
252
253 fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
254 match &self.transaction {
255 OpTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
256 OpTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
257 OpTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
258 OpTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
259 OpTypedTransaction::Deposit(tx) => tx.encode_2718(out),
260 }
261 }
262}
263
264impl Decodable2718 for OvmTransactionSigned {
265 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
266 match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? {
267 OpTxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
268 OpTxType::Eip2930 => {
269 let (tx, signature, hash) = TxEip2930::rlp_decode_signed(buf)?.into_parts();
270 Ok(Self { transaction: OpTypedTransaction::Eip2930(tx), signature, hash })
271 }
272 OpTxType::Eip1559 => {
273 let (tx, signature, hash) = TxEip1559::rlp_decode_signed(buf)?.into_parts();
274 Ok(Self { transaction: OpTypedTransaction::Eip1559(tx), signature, hash })
275 }
276 OpTxType::Eip7702 => {
277 let (tx, signature, hash) = TxEip7702::rlp_decode_signed(buf)?.into_parts();
278 Ok(Self { transaction: OpTypedTransaction::Eip7702(tx), signature, hash })
279 }
280 OpTxType::Deposit => Ok(Self::from_transaction_and_signature(
281 OpTypedTransaction::Deposit(TxDeposit::rlp_decode(buf)?),
282 TxDeposit::signature(),
283 )),
284 }
285 }
286
287 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
288 Ok(Self::decode_rlp_legacy_transaction(buf)?)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use crate::ovm_file_codec::OvmTransactionSigned;
295 use alloy_consensus::Typed2718;
296 use alloy_primitives::{address, b256, hex, TxKind, U256};
297 use op_alloy_consensus::OpTypedTransaction;
298 const DEPOSIT_FUNCTION_SELECTOR: [u8; 4] = [0xb6, 0xb5, 0x5f, 0x25];
299 use alloy_rlp::Decodable;
300
301 #[test]
302 fn test_decode_legacy_transactions() {
303 let deposit_tx_bytes = hex!("f88881f0830f481c830c6e4594a75127121d28a9bf848f3b70e7eea26570aa770080a4b6b55f2500000000000000000000000000000000000000000000000000000000000710b238a0d5c622d92ddf37f9c18a3465a572f74d8b1aeaf50c1cfb10b3833242781fd45fa02c4f1d5819bf8b70bf651e7a063b7db63c55bd336799c6ae3e5bc72ad6ef3def");
306 let deposit_decoded = OvmTransactionSigned::decode(&mut &deposit_tx_bytes[..]).unwrap();
307
308 let deposit_tx = match &deposit_decoded.transaction {
310 OpTypedTransaction::Legacy(ref tx) => tx,
311 _ => panic!("Expected legacy transaction for NFT deposit"),
312 };
313
314 assert_eq!(
315 deposit_tx.to,
316 TxKind::Call(address!("0xa75127121d28a9bf848f3b70e7eea26570aa7700"))
317 );
318 assert_eq!(deposit_tx.nonce, 240);
319 assert_eq!(deposit_tx.gas_price, 1001500);
320 assert_eq!(deposit_tx.gas_limit, 814661);
321 assert_eq!(deposit_tx.value, U256::ZERO);
322 assert_eq!(&deposit_tx.input.as_ref()[0..4], DEPOSIT_FUNCTION_SELECTOR);
323 assert_eq!(deposit_tx.chain_id, Some(10));
324 assert_eq!(
325 deposit_decoded.signature.r(),
326 U256::from_str_radix(
327 "d5c622d92ddf37f9c18a3465a572f74d8b1aeaf50c1cfb10b3833242781fd45f",
328 16
329 )
330 .unwrap()
331 );
332 assert_eq!(
333 deposit_decoded.signature.s(),
334 U256::from_str_radix(
335 "2c4f1d5819bf8b70bf651e7a063b7db63c55bd336799c6ae3e5bc72ad6ef3def",
336 16
337 )
338 .unwrap()
339 );
340
341 let system_tx_bytes = hex!("f9026c830d899383124f808302a77e94a0cc33dd6f4819d473226257792afe230ec3c67f80b902046c459a280000000000000000000000004d73adb72bc3dd368966edd0f0b2148401a178e2000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000647fac7f00000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000084704316e5000000000000000000000000000000000000000000000000000000000000006e10975631049de3c008989b0d8c19fc720dc556ca01abfbd794c6eb5075dd000d000000000000000000000000000000000000000000000000000000000000001410975631049de3c008989b0d8c19fc720dc556ca01abfbd794c6eb5075dd000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000082a39325251d44e11f3b6d92f9382438eb6c8b5068d4a488d4f177b26f2ca20db34ae53467322852afcc779f25eafd124c5586f54b9026497ba934403d4c578e3c1b5aa754c918ee2ecd25402df656c2419717e4017a7aecb84af3914fd3c7bf6930369c4e6ff76950246b98e354821775f02d33cdbee5ef6aed06c15b75691692d31c00000000000000000000000000000000000000000000000000000000000038a0e8991e95e66d809f4b6fb0af27c31368ca0f30e657165c428aa681ec5ea25bbea013ed325bd97365087ec713e9817d252b59113ea18430b71a5890c4eeb6b9efc4");
344 let system_decoded = OvmTransactionSigned::decode(&mut &system_tx_bytes[..]).unwrap();
345
346 assert!(system_decoded.is_legacy());
348
349 let system_tx = match &system_decoded.transaction {
350 OpTypedTransaction::Legacy(ref tx) => tx,
351 _ => panic!("Expected Legacy transaction"),
352 };
353
354 assert_eq!(system_tx.nonce, 887187);
355 assert_eq!(system_tx.gas_price, 1200000);
356 assert_eq!(system_tx.gas_limit, 173950);
357 assert_eq!(
358 system_tx.to,
359 TxKind::Call(address!("0xa0cc33dd6f4819d473226257792afe230ec3c67f"))
360 );
361 assert_eq!(system_tx.value, U256::ZERO);
362 assert_eq!(system_tx.chain_id, Some(10));
363
364 assert_eq!(
365 system_decoded.signature.r(),
366 U256::from_str_radix(
367 "e8991e95e66d809f4b6fb0af27c31368ca0f30e657165c428aa681ec5ea25bbe",
368 16
369 )
370 .unwrap()
371 );
372 assert_eq!(
373 system_decoded.signature.s(),
374 U256::from_str_radix(
375 "13ed325bd97365087ec713e9817d252b59113ea18430b71a5890c4eeb6b9efc4",
376 16
377 )
378 .unwrap()
379 );
380 assert_eq!(
381 system_decoded.hash,
382 b256!("0xe20b11349681dd049f8df32f5cdbb4c68d46b537685defcd86c7fa42cfe75b9e")
383 );
384 }
385}