Skip to content

Commit 54a2fe1

Browse files
committed
feat(rpc): return flashblock state for pending block queries
1 parent 86b62ae commit 54a2fe1

File tree

4 files changed

+150
-2
lines changed

4 files changed

+150
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ async-trait = "0.1.88"
3232
modular-bitfield = "0.11.2"
3333
reth = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
3434
reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
35+
reth-chain-state = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
3536
reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
3637
reth-cli = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
3738
reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", rev = "536bebfcd" }
@@ -118,6 +119,7 @@ ignored = ["modular-bitfield"]
118119
reth = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
119120
reth-rpc = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
120121
reth-basic-payload-builder = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
122+
reth-chain-state = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
121123
reth-chainspec = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
122124
reth-cli = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }
123125
reth-cli-commands = { git = "https://github.com/rezbera/reth", branch = "rezbera/modular-flashblocks" }

src/rpc/api.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use reth::{
2121
pool::{BlockingTaskGuard, BlockingTaskPool},
2222
},
2323
};
24+
use reth_chain_state::BlockState;
2425
use reth_optimism_flashblocks::{FlashBlockBuildInfo, FlashblocksListeners, PendingFlashBlock};
2526
use reth_primitives_traits::{Recovered, WithEncoded};
2627
use reth_rpc_eth_api::{
@@ -35,6 +36,7 @@ use reth_rpc_eth_types::{
3536
EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle, PendingBlock,
3637
builder::config::PendingBlockKind, error::FromEvmError,
3738
};
39+
use reth_storage_api::StateProviderFactory;
3840
use reth_transaction_pool::PoolPooledTx;
3941
use std::{sync::Arc, time::Duration};
4042
use tokio::time;
@@ -700,4 +702,29 @@ where
700702
fn pending_block_kind(&self) -> PendingBlockKind {
701703
self.inner.pending_block_kind()
702704
}
705+
706+
/// Returns a state provider for the pending flashblock state.
707+
///
708+
/// This overlays the flashblock execution output on top of the parent block's historical state,
709+
/// allowing RPC methods like `eth_getBalance`, `eth_getCode`, etc. to query pending state
710+
/// when called with the "pending" block tag.
711+
async fn local_pending_state(
712+
&self,
713+
) -> Result<Option<reth_storage_api::StateProviderBox>, Self::Error>
714+
where
715+
Self: SpawnBlocking,
716+
{
717+
let Ok(Some(pending_block)) = self.pending_flashblock().await else {
718+
return Ok(None);
719+
};
720+
721+
let latest_historical = self
722+
.provider()
723+
.history_by_block_hash(pending_block.block().parent_hash())
724+
.map_err(Into::<EthApiError>::into)?;
725+
726+
let state = BlockState::from(pending_block);
727+
728+
Ok(Some(Box::new(state.state_provider(latest_historical))))
729+
}
703730
}

