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, Recovered, 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, PoolPooledTx, 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    async fn send_transaction(
40        &self,
41        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
42    ) -> Result<B256, Self::Error> {
43        let (tx, recovered) = tx.split();
44        let mut pool_transaction =
45            <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
46
47        // TODO: remove this after Osaka transition
48        // Convert legacy blob sidecars to EIP-7594 format
49        if pool_transaction.is_eip4844() {
50            let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
51                return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
52                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
53                )));
54            };
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(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        // submit the transaction to the pool with a `Local` origin
118        let AddedTransactionOutcome { hash, .. } =
119            self.inner.add_pool_transaction(pool_transaction).await?;
120
121        Ok(hash)
122    }
123}
124
125impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
126where
127    N: RpcNodeCore,
128    EthApiError: FromEvmError<N::Evm>,
129    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
130{
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::eth::helpers::types::EthRpcConverter;
137    use alloy_consensus::{
138        BlobTransactionSidecar, Block, Header, SidecarBuilder, SimpleCoder, Transaction,
139    };
140    use alloy_primitives::{Address, U256};
141    use alloy_rpc_types_eth::request::TransactionRequest;
142    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
143    use reth_evm_ethereum::EthEvmConfig;
144    use reth_network_api::noop::NoopNetwork;
145    use reth_provider::{
146        test_utils::{ExtendedAccount, MockEthProvider},
147        ChainSpecProvider,
148    };
149    use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
150    use reth_transaction_pool::test_utils::{testing_pool, TestPool};
151    use revm_primitives::Bytes;
152    use std::collections::HashMap;
153
154    fn mock_eth_api(
155        accounts: HashMap<Address, ExtendedAccount>,
156    ) -> EthApi<
157        RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
158        EthRpcConverter<ChainSpec>,
159    > {
160        let mock_provider = MockEthProvider::default()
161            .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
162        mock_provider.extend_accounts(accounts);
163
164        let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
165        let pool = testing_pool();
166
167        let genesis_header = Header {
168            number: 0,
169            gas_limit: 30_000_000,
170            timestamp: 1,
171            excess_blob_gas: Some(0),
172            base_fee_per_gas: Some(1000000000),
173            blob_gas_used: Some(0),
174            ..Default::default()
175        };
176
177        let genesis_hash = B256::ZERO;
178        mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
179
180        EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config).build()
181    }
182
183    #[tokio::test]
184    async fn send_raw_transaction() {
185        let eth_api = mock_eth_api(Default::default());
186        let pool = eth_api.pool();
187
188        // https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
189        let tx_1 = Bytes::from(hex!(
190            "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
191        ));
192
193        let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
194        assert_eq!(
195            pool.len(),
196            1,
197            "expect 1 transaction in the pool, but pool size is {}",
198            pool.len()
199        );
200
201        // https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
202        let tx_2 = Bytes::from(hex!(
203            "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
204        ));
205
206        let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
207        assert_eq!(
208            pool.len(),
209            2,
210            "expect 2 transactions in the pool, but pool size is {}",
211            pool.len()
212        );
213
214        assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
215        assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
216    }
217
218    #[tokio::test]
219    async fn test_fill_transaction_fills_chain_id() {
220        let address = Address::random();
221        let accounts = HashMap::from([(
222            address,
223            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), // 10 ETH
224        )]);
225
226        let eth_api = mock_eth_api(accounts);
227
228        let tx_req = TransactionRequest {
229            from: Some(address),
230            to: Some(Address::random().into()),
231            gas: Some(21_000),
232            ..Default::default()
233        };
234
235        let filled =
236            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
237
238        // Should fill with the chain id from provider
239        assert!(filled.tx.chain_id().is_some());
240    }
241
242    #[tokio::test]
243    async fn test_fill_transaction_fills_nonce() {
244        let address = Address::random();
245        let nonce = 42u64;
246
247        let accounts = HashMap::from([(
248            address,
249            ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), // 1 ETH
250        )]);
251
252        let eth_api = mock_eth_api(accounts);
253
254        let tx_req = TransactionRequest {
255            from: Some(address),
256            to: Some(Address::random().into()),
257            value: Some(U256::from(1000)),
258            gas: Some(21_000),
259            ..Default::default()
260        };
261
262        let filled =
263            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
264
265        assert_eq!(filled.tx.nonce(), nonce);
266    }
267
268    #[tokio::test]
269    async fn test_fill_transaction_preserves_provided_fields() {
270        let address = Address::random();
271        let provided_nonce = 100u64;
272        let provided_gas_limit = 50_000u64;
273
274        let accounts = HashMap::from([(
275            address,
276            ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
277        )]);
278
279        let eth_api = mock_eth_api(accounts);
280
281        let tx_req = TransactionRequest {
282            from: Some(address),
283            to: Some(Address::random().into()),
284            value: Some(U256::from(1000)),
285            nonce: Some(provided_nonce),
286            gas: Some(provided_gas_limit),
287            ..Default::default()
288        };
289
290        let filled =
291            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
292
293        // Should preserve the provided nonce and gas limit
294        assert_eq!(filled.tx.nonce(), provided_nonce);
295        assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
296    }
297
298    #[tokio::test]
299    async fn test_fill_transaction_fills_all_missing_fields() {
300        let address = Address::random();
301
302        let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
303        let accounts = HashMap::from([(address, ExtendedAccount::new(5, balance))]);
304
305        let eth_api = mock_eth_api(accounts);
306
307        // Create a simple transfer transaction
308        let tx_req = TransactionRequest {
309            from: Some(address),
310            to: Some(Address::random().into()),
311            ..Default::default()
312        };
313
314        let filled =
315            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
316
317        assert!(filled.tx.is_eip1559());
318    }
319
320    #[tokio::test]
321    async fn test_fill_transaction_eip4844_blob_fee() {
322        let address = Address::random();
323        let accounts = HashMap::from([(
324            address,
325            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
326        )]);
327
328        let eth_api = mock_eth_api(accounts);
329
330        let mut builder = SidecarBuilder::<SimpleCoder>::new();
331        builder.ingest(b"dummy blob");
332
333        // EIP-4844 blob transaction with versioned hashes but no blob fee
334        let tx_req = TransactionRequest {
335            from: Some(address),
336            to: Some(Address::random().into()),
337            sidecar: Some(BlobTransactionSidecarVariant::from(
338                builder.build::<BlobTransactionSidecar>().unwrap(),
339            )),
340            ..Default::default()
341        };
342
343        let filled =
344            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
345
346        // Blob transaction should have max_fee_per_blob_gas filled
347        assert!(
348            filled.tx.max_fee_per_blob_gas().is_some(),
349            "max_fee_per_blob_gas should be filled for blob tx"
350        );
351        assert!(
352            filled.tx.blob_versioned_hashes().is_some(),
353            "blob_versioned_hashes should be preserved"
354        );
355    }
356
357    #[tokio::test]
358    async fn test_fill_transaction_eip4844_preserves_blob_fee() {
359        let address = Address::random();
360        let accounts = HashMap::from([(
361            address,
362            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
363        )]);
364
365        let eth_api = mock_eth_api(accounts);
366
367        let provided_blob_fee = 5000000u128;
368
369        let mut builder = SidecarBuilder::<SimpleCoder>::new();
370        builder.ingest(b"dummy blob");
371
372        // EIP-4844 blob transaction with blob fee already set
373        let tx_req = TransactionRequest {
374            from: Some(address),
375            to: Some(Address::random().into()),
376            transaction_type: Some(3), // EIP-4844
377            sidecar: Some(BlobTransactionSidecarVariant::from(
378                builder.build::<BlobTransactionSidecar>().unwrap(),
379            )),
380            max_fee_per_blob_gas: Some(provided_blob_fee), // Already set
381            ..Default::default()
382        };
383
384        let filled =
385            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
386
387        // Should preserve the provided blob fee
388        assert_eq!(
389            filled.tx.max_fee_per_blob_gas(),
390            Some(provided_blob_fee),
391            "should preserve provided max_fee_per_blob_gas"
392        );
393    }
394
395    #[tokio::test]
396    async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
397        let address = Address::random();
398        let accounts = HashMap::from([(
399            address,
400            ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
401        )]);
402
403        let eth_api = mock_eth_api(accounts);
404
405        // EIP-1559 transaction without blob fields
406        let tx_req = TransactionRequest {
407            from: Some(address),
408            to: Some(Address::random().into()),
409            transaction_type: Some(2), // EIP-1559
410            ..Default::default()
411        };
412
413        let filled =
414            eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
415
416        // Non-blob transaction should NOT have blob fee filled
417        assert!(
418            filled.tx.max_fee_per_blob_gas().is_none(),
419            "max_fee_per_blob_gas should not be set for non-blob tx"
420        );
421    }
422}