Skip to main content

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