Skip to content

Commit 44caf60

Browse files
authored
test(optimism): Test that sequence stops before a gap (#18228)
1 parent 358b61b commit 44caf60

File tree

5 files changed

+192
-118
lines changed

5 files changed

+192
-118
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.

crates/optimism/flashblocks/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ eyre.workspace = true
4747

4848
[dev-dependencies]
4949
test-case.workspace = true
50+
alloy-consensus.workspace = true

crates/optimism/flashblocks/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub use service::FlashBlockService;
88
pub use ws::{WsConnect, WsFlashBlockStream};
99

1010
mod payload;
11+
mod sequence;
1112
mod service;
1213
mod ws;
1314

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use crate::{ExecutionPayloadBaseV1, FlashBlock};
2+
use alloy_eips::eip2718::WithEncoded;
3+
use reth_primitives_traits::{Recovered, SignedTransaction};
4+
use std::collections::BTreeMap;
5+
use tracing::trace;
6+
7+
/// An ordered B-tree keeping the track of a sequence of [`FlashBlock`]s by their indices.
8+
#[derive(Debug)]
9+
pub(crate) struct FlashBlockSequence<T> {
10+
/// tracks the individual flashblocks in order
11+
///
12+
/// With a blocktime of 2s and flashblock tick-rate of 200ms plus one extra flashblock per new
13+
/// pending block, we expect 11 flashblocks per slot.
14+
inner: BTreeMap<u64, PreparedFlashBlock<T>>,
15+
}
16+
17+
impl<T> FlashBlockSequence<T>
18+
where
19+
T: SignedTransaction,
20+
{
21+
pub(crate) const fn new() -> Self {
22+
Self { inner: BTreeMap::new() }
23+
}
24+
25+
/// Inserts a new block into the sequence.
26+
///
27+
/// A [`FlashBlock`] with index 0 resets the set.
28+
pub(crate) fn insert(&mut self, flashblock: FlashBlock) -> eyre::Result<()> {
29+
if flashblock.index == 0 {
30+
trace!(number=%flashblock.block_number(), "Tracking new flashblock sequence");
31+
// Flash block at index zero resets the whole state
32+
self.clear();
33+
self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?);
34+
return Ok(())
35+
}
36+
37+
// only insert if we we previously received the same block, assume we received index 0
38+
if self.block_number() == Some(flashblock.metadata.block_number) {
39+
trace!(number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock");
40+
self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?);
41+
} else {
42+
trace!(number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following");
43+
}
44+
45+
Ok(())
46+
}
47+
48+
/// Returns the first block number
49+
pub(crate) fn block_number(&self) -> Option<u64> {
50+
Some(self.inner.values().next()?.block().metadata.block_number)
51+
}
52+
53+
/// Returns the payload base of the first tracked flashblock.
54+
pub(crate) fn payload_base(&self) -> Option<ExecutionPayloadBaseV1> {
55+
self.inner.values().next()?.block().base.clone()
56+
}
57+
58+
/// Iterator over sequence of executable transactions.
59+
///
60+
/// A flashblocks is not ready if there's missing previous flashblocks, i.e. there's a gap in
61+
/// the sequence
62+
///
63+
/// Note: flashblocks start at `index 0`.
64+
pub(crate) fn ready_transactions(
65+
&self,
66+
) -> impl Iterator<Item = WithEncoded<Recovered<T>>> + '_ {
67+
self.inner
68+
.values()
69+
.enumerate()
70+
.take_while(|(idx, block)| {
71+
// flashblock index 0 is the first flashblock
72+
block.block().index == *idx as u64
73+
})
74+
.flat_map(|(_, block)| block.txs.clone())
75+
}
76+
77+
/// Returns the number of tracked flashblocks.
78+
pub(crate) fn count(&self) -> usize {
79+
self.inner.len()
80+
}
81+
82+
fn clear(&mut self) {
83+
self.inner.clear();
84+
}
85+
}
86+
87+
#[derive(Debug)]
88+
struct PreparedFlashBlock<T> {
89+
/// The prepared transactions, ready for execution
90+
txs: Vec<WithEncoded<Recovered<T>>>,
91+
/// The tracked flashblock
92+
block: FlashBlock,
93+
}
94+
95+
impl<T> PreparedFlashBlock<T> {
96+
const fn block(&self) -> &FlashBlock {
97+
&self.block
98+
}
99+
}
100+
101+
impl<T> PreparedFlashBlock<T>
102+
where
103+
T: SignedTransaction,
104+
{
105+
/// Creates a flashblock that is ready for execution by preparing all transactions
106+
///
107+
/// Returns an error if decoding or signer recovery fails.
108+
fn new(block: FlashBlock) -> eyre::Result<Self> {
109+
let mut txs = Vec::with_capacity(block.diff.transactions.len());
110+
for encoded in block.diff.transactions.iter().cloned() {
111+
let tx = T::decode_2718_exact(encoded.as_ref())?;
112+
let signer = tx.try_recover()?;
113+
let tx = WithEncoded::new(encoded, tx.with_signer(signer));
114+
txs.push(tx);
115+
}
116+
117+
Ok(Self { txs, block })
118+
}
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
use crate::ExecutionPayloadFlashblockDeltaV1;
125+
use alloy_consensus::{
126+
transaction::SignerRecoverable, EthereumTxEnvelope, EthereumTypedTransaction, TxEip1559,
127+
};
128+
use alloy_eips::Encodable2718;
129+
use alloy_primitives::{hex, Signature, TxKind, U256};
130+
131+
#[test]
132+
fn test_sequence_stops_before_gap() {
133+
let mut sequence = FlashBlockSequence::new();
134+
let tx = EthereumTxEnvelope::new_unhashed(
135+
EthereumTypedTransaction::<TxEip1559>::Eip1559(TxEip1559 {
136+
chain_id: 4,
137+
nonce: 26u64,
138+
max_priority_fee_per_gas: 1500000000,
139+
max_fee_per_gas: 1500000013,
140+
gas_limit: 21_000u64,
141+
to: TxKind::Call(hex!("61815774383099e24810ab832a5b2a5425c154d5").into()),
142+
value: U256::from(3000000000000000000u64),
143+
input: Default::default(),
144+
access_list: Default::default(),
145+
}),
146+
Signature::new(
147+
U256::from_be_bytes(hex!(
148+
"59e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafd"
149+
)),
150+
U256::from_be_bytes(hex!(
151+
"016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469"
152+
)),
153+
true,
154+
),
155+
);
156+
let tx = Recovered::new_unchecked(tx.clone(), tx.recover_signer_unchecked().unwrap());
157+
158+
sequence
159+
.insert(FlashBlock {
160+
payload_id: Default::default(),
161+
index: 0,
162+
base: None,
163+
diff: ExecutionPayloadFlashblockDeltaV1 {
164+
transactions: vec![tx.encoded_2718().into()],
165+
..Default::default()
166+
},
167+
metadata: Default::default(),
168+
})
169+
.unwrap();
170+
171+
sequence
172+
.insert(FlashBlock {
173+
payload_id: Default::default(),
174+
index: 2,
175+
base: None,
176+
diff: Default::default(),
177+
metadata: Default::default(),
178+
})
179+
.unwrap();
180+
181+
let actual_txs: Vec<_> = sequence.ready_transactions().collect();
182+
let expected_txs = vec![WithEncoded::new(tx.encoded_2718().into(), tx)];
183+
184+
assert_eq!(actual_txs, expected_txs);
185+
}
186+
}

