reth_rpc/eth/helpers/
transaction.rs

1//! Contains RPC handler implementations specific to transactions
2
3use std::time::Duration;
4
5use crate::EthApi;
6use alloy_consensus::BlobTransactionValidationError;
7use alloy_eips::{eip7594::BlobTransactionSidecarVariant, BlockId, Typed2718};
8use alloy_primitives::{hex, Bytes, B256};
9use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
10use reth_primitives_traits::AlloyBlockHeader;
11use reth_rpc_convert::RpcConvert;
12use reth_rpc_eth_api::{
13    helpers::{spec::SignersForRpc, EthTransactions, LoadTransaction},
14    FromEvmError, RpcNodeCore,
15};
16use reth_rpc_eth_types::{error::RpcPoolError, utils::recover_raw_transaction, EthApiError};
17use reth_storage_api::BlockReaderIdExt;
18use reth_transaction_pool::{
19    error::Eip4844PoolTransactionError, AddedTransactionOutcome, EthBlobTransactionSidecar,
20    EthPoolTransaction, PoolTransaction, TransactionPool,
21};
22
23impl<N, Rpc> EthTransactions for EthApi<N, Rpc>
24where
25    N: RpcNodeCore,
26    EthApiError: FromEvmError<N::Evm>,
27    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
28{
29    #[inline]
30    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
31        self.inner.signers()
32    }
33
34    #[inline]
35    fn send_raw_transaction_sync_timeout(&self) -> Duration {
36        self.inner.send_raw_transaction_sync_timeout()
37    }
38
39    /// Decodes and recovers the transaction and submits it to the pool.
40    ///
41    /// Returns the hash of the transaction.
42    async fn send_raw_transaction(&self, tx: Bytes) -> Result<B256, Self::Error> {
43        let recovered = recover_raw_transaction(&tx)?;
44
45        let mut pool_transaction =
46            <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
47
48        // TODO: remove this after Osaka transition
49        // Convert legacy blob sidecars to EIP-7594 format
50        if pool_transaction.is_eip4844() {
51            let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
52                return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
53                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
54                )));
55            };
56
57            let sidecar = match sidecar {
58                BlobTransactionSidecarVariant::Eip4844(sidecar) => {
59                    let latest = self
60                        .provider()
61                        .latest_header()?
62                        .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
63                    // Convert to EIP-7594 if next block is Osaka
64                    if self
65                        .provider()
66                        .chain_spec()
67                        .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
68                    {
69                        BlobTransactionSidecarVariant::Eip7594(
70                            self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
71                                || {
72                                    RpcPoolError::Eip4844(
73                                        Eip4844PoolTransactionError::InvalidEip4844Blob(
74                                            BlobTransactionValidationError::InvalidProof,
75                                        ),
76                                    )
77                                },
78                            )?,
79                        )
80                    } else {
81                        BlobTransactionSidecarVariant::Eip4844(sidecar)
82                    }
83                }
84                sidecar => sidecar,
85            };
86
87            pool_transaction =
88                EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
89                    .ok_or_else(|| {
90                        RpcPoolError::Eip4844(
91                            Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
92                        )
93                    })?;
94        }
95
96        // forward the transaction to the specific endpoint if configured.
97        if let Some(client) = self.raw_tx_forwarder() {
98            tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
99            let rlp_hex = hex::encode_prefixed(&tx);
100
101            // broadcast raw transaction to subscribers if there is any.
102            self.broadcast_raw_transaction(tx);
103
104            let hash =
105                client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
106                    tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
107                }).map_err(EthApiError::other)?;
108
109            // Retain tx in local tx pool after forwarding, for local RPC usage.
110            let _ = self.inner.add_pool_transaction(pool_transaction).await;
111
112            return Ok(hash);
113        }
114
115        // broadcast raw transaction to subscribers if there is any.
116        self.broadcast_raw_transaction(tx);
117
118        // submit the transaction to the pool with a `Local` origin
119        let AddedTransactionOutcome { hash, .. } =
120            self.inner.add_pool_transaction(pool_transaction).await?;
121
122        Ok(hash)
123    }
124}
125
126impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
127where
128    N: RpcNodeCore,
129    EthApiError: FromEvmError<N::Evm>,
130    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
131{
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::eth::helpers::types::EthRpcConverter;
138    use alloy_consensus::{Block, Header, SidecarBuilder, SimpleCoder, Transaction};
139    use alloy_primitives::{Address, U256};
140    use alloy_rpc_types_eth::request::TransactionRequest;
141    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
142    use reth_evm_ethereum::EthEvmConfig;
143    use reth_network_api::noop::NoopNetwork;
144    use reth_provider::{
145        test_utils::{ExtendedAccount, MockEthProvider},
146        ChainSpecProvider,
147    };
148    use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
149    use reth_transaction_pool::test_utils::{testing_pool, TestPool};
150    use std::collections::HashMap;
151
152    fn mock_eth_api(
153        accounts: HashMap<Address, ExtendedAccount>,
154    ) -> EthApi<
155        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
156        EthRpcConverter<ChainSpec>,
157    > {
158        let mock_provider = MockEthProvider::default()
159            .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
160        mock_provider.extend_accounts(accounts);
161
162        let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
163        let pool = testing_pool();
164
165        let genesis_header = Header {
166            number: 0,
167            gas_limit: 30_000_000,
168            timestamp: 1,
169            excess_blob_gas: Some(0),
170            base_fee_per_gas: Some(1000000000),
171            blob_gas_used: Some(0),
172            ..Default::default()
173        };
174
175        let genesis_hash = B256::ZERO;
176        mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
177
178        EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config).build()
179    }
180
181    #[tokio::test]
182    async fn send_raw_transaction() {
183        let eth_api = mock_eth_api(Default::default());
184        let pool = eth_api.pool();
185
186        // https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
187        let tx_1 = Bytes::from(hex!(
188            "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
189        ));
190
191        let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
192        assert_eq!(
193            pool.len(),
194            1,
195            "expect 1 transaction in the pool, but pool size is {}",
196            pool.len()
197        );
198
199        // https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
200        let tx_2 = Bytes::from(hex!(
201            "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
202        ));
203
204        let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
205        assert_eq!(
206            pool.len(),
207            2,
208            "expect 2 transactions in the pool, but pool size is {}",
209            pool.len()
210        );
211
212        assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
213        assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
214    }
215
216    #[tokio::test]
217    async fn test_fill_transaction_fills_chain_id() {
218        let address = Address::random();
219        let accounts = HashMap::from([(
220            address,
221            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), // 10 ETH
222        )]);
223
224        let eth_api = mock_eth_api(accounts);
225
226        let tx_req = TransactionRequest {
227            from: Some(address),
228            to: Some(Address::random().into()),
229            gas: Some(21_000),
230            ..Default::default()
231        };
232
233        let filled =
234            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
235
236        // Should fill with the chain id from provider
237        assert!(filled.tx.chain_id().is_some());
238    }
239
240    #[tokio::test]
241    async fn test_fill_transaction_fills_nonce() {
242        let address = Address::random();
243        let nonce = 42u64;
244
245        let accounts = HashMap::from([(
246            address,
247            ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), // 1 ETH
248        )]);
249
250        let eth_api = mock_eth_api(accounts);
251
252        let tx_req = TransactionRequest {
253            from: Some(address),
254            to: Some(Address::random().into()),
255            value: Some(U256::from(1000)),
256            gas: Some(21_000),
257            ..Default::default()
258        };
259
260        let filled =
261            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
262
263        assert_eq!(filled.tx.nonce(), nonce);
264    }
265
266    #[tokio::test]
267    async fn test_fill_transaction_preserves_provided_fields() {
268        let address = Address::random();
269        let provided_nonce = 100u64;
270        let provided_gas_limit = 50_000u64;
271
272        let accounts = HashMap::from([(
273            address,
274            ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
275        )]);
276
277        let eth_api = mock_eth_api(accounts);
278
279        let tx_req = TransactionRequest {
280            from: Some(address),
281            to: Some(Address::random().into()),
282            value: Some(U256::from(1000)),
283            nonce: Some(provided_nonce),
284            gas: Some(provided_gas_limit),
285            ..Default::default()
286        };
287
288        let filled =
289            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
290
291        // Should preserve the provided nonce and gas limit
292        assert_eq!(filled.tx.nonce(), provided_nonce);
293        assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
294    }
295
296    #[tokio::test]
297    async fn test_fill_transaction_fills_all_missing_fields() {
298        let address = Address::random();
299
300        let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
301        let accounts = HashMap::from([(address, ExtendedAccount::new(5, balance))]);
302
303        let eth_api = mock_eth_api(accounts);
304
305        // Create a simple transfer transaction
306        let tx_req = TransactionRequest {
307            from: Some(address),
308            to: Some(Address::random().into()),
309            ..Default::default()
310        };
311
312        let filled =
313            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
314
315        assert!(filled.tx.is_eip1559());
316    }
317
318    #[tokio::test]
319    async fn test_fill_transaction_eip4844_blob_fee() {
320        let address = Address::random();
321        let accounts = HashMap::from([(
322            address,
323            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
324        )]);
325
326        let eth_api = mock_eth_api(accounts);
327
328        let mut builder = SidecarBuilder::<SimpleCoder>::new();
329        builder.ingest(b"dummy blob");
330
331        // EIP-4844 blob transaction with versioned hashes but no blob fee
332        let tx_req = TransactionRequest {
333            from: Some(address),
334            to: Some(Address::random().into()),
335            sidecar: Some(builder.build().unwrap()),
336            ..Default::default()
337        };
338
339        let filled =
340            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
341
342        // Blob transaction should have max_fee_per_blob_gas filled
343        assert!(
344            filled.tx.max_fee_per_blob_gas().is_some(),
345            "max_fee_per_blob_gas should be filled for blob tx"
346        );
347        assert!(
348            filled.tx.blob_versioned_hashes().is_some(),
349            "blob_versioned_hashes should be preserved"
350        );
351    }
352
353    #[tokio::test]
354    async fn test_fill_transaction_eip4844_preserves_blob_fee() {
355        let address = Address::random();
356        let accounts = HashMap::from([(
357            address,
358            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
359        )]);
360
361        let eth_api = mock_eth_api(accounts);
362
363        let provided_blob_fee = 5000000u128;
364
365        let mut builder = SidecarBuilder::<SimpleCoder>::new();
366        builder.ingest(b"dummy blob");
367
368        // EIP-4844 blob transaction with blob fee already set
369        let tx_req = TransactionRequest {
370            from: Some(address),
371            to: Some(Address::random().into()),
372            transaction_type: Some(3), // EIP-4844
373            sidecar: Some(builder.build().unwrap()),
374            max_fee_per_blob_gas: Some(provided_blob_fee), // Already set
375            ..Default::default()
376        };
377
378        let filled =
379            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
380
381        // Should preserve the provided blob fee
382        assert_eq!(
383            filled.tx.max_fee_per_blob_gas(),
384            Some(provided_blob_fee),
385            "should preserve provided max_fee_per_blob_gas"
386        );
387    }
388
389    #[tokio::test]
390    async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
391        let address = Address::random();
392        let accounts = HashMap::from([(
393            address,
394            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
395        )]);
396
397        let eth_api = mock_eth_api(accounts);
398
399        // EIP-1559 transaction without blob fields
400        let tx_req = TransactionRequest {
401            from: Some(address),
402            to: Some(Address::random().into()),
403            transaction_type: Some(2), // EIP-1559
404            ..Default::default()
405        };
406
407        let filled =
408            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
409
410        // Non-blob transaction should NOT have blob fee filled
411        assert!(
412            filled.tx.max_fee_per_blob_gas().is_none(),
413            "max_fee_per_blob_gas should not be set for non-blob tx"
414        );
415    }
416}