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            let sidecar = sidecar.into_sidecar();
55
56            let sidecar = match sidecar {
57                BlobTransactionSidecarVariant::Eip4844(sidecar) => {
58                    let latest = self
59                        .provider()
60                        .latest_header()?
61                        .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
62                    // Convert to EIP-7594 if next block is Osaka
63                    if self
64                        .provider()
65                        .chain_spec()
66                        .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
67                    {
68                        BlobTransactionSidecarVariant::Eip7594(
69                            self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
70                                || {
71                                    RpcPoolError::Eip4844(
72                                        Eip4844PoolTransactionError::InvalidEip4844Blob(
73                                            BlobTransactionValidationError::InvalidProof,
74                                        ),
75                                    )
76                                },
77                            )?,
78                        )
79                    } else {
80                        BlobTransactionSidecarVariant::Eip4844(sidecar)
81                    }
82                }
83                sidecar => sidecar,
84            };
85
86            pool_transaction =
87                EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
88                    .ok_or_else(|| {
89                        RpcPoolError::Eip4844(
90                            Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
91                        )
92                    })?;
93        }
94
95        // forward the transaction to the specific endpoint if configured.
96        if let Some(client) = self.raw_tx_forwarder() {
97            tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
98            let rlp_hex = hex::encode_prefixed(&tx);
99
100            // broadcast raw transaction to subscribers if there is any.
101            self.broadcast_raw_transaction(tx);
102
103            let hash =
104                client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
105                    tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
106                }).map_err(EthApiError::other)?;
107
108            // Retain tx in local tx pool after forwarding, for local RPC usage.
109            let _ = self.inner.add_pool_transaction(origin, pool_transaction).await;
110
111            return Ok(hash);
112        }
113
114        // broadcast raw transaction to subscribers if there is any.
115        self.broadcast_raw_transaction(tx);
116
117        let AddedTransactionOutcome { hash, .. } =
118            self.inner.add_pool_transaction(origin, pool_transaction).await?;
119
120        Ok(hash)
121    }
122}
123
124impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
125where
126    N: RpcNodeCore,
127    EthApiError: FromEvmError<N::Evm>,
128    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
129{
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::eth::helpers::{signer::DevSigner, types::EthRpcConverter};
136    use alloy_consensus::{
137        BlobTransactionSidecar, Block, Header, SidecarBuilder, SimpleCoder, Transaction,
138    };
139    use alloy_primitives::{map::AddressMap, Address, Bytes, 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::{
150        test_utils::{testing_pool, TestPool},
151        TransactionOrigin, TransactionPool,
152    };
153
154    fn mock_eth_api(
155        accounts: AddressMap<ExtendedAccount>,
156    ) -> EthApi<
157        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
158        EthRpcConverter<ChainSpec>,
159    > {
160        mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
161    }
162
163    fn mock_eth_api_with_sync_timeout(
164        accounts: AddressMap<ExtendedAccount>,
165        send_raw_transaction_sync_timeout: Duration,
166    ) -> EthApi<
167        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
168        EthRpcConverter<ChainSpec>,
169    > {
170        let mock_provider = MockEthProvider::default()
171            .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
172        mock_provider.extend_accounts(accounts);
173
174        let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
175        let pool = testing_pool();
176
177        let genesis_header = Header {
178            number: 0,
179            gas_limit: 30_000_000,
180            timestamp: 1,
181            excess_blob_gas: Some(0),
182            base_fee_per_gas: Some(1000000000),
183            blob_gas_used: Some(0),
184            ..Default::default()
185        };
186
187        let genesis_hash = B256::ZERO;
188        mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
189
190        EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
191            .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
192            .build()
193    }
194
195    fn raw_transfer_tx() -> Bytes {
196        // https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
197        Bytes::from(hex!(
198            "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
199        ))
200    }
201
202    #[tokio::test]
203    async fn send_raw_transaction() {
204        let eth_api = mock_eth_api(Default::default());
205        let pool = eth_api.pool();
206
207        let tx_1 = raw_transfer_tx();
208
209        let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
210        assert_eq!(
211            pool.len(),
212            1,
213            "expect 1 transaction in the pool, but pool size is {}",
214            pool.len()
215        );
216
217        // https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
218        let tx_2 = Bytes::from(hex!(
219            "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
220        ));
221
222        let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
223        assert_eq!(
224            pool.len(),
225            2,
226            "expect 2 transactions in the pool, but pool size is {}",
227            pool.len()
228        );
229
230        assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
231        assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
232        assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
233        assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
234    }
235
236    #[tokio::test]
237    async fn send_raw_transaction_sync_uses_request_timeout() {
238        let eth_api = mock_eth_api(Default::default());
239
240        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
241
242        assert!(matches!(
243            err,
244            EthApiError::TransactionConfirmationTimeout { duration, .. }
245                if duration == Duration::from_millis(1)
246        ));
247        assert_eq!(eth_api.pool().len(), 1);
248    }
249
250    #[tokio::test]
251    async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
252        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
253
254        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
255
256        assert!(matches!(
257            err,
258            EthApiError::TransactionConfirmationTimeout { duration, .. }
259                if duration == Duration::from_millis(1)
260        ));
261        assert_eq!(eth_api.pool().len(), 1);
262    }
263
264    #[tokio::test]
265    async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
266        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
267
268        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
269
270        assert!(matches!(
271            err,
272            EthApiError::TransactionConfirmationTimeout { duration, .. }
273                if duration == Duration::from_millis(1)
274        ));
275        assert_eq!(eth_api.pool().len(), 1);
276    }
277
278    #[tokio::test]
279    async fn send_raw_transaction_sync_caps_request_timeout() {
280        let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
281
282        let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
283
284        assert!(matches!(
285            err,
286            EthApiError::TransactionConfirmationTimeout { duration, .. }
287                if duration == Duration::from_millis(1)
288        ));
289        assert_eq!(eth_api.pool().len(), 1);
290    }
291
292    #[tokio::test]
293    async fn send_transaction_preserves_provided_gas_limit() {
294        let signers = DevSigner::random_signers(1);
295        let address = signers[0].accounts()[0];
296        let accounts = AddressMap::from_iter([(
297            address,
298            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
299        )]);
300        let eth_api = mock_eth_api(accounts);
301        eth_api.signers().write().extend(signers);
302
303        let provided_gas_limit = 90_000;
304        let tx_req = TransactionRequest {
305            from: Some(address),
306            to: Some(address.into()),
307            gas: Some(provided_gas_limit),
308            gas_price: Some(1_000_000_000),
309            ..Default::default()
310        };
311
312        let hash = eth_api
313            .send_transaction_request(tx_req)
314            .await
315            .expect("send_transaction should succeed");
316        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
317
318        assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
319    }
320
321    #[tokio::test]
322    async fn send_transaction_rejects_mismatched_chain_id() {
323        let signers = DevSigner::random_signers(1);
324        let address = signers[0].accounts()[0];
325        let accounts = AddressMap::from_iter([(
326            address,
327            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
328        )]);
329        let eth_api = mock_eth_api(accounts);
330        eth_api.signers().write().extend(signers);
331
332        // The mock node is mainnet (chain id 1); the caller pins a different chain.
333        let tx_req = TransactionRequest {
334            from: Some(address),
335            to: Some(address.into()),
336            gas: Some(90_000),
337            gas_price: Some(1_000_000_000),
338            chain_id: Some(999),
339            ..Default::default()
340        };
341
342        let err = eth_api
343            .send_transaction_request(tx_req)
344            .await
345            .expect_err("a chain id that is not the node's must be rejected, not rewritten");
346        assert!(
347            err.to_string().contains("chainId does not match node's"),
348            "unexpected error: {err}"
349        );
350        assert!(eth_api.pool().is_empty(), "no transaction should have been submitted");
351    }
352
353    #[tokio::test]
354    async fn send_transaction_accepts_matching_chain_id() {
355        let signers = DevSigner::random_signers(1);
356        let address = signers[0].accounts()[0];
357        let accounts = AddressMap::from_iter([(
358            address,
359            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
360        )]);
361        let eth_api = mock_eth_api(accounts);
362        eth_api.signers().write().extend(signers);
363
364        let tx_req = TransactionRequest {
365            from: Some(address),
366            to: Some(address.into()),
367            gas: Some(90_000),
368            gas_price: Some(1_000_000_000),
369            chain_id: Some(1),
370            ..Default::default()
371        };
372
373        let hash = eth_api
374            .send_transaction_request(tx_req)
375            .await
376            .expect("a matching chain id must still be accepted");
377        let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
378        assert_eq!(pooled.transaction.chain_id(), Some(1));
379    }
380
381    #[tokio::test]
382    async fn test_fill_transaction_fills_chain_id() {
383        let address = Address::random();
384        let accounts = AddressMap::from_iter([(
385            address,
386            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), // 10 ETH
387        )]);
388
389        let eth_api = mock_eth_api(accounts);
390
391        let tx_req = TransactionRequest {
392            from: Some(address),
393            to: Some(Address::random().into()),
394            gas: Some(21_000),
395            ..Default::default()
396        };
397
398        let filled =
399            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
400
401        // Should fill with the chain id from provider
402        assert!(filled.tx.chain_id().is_some());
403    }
404
405    #[tokio::test]
406    async fn test_fill_transaction_fills_nonce() {
407        let address = Address::random();
408        let nonce = 42u64;
409
410        let accounts = AddressMap::from_iter([(
411            address,
412            ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), // 1 ETH
413        )]);
414
415        let eth_api = mock_eth_api(accounts);
416
417        let tx_req = TransactionRequest {
418            from: Some(address),
419            to: Some(Address::random().into()),
420            value: Some(U256::from(1000)),
421            gas: Some(21_000),
422            ..Default::default()
423        };
424
425        let filled =
426            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
427
428        assert_eq!(filled.tx.nonce(), nonce);
429    }
430
431    #[tokio::test]
432    async fn test_fill_transaction_preserves_provided_fields() {
433        let address = Address::random();
434        let provided_nonce = 100u64;
435        let provided_gas_limit = 50_000u64;
436
437        let accounts = AddressMap::from_iter([(
438            address,
439            ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
440        )]);
441
442        let eth_api = mock_eth_api(accounts);
443
444        let tx_req = TransactionRequest {
445            from: Some(address),
446            to: Some(Address::random().into()),
447            value: Some(U256::from(1000)),
448            nonce: Some(provided_nonce),
449            gas: Some(provided_gas_limit),
450            ..Default::default()
451        };
452
453        let filled =
454            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
455
456        // Should preserve the provided nonce and gas limit
457        assert_eq!(filled.tx.nonce(), provided_nonce);
458        assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
459    }
460
461    #[tokio::test]
462    async fn test_fill_transaction_fills_all_missing_fields() {
463        let address = Address::random();
464
465        let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
466        let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
467
468        let eth_api = mock_eth_api(accounts);
469
470        // Create a simple transfer transaction
471        let tx_req = TransactionRequest {
472            from: Some(address),
473            to: Some(Address::random().into()),
474            ..Default::default()
475        };
476
477        let filled =
478            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
479
480        assert!(filled.tx.is_eip1559());
481    }
482
483    #[tokio::test]
484    async fn test_fill_transaction_eip4844_blob_fee() {
485        let address = Address::random();
486        let accounts = AddressMap::from_iter([(
487            address,
488            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
489        )]);
490
491        let eth_api = mock_eth_api(accounts);
492
493        let mut builder = SidecarBuilder::<SimpleCoder>::new();
494        builder.ingest(b"dummy blob");
495
496        // EIP-4844 blob transaction with versioned hashes but no blob fee
497        let tx_req = TransactionRequest {
498            from: Some(address),
499            to: Some(Address::random().into()),
500            sidecar: Some(BlobTransactionSidecarVariant::from(
501                builder.build::<BlobTransactionSidecar>().unwrap(),
502            )),
503            ..Default::default()
504        };
505
506        let filled =
507            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
508
509        // Blob transaction should have max_fee_per_blob_gas filled
510        assert!(
511            filled.tx.max_fee_per_blob_gas().is_some(),
512            "max_fee_per_blob_gas should be filled for blob tx"
513        );
514        assert!(
515            filled.tx.blob_versioned_hashes().is_some(),
516            "blob_versioned_hashes should be preserved"
517        );
518    }
519
520    #[tokio::test]
521    async fn test_fill_transaction_eip4844_preserves_blob_fee() {
522        let address = Address::random();
523        let accounts = AddressMap::from_iter([(
524            address,
525            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
526        )]);
527
528        let eth_api = mock_eth_api(accounts);
529
530        let provided_blob_fee = 5000000u128;
531
532        let mut builder = SidecarBuilder::<SimpleCoder>::new();
533        builder.ingest(b"dummy blob");
534
535        // EIP-4844 blob transaction with blob fee already set
536        let tx_req = TransactionRequest {
537            from: Some(address),
538            to: Some(Address::random().into()),
539            transaction_type: Some(3), // EIP-4844
540            sidecar: Some(BlobTransactionSidecarVariant::from(
541                builder.build::<BlobTransactionSidecar>().unwrap(),
542            )),
543            max_fee_per_blob_gas: Some(provided_blob_fee), // Already set
544            ..Default::default()
545        };
546
547        let filled =
548            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
549
550        // Should preserve the provided blob fee
551        assert_eq!(
552            filled.tx.max_fee_per_blob_gas(),
553            Some(provided_blob_fee),
554            "should preserve provided max_fee_per_blob_gas"
555        );
556    }
557
558    #[tokio::test]
559    async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
560        let address = Address::random();
561        let accounts = AddressMap::from_iter([(
562            address,
563            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
564        )]);
565
566        let eth_api = mock_eth_api(accounts);
567
568        // EIP-1559 transaction without blob fields
569        let tx_req = TransactionRequest {
570            from: Some(address),
571            to: Some(Address::random().into()),
572            transaction_type: Some(2), // EIP-1559
573            ..Default::default()
574        };
575
576        let filled =
577            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
578
579        // Non-blob transaction should NOT have blob fee filled
580        assert!(
581            filled.tx.max_fee_per_blob_gas().is_none(),
582            "max_fee_per_blob_gas should not be set for non-blob tx"
583        );
584    }
585}