Skip to main content

signet_sim/cache/
validity.rs

1use crate::cache::StateSource;
2use alloy::primitives::Address;
3use core::fmt::{self, Display, Formatter};
4use signet_bundle::TxRequirement;
5use std::collections::BTreeMap;
6use tracing::{trace, trace_span};
7
8/// The validity status of a simulation item.
9///
10/// These are ordered from least to most valid. An item that is `Never` valid
11/// is always invalid, an item that is `Future` valid may become valid in the
12/// future, and an item that is `Now` valid is currently valid.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum SimItemValidity {
15    /// The item is invalid and should not be simulated.
16    Never,
17    /// The item is currently invalid, but may become valid in the future.
18    ///
19    /// For example, this may be due to nonce gaps.
20    Future,
21    /// The item is valid and can be simulated.
22    Now,
23}
24
25impl SimItemValidity {
26    /// Returns true if the item is valid now.
27    pub const fn is_valid_now(&self) -> bool {
28        matches!(self, SimItemValidity::Now)
29    }
30
31    /// Returns true if the item is never valid.
32    pub const fn is_never_valid(&self) -> bool {
33        matches!(self, SimItemValidity::Never)
34    }
35
36    /// Returns true if the item may be valid in the future.
37    pub const fn is_future_valid(&self) -> bool {
38        matches!(self, SimItemValidity::Future)
39    }
40}
41
42impl Display for SimItemValidity {
43    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Never => f.write_str("never"),
46            Self::Future => f.write_str("future"),
47            Self::Now => f.write_str("now"),
48        }
49    }
50}
51
52/// Check a list of bundle transactions for validity against a state source.
53///
54/// Validates nonces sequentially, building a per-signer nonce cache so that
55/// multiple transactions from the same signer are checked with incrementing
56/// nonces. The first transaction's balance is also checked.
57pub async fn check_bundle_tx_list<S>(
58    items: impl Iterator<Item = TxRequirement>,
59    source: &S,
60) -> Result<SimItemValidity, S::Error>
61where
62    S: StateSource,
63{
64    // For bundles, we want to check the nonce of each transaction. To do
65    // this, we build a small in memory cache so that if the same signer
66    // appears, we can reuse the nonce info. We do not check balances after
67    // the first tx, as they may have changed due to prior txs in the
68    // bundle.
69
70    let mut nonce_cache: BTreeMap<Address, u64> = BTreeMap::new();
71    let mut items = items.peekable();
72
73    // Peek to perform the balance check for the first tx
74    if let Some(first) = items.peek() {
75        let info = source.account_details(&first.signer).await?;
76
77        // check balance for the first tx is sufficient
78        if first.balance > info.balance {
79            trace!(
80                required = %first.balance,
81                available = %info.balance,
82                signer = %first.signer,
83                "insufficient balance",
84            );
85            return Ok(SimItemValidity::Future);
86        }
87
88        // Cache the nonce. This will be used for the first tx.
89        nonce_cache.insert(first.signer, info.nonce);
90    }
91
92    for requirement in items {
93        let state_nonce = match nonce_cache.get(&requirement.signer) {
94            Some(cached_nonce) => *cached_nonce,
95            None => {
96                let nonce = source.nonce(&requirement.signer).await?;
97                nonce_cache.insert(requirement.signer, nonce);
98                nonce
99            }
100        };
101
102        let _guard = trace_span!(
103            "check_bundle_tx",
104            signer = %requirement.signer,
105            item_nonce = requirement.nonce,
106            expected_nonce = state_nonce,
107        )
108        .entered();
109
110        if requirement.nonce < state_nonce {
111            trace!("nonce too low");
112            return Ok(SimItemValidity::Never);
113        }
114        if requirement.nonce > state_nonce {
115            trace!("nonce too high");
116            return Ok(SimItemValidity::Future);
117        }
118
119        // Increment the cached nonce for the next transaction from this
120        // signer. Map _must_ have the entry as we just either loaded or
121        // stored it above
122        nonce_cache.entry(requirement.signer).and_modify(|n| *n += 1);
123    }
124
125    // All transactions passed
126    Ok(SimItemValidity::Now)
127}