reth_primitives/transaction/
sidecar.rs

1#![cfg_attr(docsrs, doc(cfg(feature = "c-kzg")))]
2
3use crate::{Transaction, TransactionSigned};
4use alloy_consensus::{transaction::RlpEcdsaTx, Signed, TxEip4844WithSidecar};
5use alloy_eips::eip4844::BlobTransactionSidecar;
6use derive_more::Deref;
7use reth_primitives_traits::InMemorySize;
8use serde::{Deserialize, Serialize};
9
10/// A response to `GetPooledTransactions` that includes blob data, their commitments, and their
11/// corresponding proofs.
12///
13/// This is defined in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#networking) as an element
14/// of a `PooledTransactions` response.
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Deref)]
16pub struct BlobTransaction(pub Signed<TxEip4844WithSidecar>);
17
18impl BlobTransaction {
19    /// Constructs a new [`BlobTransaction`] from a [`TransactionSigned`] and a
20    /// [`BlobTransactionSidecar`].
21    ///
22    /// Returns an error if the signed transaction is not [`Transaction::Eip4844`]
23    pub fn try_from_signed(
24        tx: TransactionSigned,
25        sidecar: BlobTransactionSidecar,
26    ) -> Result<Self, (TransactionSigned, BlobTransactionSidecar)> {
27        let hash = tx.hash();
28        let TransactionSigned { transaction, signature, .. } = tx;
29        match transaction {
30            Transaction::Eip4844(transaction) => Ok(Self(Signed::new_unchecked(
31                TxEip4844WithSidecar { tx: transaction, sidecar },
32                signature,
33                hash,
34            ))),
35            transaction => {
36                let tx = TransactionSigned::new(transaction, signature, hash);
37                Err((tx, sidecar))
38            }
39        }
40    }
41
42    /// Splits the [`BlobTransaction`] into its [`TransactionSigned`] and [`BlobTransactionSidecar`]
43    /// components.
44    pub fn into_parts(self) -> (TransactionSigned, BlobTransactionSidecar) {
45        let (transaction, signature, hash) = self.0.into_parts();
46        let (transaction, sidecar) = transaction.into_parts();
47        let transaction = TransactionSigned::new(transaction.into(), signature, hash);
48        (transaction, sidecar)
49    }
50
51    /// Decodes a [`BlobTransaction`] from RLP. This expects the encoding to be:
52    /// `rlp([transaction_payload_body, blobs, commitments, proofs])`
53    ///
54    /// where `transaction_payload_body` is a list:
55    /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]`
56    ///
57    /// Note: this should be used only when implementing other RLP decoding methods, and does not
58    /// represent the full RLP decoding of the `PooledTransactionsElement` type.
59    pub(crate) fn decode_inner(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
60        let (transaction, signature, hash) =
61            TxEip4844WithSidecar::rlp_decode_signed(data)?.into_parts();
62        Ok(Self(Signed::new_unchecked(transaction, signature, hash)))
63    }
64}
65
66impl InMemorySize for BlobTransaction {
67    fn size(&self) -> usize {
68        // TODO(mattsse): replace with next alloy bump
69        self.0.hash().size() +
70            self.0.signature().size() +
71            self.0.tx().tx().size() +
72            self.0.tx().sidecar.size()
73    }
74}
75
76#[cfg(all(test, feature = "c-kzg"))]
77mod tests {
78    use super::*;
79    use crate::{kzg::Blob, PooledTransactionsElement};
80    use alloc::vec::Vec;
81    use alloy_eips::{
82        eip2718::{Decodable2718, Encodable2718},
83        eip4844::Bytes48,
84    };
85    use alloy_primitives::hex;
86    use std::{fs, path::PathBuf, str::FromStr};
87
88    #[test]
89    fn test_blob_transaction_sidecar_generation() {
90        // Read the contents of the JSON file into a string.
91        let json_content = fs::read_to_string(
92            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/transaction/blob_data/blob1.json"),
93        )
94        .expect("Failed to read the blob data file");
95
96        // Parse the JSON contents into a serde_json::Value
97        let json_value: serde_json::Value =
98            serde_json::from_str(&json_content).expect("Failed to deserialize JSON");
99
100        // Extract blob data from JSON and convert it to Blob
101        let blobs: Vec<Blob> = vec![Blob::from_hex(
102            json_value.get("data").unwrap().as_str().expect("Data is not a valid string"),
103        )
104        .unwrap()];
105
106        // Generate a BlobTransactionSidecar from the blobs
107        let sidecar = BlobTransactionSidecar::try_from_blobs(blobs).unwrap();
108
109        // Assert commitment equality
110        assert_eq!(
111            sidecar.commitments,
112            vec![
113                Bytes48::from_str(json_value.get("commitment").unwrap().as_str().unwrap()).unwrap()
114            ]
115        );
116    }
117
118    #[test]
119    fn test_blob_transaction_sidecar_size() {
120        // Vector to store blob data from each file
121        let mut blobs: Vec<Blob> = Vec::new();
122
123        // Iterate over each file in the folder
124        for entry in fs::read_dir(
125            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/transaction/blob_data/"),
126        )
127        .expect("Failed to read blob_data folder")
128        {
129            let entry = entry.expect("Failed to read directory entry");
130            let file_path = entry.path();
131
132            // Ensure the entry is a file and not a directory
133            if !file_path.is_file() || file_path.extension().unwrap_or_default() != "json" {
134                continue
135            }
136
137            // Read the contents of the JSON file into a string.
138            let json_content =
139                fs::read_to_string(file_path).expect("Failed to read the blob data file");
140
141            // Parse the JSON contents into a serde_json::Value
142            let json_value: serde_json::Value =
143                serde_json::from_str(&json_content).expect("Failed to deserialize JSON");
144
145            // Extract blob data from JSON and convert it to Blob
146            if let Some(data) = json_value.get("data") {
147                if let Some(data_str) = data.as_str() {
148                    if let Ok(blob) = Blob::from_hex(data_str) {
149                        blobs.push(blob);
150                    }
151                }
152            }
153        }
154
155        // Generate a BlobTransactionSidecar from the blobs
156        let sidecar = BlobTransactionSidecar::try_from_blobs(blobs).unwrap();
157
158        // Assert sidecar size
159        assert_eq!(sidecar.size(), 524672);
160    }
161
162    #[test]
163    fn test_blob_transaction_sidecar_rlp_encode() {
164        // Read the contents of the JSON file into a string.
165        let json_content = fs::read_to_string(
166            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/transaction/blob_data/blob1.json"),
167        )
168        .expect("Failed to read the blob data file");
169
170        // Parse the JSON contents into a serde_json::Value
171        let json_value: serde_json::Value =
172            serde_json::from_str(&json_content).expect("Failed to deserialize JSON");
173
174        // Extract blob data from JSON and convert it to Blob
175        let blobs: Vec<Blob> = vec![Blob::from_hex(
176            json_value.get("data").unwrap().as_str().expect("Data is not a valid string"),
177        )
178        .unwrap()];
179
180        // Generate a BlobTransactionSidecar from the blobs
181        let sidecar = BlobTransactionSidecar::try_from_blobs(blobs).unwrap();
182
183        // Create a vector to store the encoded RLP
184        let mut encoded_rlp = Vec::new();
185
186        // Encode the inner data of the BlobTransactionSidecar into RLP
187        sidecar.rlp_encode_fields(&mut encoded_rlp);
188
189        // Assert the equality between the expected RLP from the JSON and the encoded RLP
190        assert_eq!(json_value.get("rlp").unwrap().as_str().unwrap(), hex::encode(&encoded_rlp));
191    }
192
193    #[test]
194    fn test_blob_transaction_sidecar_rlp_decode() {
195        // Read the contents of the JSON file into a string.
196        let json_content = fs::read_to_string(
197            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/transaction/blob_data/blob1.json"),
198        )
199        .expect("Failed to read the blob data file");
200
201        // Parse the JSON contents into a serde_json::Value
202        let json_value: serde_json::Value =
203            serde_json::from_str(&json_content).expect("Failed to deserialize JSON");
204
205        // Extract blob data from JSON and convert it to Blob
206        let blobs: Vec<Blob> = vec![Blob::from_hex(
207            json_value.get("data").unwrap().as_str().expect("Data is not a valid string"),
208        )
209        .unwrap()];
210
211        // Generate a BlobTransactionSidecar from the blobs
212        let sidecar = BlobTransactionSidecar::try_from_blobs(blobs).unwrap();
213
214        // Create a vector to store the encoded RLP
215        let mut encoded_rlp = Vec::new();
216
217        // Encode the inner data of the BlobTransactionSidecar into RLP
218        sidecar.rlp_encode_fields(&mut encoded_rlp);
219
220        // Decode the RLP-encoded data back into a BlobTransactionSidecar
221        let decoded_sidecar =
222            BlobTransactionSidecar::rlp_decode_fields(&mut encoded_rlp.as_slice()).unwrap();
223
224        // Assert the equality between the original BlobTransactionSidecar and the decoded one
225        assert_eq!(sidecar, decoded_sidecar);
226    }
227
228    #[test]
229    fn decode_encode_raw_4844_rlp() {
230        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/4844rlp");
231        let dir = fs::read_dir(path).expect("Unable to read folder");
232        for entry in dir {
233            let entry = entry.unwrap();
234            let content = fs::read_to_string(entry.path()).unwrap();
235            let raw = hex::decode(content.trim()).unwrap();
236            let tx = PooledTransactionsElement::decode_2718(&mut raw.as_ref())
237                .map_err(|err| {
238                    panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
239                })
240                .unwrap();
241            // We want to test only EIP-4844 transactions
242            assert!(tx.is_eip4844());
243            let encoded = tx.encoded_2718();
244            assert_eq!(encoded.as_slice(), &raw[..], "{:?}", entry.path());
245        }
246    }
247}