Skip to main content

signet_sim/cache/
item.rs

1use crate::{
2    cache::{check_bundle_tx_list, StateSource},
3    CacheError, SimItemValidity,
4};
5use alloy::{
6    consensus::{
7        transaction::{Recovered, SignerRecoverable},
8        Transaction, TxEnvelope,
9    },
10    primitives::{TxHash, U256},
11};
12use signet_bundle::{RecoveredBundle, SignetEthBundle, TxRequirement};
13use std::{
14    borrow::{Borrow, Cow},
15    hash::Hash,
16    sync::Arc,
17};
18use tracing::{instrument, trace, trace_span};
19
20/// An item that can be simulated, wrapped in an Arc for cheap cloning.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SimItem {
23    /// A bundle to be simulated.
24    Bundle(Arc<RecoveredBundle>),
25    /// A transaction to be simulated.
26    Tx(Arc<Recovered<TxEnvelope>>),
27}
28
29impl TryFrom<SignetEthBundle> for SimItem {
30    type Error = CacheError;
31
32    fn try_from(bundle: SignetEthBundle) -> Result<Self, Self::Error> {
33        bundle.try_into_recovered().map_err(CacheError::BundleRecover).and_then(TryInto::try_into)
34    }
35}
36
37impl TryFrom<RecoveredBundle> for SimItem {
38    type Error = CacheError;
39
40    fn try_from(bundle: RecoveredBundle) -> Result<Self, Self::Error> {
41        if bundle.replacement_uuid().is_some() {
42            Ok(Self::Bundle(bundle.into()))
43        } else {
44            Err(CacheError::BundleWithoutReplacementUuid)
45        }
46    }
47}
48
49impl From<Recovered<TxEnvelope>> for SimItem {
50    fn from(tx: Recovered<TxEnvelope>) -> Self {
51        Self::Tx(tx.into())
52    }
53}
54
55impl TryFrom<TxEnvelope> for SimItem {
56    type Error = CacheError;
57
58    fn try_from(tx: TxEnvelope) -> Result<Self, Self::Error> {
59        tx.try_into_recovered().map_err(Into::into).map(Self::from)
60    }
61}
62
63impl SimItem {
64    /// Get the bundle if it is a bundle.
65    pub fn as_bundle(&self) -> Option<&RecoveredBundle> {
66        match self {
67            Self::Bundle(bundle) => Some(bundle),
68            Self::Tx(_) => None,
69        }
70    }
71
72    /// Get the transaction if it is a transaction.
73    pub fn as_tx(&self) -> Option<&Recovered<TxEnvelope>> {
74        match self {
75            Self::Bundle(_) => None,
76            Self::Tx(tx) => Some(tx),
77        }
78    }
79
80    /// Calculate the maximum gas fee payable, this may be used as a heuristic
81    /// to determine simulation order.
82    pub fn calculate_total_fee(&self, basefee: u64) -> u128 {
83        match self {
84            Self::Bundle(bundle) => {
85                let mut total_tx_fee = 0;
86                for tx in bundle.txs() {
87                    total_tx_fee += tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128;
88                }
89                total_tx_fee
90            }
91            Self::Tx(tx) => tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128,
92        }
93    }
94}
95
96// Testing functions
97impl SimItem {
98    /// Returns a unique identifier for this item, which can be used to
99    /// distinguish it from other items.
100    pub fn identifier(&self) -> SimIdentifier<'_> {
101        match self {
102            Self::Bundle(bundle) => {
103                SimIdentifier::Bundle(Cow::Borrowed(bundle.replacement_uuid().unwrap()))
104            }
105            Self::Tx(tx) => SimIdentifier::Tx(*tx.inner().hash()),
106        }
107    }
108
109    /// Returns an unique, owned identifier for this item.
110    pub fn identifier_owned(&self) -> SimIdentifier<'static> {
111        match self {
112            Self::Bundle(bundle) => {
113                SimIdentifier::Bundle(Cow::Owned(bundle.replacement_uuid().unwrap().to_string()))
114            }
115            Self::Tx(tx) => SimIdentifier::Tx(*tx.inner().hash()),
116        }
117    }
118
119    async fn check_tx<S>(&self, source: &S) -> Result<SimItemValidity, Box<dyn std::error::Error>>
120    where
121        S: StateSource,
122    {
123        let item = self.as_tx().expect("SimItem is not a Tx");
124        let signer = item.signer();
125        let item_nonce = item.nonce();
126        let total = U256::from(item.max_fee_per_gas() * item.gas_limit() as u128) + item.value();
127
128        source
129            .map(&signer, |info| {
130                let _guard = trace_span!(
131                    "check_tx",
132                    %signer,
133                    item_nonce,
134                    expected_nonce = info.nonce,
135                )
136                .entered();
137
138                // if the chain nonce is greater than the tx nonce, it is
139                // no longer valid
140                if info.nonce > item_nonce {
141                    trace!("nonce too low");
142                    return SimItemValidity::Never;
143                }
144                // if the chain nonce is less than the tx nonce, we need to wait
145                if info.nonce < item_nonce {
146                    trace!("nonce too high");
147                    return SimItemValidity::Future;
148                }
149                // if the balance is insufficient, we need to wait
150                if info.balance < total {
151                    trace!(
152                        required = %total,
153                        available = %info.balance,
154                        "insufficient balance",
155                    );
156                    return SimItemValidity::Future;
157                }
158                // nonce is equal and balance is sufficient
159                SimItemValidity::Now
160            })
161            .await
162            .map_err(Into::into)
163    }
164
165    #[instrument(level = "trace", skip_all)]
166    async fn check_bundle_tx_list_for_rollup<S>(
167        items: impl Iterator<Item = TxRequirement>,
168        source: &S,
169    ) -> Result<SimItemValidity, S::Error>
170    where
171        S: StateSource,
172    {
173        check_bundle_tx_list(items, source).await
174    }
175
176    #[instrument(level = "trace", skip_all)]
177    async fn check_bundle_tx_list_for_host<S>(
178        items: impl Iterator<Item = TxRequirement>,
179        source: &S,
180    ) -> Result<SimItemValidity, S::Error>
181    where
182        S: StateSource,
183    {
184        check_bundle_tx_list(items, source).await
185    }
186
187    async fn check_bundle<S, S2>(
188        &self,
189        source: &S,
190        host_source: &S2,
191    ) -> Result<SimItemValidity, Box<dyn std::error::Error>>
192    where
193        S: StateSource,
194        S2: StateSource,
195    {
196        let item = self.as_bundle().expect("SimItem is not a Bundle");
197
198        let ru_tx = Self::check_bundle_tx_list_for_rollup(item.tx_reqs(), source).await?;
199        let host_tx = Self::check_bundle_tx_list_for_host(item.host_tx_reqs(), host_source).await?;
200
201        // Check both the regular txs and the host txs.
202        Ok(ru_tx.min(host_tx))
203    }
204
205    /// Check if the item is valid against the provided state sources.
206    ///
207    /// This will check that nonces and balances are sufficient for the item to
208    /// be included on the current state.
209    #[instrument(
210        level = "trace",
211        name = "preflight_check",
212        skip_all,
213        fields(
214            item_identifier = %self.identifier(),
215            item_type = if self.as_bundle().is_some() { "bundle" } else { "tx" },
216        ),
217        ret(level = "debug", Display),
218        err(level = "debug", Display),
219    )]
220    pub async fn check<S, S2>(
221        &self,
222        source: &S,
223        host_source: &S2,
224    ) -> Result<SimItemValidity, Box<dyn std::error::Error>>
225    where
226        S: StateSource,
227        S2: StateSource,
228    {
229        match self {
230            SimItem::Bundle(_) => self.check_bundle(source, host_source).await,
231            SimItem::Tx(_) => self.check_tx(source).await,
232        }
233    }
234}
235
236/// A simulation cache item identifier.
237#[derive(Debug, Clone)]
238pub enum SimIdentifier<'a> {
239    /// A bundle identifier.
240    Bundle(Cow<'a, str>),
241    /// A transaction identifier.
242    Tx(TxHash),
243}
244
245impl core::fmt::Display for SimIdentifier<'_> {
246    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247        match self {
248            Self::Bundle(id) => write!(f, "{id}"),
249            Self::Tx(id) => write!(f, "{id}"),
250        }
251    }
252}
253
254impl PartialEq for SimIdentifier<'_> {
255    fn eq(&self, other: &Self) -> bool {
256        self.as_bytes().eq(other.as_bytes())
257    }
258}
259
260impl Eq for SimIdentifier<'_> {}
261
262impl Hash for SimIdentifier<'_> {
263    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
264        self.as_bytes().hash(state);
265    }
266}
267
268impl Borrow<[u8]> for SimIdentifier<'_> {
269    fn borrow(&self) -> &[u8] {
270        self.as_bytes()
271    }
272}
273
274impl From<TxHash> for SimIdentifier<'_> {
275    fn from(tx_hash: TxHash) -> Self {
276        Self::Tx(tx_hash)
277    }
278}
279
280impl SimIdentifier<'_> {
281    /// Create a new [`SimIdentifier::Bundle`].
282    pub const fn bundle<'a>(id: Cow<'a, str>) -> SimIdentifier<'a> {
283        SimIdentifier::Bundle(id)
284    }
285
286    /// Create a new [`SimIdentifier::Tx`].
287    pub const fn tx(id: TxHash) -> Self {
288        Self::Tx(id)
289    }
290
291    /// Check if this identifier is a bundle.
292    pub const fn is_bundle(&self) -> bool {
293        matches!(self, Self::Bundle(_))
294    }
295
296    /// Check if this identifier is a transaction.
297    pub const fn is_tx(&self) -> bool {
298        matches!(self, Self::Tx(_))
299    }
300
301    /// Get the identifier as a byte slice.
302    pub fn as_bytes(&self) -> &[u8] {
303        match self {
304            Self::Bundle(id) => id.as_bytes(),
305            Self::Tx(id) => id.as_ref(),
306        }
307    }
308}