signet_sim/
built.rs

1use crate::{outcome::SimulatedItem, SimItem};
2use alloy::{
3    consensus::{SidecarBuilder, SidecarCoder, TxEnvelope},
4    eips::Decodable2718,
5    primitives::{keccak256, Bytes, B256},
6    rlp::Buf,
7};
8use core::fmt;
9use signet_bundle::SignetEthBundle;
10use signet_types::SignedFill;
11use signet_zenith::{encode_txns, Alloy2718Coder};
12use std::sync::OnceLock;
13use tracing::{error, trace};
14
15/// A block that has been built by the simulator.
16#[derive(Clone, Default)]
17pub struct BuiltBlock {
18    /// The host fill actions.
19    pub(crate) host_fills: Vec<SignedFill>,
20    /// Transactions in the block.
21    pub(crate) transactions: Vec<TxEnvelope>,
22    /// The block number for the block.
23    pub(crate) block_number: u64,
24
25    /// The amount of gas used by the block so far
26    pub(crate) gas_used: u64,
27
28    /// Memoized raw encoding of the block.
29    pub(crate) raw_encoding: OnceLock<Bytes>,
30    /// Memoized hash of the block.
31    pub(crate) hash: OnceLock<B256>,
32}
33
34impl fmt::Debug for BuiltBlock {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.debug_struct("BuiltBlock")
37            .field("host_fills", &self.host_fills.len())
38            .field("transactions", &self.transactions.len())
39            .field("gas_used", &self.gas_used)
40            .field("block_number", &self.block_number)
41            .finish_non_exhaustive()
42    }
43}
44
45impl BuiltBlock {
46    /// Create a new `BuiltBlock`
47    pub const fn new(block_number: u64) -> Self {
48        Self {
49            host_fills: Vec::new(),
50            transactions: Vec::new(),
51            block_number,
52            gas_used: 0,
53            raw_encoding: OnceLock::new(),
54            hash: OnceLock::new(),
55        }
56    }
57
58    /// Gets the block number for the block.
59    pub const fn block_number(&self) -> u64 {
60        self.block_number
61    }
62
63    /// Get the amount of gas used by the block.
64    pub const fn gas_used(&self) -> u64 {
65        self.gas_used
66    }
67
68    /// Get the number of transactions in the block.
69    pub fn tx_count(&self) -> usize {
70        self.transactions.len()
71    }
72
73    /// Check if the block is empty.
74    pub fn is_empty(&self) -> bool {
75        self.transactions.is_empty()
76    }
77
78    /// Get the current list of transactions included in this block.
79    #[allow(clippy::missing_const_for_fn)] // false positive, const deref
80    pub fn transactions(&self) -> &[TxEnvelope] {
81        &self.transactions
82    }
83
84    /// Get the current list of host fills included in this block.
85    #[allow(clippy::missing_const_for_fn)] // false positive, const deref
86    pub fn host_fills(&self) -> &[SignedFill] {
87        &self.host_fills
88    }
89
90    /// Unseal the block
91    pub(crate) fn unseal(&mut self) {
92        self.raw_encoding.take();
93        self.hash.take();
94    }
95
96    /// Seal the block by encoding the transactions and calculating the hash of
97    /// the block contents.
98    pub(crate) fn seal(&self) {
99        self.raw_encoding.get_or_init(|| encode_txns::<Alloy2718Coder>(&self.transactions).into());
100        self.hash.get_or_init(|| keccak256(self.raw_encoding.get().unwrap().as_ref()));
101    }
102
103    /// Ingest a transaction into the in-progress block.
104    pub fn ingest_tx(&mut self, tx: TxEnvelope) {
105        trace!(hash = %tx.tx_hash(), "ingesting tx");
106        self.unseal();
107        self.transactions.push(tx);
108    }
109
110    /// Ingest a bundle into the in-progress block.
111    /// Ignores Signed Orders for now.
112    pub fn ingest_bundle(&mut self, bundle: SignetEthBundle) {
113        trace!(replacement_uuid = bundle.replacement_uuid(), "adding bundle to block");
114
115        let txs = bundle
116            .bundle
117            .txs
118            .into_iter()
119            .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
120            .collect::<Result<Vec<_>, _>>();
121
122        if let Ok(txs) = txs {
123            self.unseal();
124            // extend the transactions with the decoded transactions.
125            // As this builder does not provide bundles landing "top of block", its fine to just extend.
126            self.transactions.extend(txs);
127
128            if let Some(host_fills) = bundle.host_fills {
129                self.host_fills.push(host_fills);
130            }
131        } else {
132            error!("failed to decode bundle. dropping");
133        }
134    }
135
136    /// Ingest a simulated item, extending the block.
137    pub fn ingest(&mut self, item: SimulatedItem) {
138        self.gas_used += item.gas_used;
139
140        match item.item {
141            SimItem::Bundle(bundle) => self.ingest_bundle(bundle),
142            SimItem::Tx(tx) => self.ingest_tx(tx),
143        }
144    }
145
146    /// Encode the in-progress block.
147    pub(crate) fn encode_raw(&self) -> &Bytes {
148        self.seal();
149        self.raw_encoding.get().unwrap()
150    }
151
152    /// Calculate the hash of the in-progress block, finishing the block.
153    pub fn contents_hash(&self) -> &B256 {
154        self.seal();
155        self.hash.get().unwrap()
156    }
157
158    /// Convert the in-progress block to sign request contents.
159    pub fn encode_calldata(&self) -> &Bytes {
160        self.encode_raw()
161    }
162
163    /// Convert the in-progress block to a blob transaction sidecar.
164    pub fn encode_blob<T: SidecarCoder + Default>(&self) -> SidecarBuilder<T> {
165        let mut coder = SidecarBuilder::<T>::default();
166        coder.ingest(self.encode_raw());
167        coder
168    }
169}