tests/e2e/flashblocks.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
66
use crate::e2e::{setup_test_boilerplate, test_signer};
77
use alloy_consensus::BlockHeader;
8-
use alloy_eips::eip2718::Encodable2718;
9-
use alloy_primitives::{Address, B256, Bloom, Bytes, U256};
8+
use alloy_eips::{BlockId, eip2718::Encodable2718};
9+
use alloy_primitives::{Address, B256, Bloom, Bytes, TxKind, U256};
1010
use alloy_provider::Provider;
11+
use alloy_rpc_types_eth::TransactionRequest;
1112
use bera_reth::{
1213
engine::validator::BerachainEngineValidatorBuilder,
1314
flashblocks::{
@@ -233,6 +234,123 @@ async fn test_rpc_returns_flashblock_pending_receipt() -> eyre::Result<()> {
233234
Ok(())
234235
}
235236

237+
/// Tests that state-reading RPC methods return flashblock pending state.
238+
///
239+
/// This test verifies that when a flashblock contains a transfer transaction:
240+
/// - `eth_getBalance` returns the recipient's updated balance
241+
/// - `eth_getTransactionCount` returns the sender's incremented nonce
242+
#[tokio::test]
243+
async fn test_rpc_returns_flashblock_pending_state() -> eyre::Result<()> {
244+
let (tasks, chain_spec) = setup_test_boilerplate().await?;
245+
let executor = tasks.executor();
246+
247+
let (pending_tx, pending_rx) = watch::channel(None);
248+
let (in_progress_tx, in_progress_rx) = watch::channel(None);
249+
let (unused_sequence_tx, _) =
250+
broadcast::channel::<FlashBlockCompleteSequence<BerachainFlashblockPayload>>(1);
251+
let (unused_received_tx, _) = broadcast::channel::<Arc<BerachainFlashblockPayload>>(1);
252+
253+
let listeners: FlashblocksListeners<BerachainPrimitives, BerachainFlashblockPayload> =
254+
FlashblocksListeners::new(
255+
pending_rx,
256+
unused_sequence_tx,
257+
in_progress_rx,
258+
unused_received_tx,
259+
);
260+
261+
let eth_api_builder = BerachainEthApiBuilder::default().with_flashblocks_listeners(listeners);
262+
let add_ons = BerachainAddOns::<_, _, BerachainEngineValidatorBuilder>::new(eth_api_builder);
263+
264+
let node_config = NodeConfig::new(chain_spec.clone())
265+
.with_unused_ports()
266+
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http());
267+
268+
let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)
269+
.testing_node(executor.clone())
270+
.with_types::<BerachainNode>()
271+
.with_components(BerachainNode::default().components_builder())
272+
.with_add_ons(add_ons)
273+
.launch()
274+
.await?;
275+
276+
let (fb_tx, fb_rx) = tokio::sync::mpsc::channel::<BerachainFlashblockPayload>(128);
277+
let stream = MockFlashblockStream { rx: fb_rx };
278+
279+
let service = FlashBlockService::new(
280+
stream,
281+
node.evm_config.clone(),
282+
node.provider().clone(),
283+
executor.clone(),
284+
false,
285+
);
286+
287+
let mut service_in_progress_rx = service.subscribe_in_progress();
288+
289+
executor.spawn_critical(
290+
"flashblock-service",
291+
Box::pin(async move {
292+
service.run(pending_tx).await;
293+
}),
294+
);
295+
296+
let latest = node.provider().latest_header()?.expect("should have genesis");
297+
let latest_hash = latest.hash();
298+
let next_block = latest.number() + 1;
299+
let next_timestamp = latest.timestamp() + 2;
300+
301+
// Create a transfer transaction to a fresh recipient address
302+
let recipient = Address::random();
303+
let transfer_value = U256::from(100);
304+
305+
let signer = test_signer()?;
306+
let sender = signer.address();
307+
let chain_id = chain_spec.chain_id();
308+
309+
// Build a transfer transaction with a specific recipient
310+
let tx_request = TransactionRequest {
311+
nonce: Some(0),
312+
value: Some(transfer_value),
313+
to: Some(TxKind::Call(recipient)),
314+
gas: Some(21000),
315+
max_fee_per_gas: Some(20e9 as u128),
316+
max_priority_fee_per_gas: Some(20e9 as u128),
317+
chain_id: Some(chain_id),
318+
..Default::default()
319+
};
320+
let tx = TransactionTestContext::sign_tx(signer, tx_request).await;
321+
let tx_bytes = Bytes::from(tx.encoded_2718());
322+
323+
let payload_id = PayloadId::new([2u8; 8]);
324+
let mut fb0 = create_test_flashblock(0, next_block, payload_id, latest_hash, next_timestamp);
325+
fb0.diff.transactions = vec![tx_bytes];
326+
fb0.diff.gas_used = 21000;
327+
328+
// Inject the flashblock
329+
fb_tx.send(fb0).await?;
330+
331+
// Wait for service to signal it's building
332+
tokio::time::timeout(Duration::from_millis(500), service_in_progress_rx.changed()).await??;
333+
in_progress_tx.send(*service_in_progress_rx.borrow())?;
334+
335+
// Give the service time to build and publish the pending block
336+
tokio::time::sleep(Duration::from_millis(100)).await;
337+
338+
let rpc_url =
339+
format!("http://127.0.0.1:{}", node.rpc_server_handle().http_local_addr().unwrap().port());
340+
let client = alloy_provider::ProviderBuilder::new().connect(&rpc_url).await?;
341+
342+
// Check recipient balance with "pending" block tag - should reflect the pending transfer
343+
let balance = client.get_balance(recipient).block_id(BlockId::pending()).await?;
344+
assert_eq!(balance, transfer_value);
345+
346+
// Verify that the nonce is 1 more in flashblock state
347+
let nonce_latest = client.get_transaction_count(sender).block_id(BlockId::latest()).await?;
348+
let nonce_pending = client.get_transaction_count(sender).block_id(BlockId::pending()).await?;
349+
assert_eq!(nonce_pending, nonce_latest + 1);
350+
351+
Ok(())
352+
}
353+
236354
/// Tests that flashblocks with invalid parent hashes are rejected.
237355
#[tokio::test]
238356
async fn test_flashblock_rejects_invalid_parent_hash() -> eyre::Result<()> {

0 commit comments

Comments
 (0)