crates/optimism/flashblocks/src/service.rs

Lines changed: 3 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::{ExecutionPayloadBaseV1, FlashBlock};
2-
use alloy_eips::{eip2718::WithEncoded, BlockNumberOrTag};
1+
use crate::{sequence::FlashBlockSequence, ExecutionPayloadBaseV1, FlashBlock};
2+
use alloy_eips::BlockNumberOrTag;
33
use alloy_primitives::B256;
44
use futures_util::{FutureExt, Stream, StreamExt};
55
use reth_chain_state::{CanonStateNotifications, CanonStateSubscriptions, ExecutedBlock};
@@ -9,14 +9,11 @@ use reth_evm::{
99
ConfigureEvm,
1010
};
1111
use reth_execution_types::ExecutionOutcome;
12-
use reth_primitives_traits::{
13-
AlloyBlockHeader, BlockTy, HeaderTy, NodePrimitives, ReceiptTy, Recovered, SignedTransaction,
14-
};
12+
use reth_primitives_traits::{AlloyBlockHeader, BlockTy, HeaderTy, NodePrimitives, ReceiptTy};
1513
use reth_revm::{cached::CachedReads, database::StateProviderDatabase, db::State};
1614
use reth_rpc_eth_types::{EthApiError, PendingBlock};
1715
use reth_storage_api::{noop::NoopProvider, BlockReaderIdExt, StateProviderFactory};
1816
use std::{
19-
collections::BTreeMap,
2017
pin::Pin,
2118
sync::Arc,
2219
task::{Context, Poll},
@@ -238,115 +235,3 @@ where
238235
Poll::Pending
239236
}
240237
}
241-
242-
/// Simple wrapper around an ordered B-tree to keep track of a sequence of flashblocks by index.
243-
#[derive(Debug)]
244-
struct FlashBlockSequence<T> {
245-
/// tracks the individual flashblocks in order
246-
///
247-
/// With a blocktime of 2s and flashblock tickrate of ~200ms, we expect 10 or 11 flashblocks
248-
/// per slot.
249-
inner: BTreeMap<u64, PreparedFlashBlock<T>>,
250-
}
251-
252-
impl<T> FlashBlockSequence<T>
253-
where
254-
T: SignedTransaction,
255-
{
256-
const fn new() -> Self {
257-
Self { inner: BTreeMap::new() }
258-
}
259-
260-
/// Inserts a new block into the sequence.
261-
///
262-
/// A [`FlashBlock`] with index 0 resets the set.
263-
fn insert(&mut self, flashblock: FlashBlock) -> eyre::Result<()> {
264-
if flashblock.index == 0 {
265-
trace!(number=%flashblock.block_number(), "Tracking new flashblock sequence");
266-
// Flash block at index zero resets the whole state
267-
self.clear();
268-
self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?);
269-
return Ok(())
270-
}
271-
272-
// only insert if we we previously received the same block, assume we received index 0
273-
if self.block_number() == Some(flashblock.metadata.block_number) {
274-
trace!(number=%flashblock.block_number(), index = %flashblock.index, block_count = self.inner.len() ,"Received followup flashblock");
275-
self.inner.insert(flashblock.index, PreparedFlashBlock::new(flashblock)?);
276-
} else {
277-
trace!(number=%flashblock.block_number(), index = %flashblock.index, current=?self.block_number() ,"Ignoring untracked flashblock following");
278-
}
279-
280-
Ok(())
281-
}
282-
283-
/// Returns the number of tracked flashblocks.
284-
fn count(&self) -> usize {
285-
self.inner.len()
286-
}
287-
288-
/// Returns the first block number
289-
fn block_number(&self) -> Option<u64> {
290-
Some(self.inner.values().next()?.block().metadata.block_number)
291-
}
292-
293-
/// Returns the payload base of the first tracked flashblock.
294-
fn payload_base(&self) -> Option<ExecutionPayloadBaseV1> {
295-
self.inner.values().next()?.block().base.clone()
296-
}
297-
298-
fn clear(&mut self) {
299-
self.inner.clear();
300-
}
301-
302-
/// Iterator over sequence of executable transactions.
303-
///
304-
/// A flashblocks is not ready if there's missing previous flashblocks, i.e. there's a gap in
305-
/// the sequence
306-
///
307-
/// Note: flashblocks start at `index 0`.
308-
fn ready_transactions(&self) -> impl Iterator<Item = WithEncoded<Recovered<T>>> + '_ {
309-
self.inner
310-
.values()
311-
.enumerate()
312-
.take_while(|(idx, block)| {
313-
// flashblock index 0 is the first flashblock
314-
block.block().index == *idx as u64
315-
})
316-
.flat_map(|(_, block)| block.txs.clone())
317-
}
318-
}
319-
320-
#[derive(Debug)]
321-
struct PreparedFlashBlock<T> {
322-
/// The prepared transactions, ready for execution
323-
txs: Vec<WithEncoded<Recovered<T>>>,
324-
/// The tracked flashblock
325-
block: FlashBlock,
326-
}
327-
328-
impl<T> PreparedFlashBlock<T> {
329-
const fn block(&self) -> &FlashBlock {
330-
&self.block
331-
}
332-
}
333-
334-
impl<T> PreparedFlashBlock<T>
335-
where
336-
T: SignedTransaction,
337-
{
338-
/// Creates a flashblock that is ready for execution by preparing all transactions
339-
///
340-
/// Returns an error if decoding or signer recovery fails.
341-
fn new(block: FlashBlock) -> eyre::Result<Self> {
342-
let mut txs = Vec::with_capacity(block.diff.transactions.len());
343-
for encoded in block.diff.transactions.iter().cloned() {
344-
let tx = T::decode_2718_exact(encoded.as_ref())?;
345-
let signer = tx.try_recover()?;
346-
let tx = WithEncoded::new(encoded, tx.with_signer(signer));
347-
txs.push(tx);
348-
}
349-
350-
Ok(Self { txs, block })
351-
}
352-
}

0 commit comments

Comments
 (0)