reth_optimism_flashblocks/
sequence.rs

1use crate::{ExecutionPayloadBaseV1, FlashBlock};
2use alloy_eips::eip2718::WithEncoded;
3use reth_primitives_traits::{Recovered, SignedTransaction};
4use std::collections::BTreeMap;
5use tracing::trace;
6
7/// An ordered B-tree keeping the track of a sequence of [`FlashBlock`]s by their indices.
8#[derive(Debug)]
9pub(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
17impl<T> FlashBlockSequence<T>
18where
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 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)]
88struct PreparedFlashBlock<T> {
89    /// The prepared transactions, ready for execution
90    txs: Vec<WithEncoded<Recovered<T>>>,
91    /// The tracked flashblock
92    block: FlashBlock,
93}
94
95impl<T> PreparedFlashBlock<T> {
96    const fn block(&self) -> &FlashBlock {
97        &self.block
98    }
99}
100
101impl<T> PreparedFlashBlock<T>
102where
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)]
122mod 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}