Skip to main content

ab_transaction_pool/
lib.rs

1use ab_core_primitives::block::{BlockNumber, BlockRoot};
2use ab_core_primitives::transaction::TransactionHash;
3use ab_core_primitives::transaction::owned::OwnedTransaction;
4use std::collections::{HashMap, HashSet, VecDeque};
5use std::num::{NonZeroU8, NonZeroU64, NonZeroUsize};
6
7/// Transaction pool limits
8#[derive(Debug, Copy, Clone)]
9pub struct TransactionPoolLimits {
10    /// Number of transactions
11    pub count: NonZeroUsize,
12    /// Total size of all transactions
13    pub size: NonZeroUsize,
14}
15
16#[derive(Debug)]
17pub struct TransactionAuthorizedDetails {
18    /// Block number at which transaction was authorized
19    pub block_number: BlockNumber,
20    /// Block root at which transaction was authorized
21    pub block_root: BlockRoot,
22}
23
24#[derive(Debug)]
25pub enum TransactionState {
26    New,
27    Authorized {
28        at: VecDeque<TransactionAuthorizedDetails>,
29    },
30}
31
32#[derive(Debug)]
33#[non_exhaustive]
34pub struct PoolTransaction {
35    pub tx: OwnedTransaction,
36    pub state: TransactionState,
37    // TODO: Slots, other things?
38}
39
40/// Error for [`TransactionPool::add()`] method
41#[derive(Debug, thiserror::Error)]
42pub enum TransactionAddError {
43    /// Already exists
44    #[error("Already exists")]
45    AlreadyExists,
46    /// The block isn't found, possibly too old
47    #[error("The block isn't found, possibly too old")]
48    BlockNotFound,
49    /// Too many transactions
50    #[error("Too many transactions")]
51    TooManyTransactions,
52    /// Total size too large
53    #[error("Total size too large")]
54    TotalSizeTooLarge,
55}
56
57#[derive(Debug)]
58struct BlockRootDetails {
59    txs: HashSet<TransactionHash>,
60}
61
62// TODO: Some integration or tracking of slots, including optimization where only changes to read
63//  slots impact transaction authorization
64/// Transaction pool implementation.
65///
66/// The goal of the transaction pool is to retain a set of transactions and associated authorization
67/// information, which can be used for block production and propagation through the network.
68#[derive(Debug)]
69pub struct TransactionPool {
70    transactions: HashMap<TransactionHash, PoolTransaction>,
71    total_size: usize,
72    /// Map from block root at which transactions were created to a set of transaction hashes
73    by_block_root: HashMap<BlockRoot, BlockRootDetails>,
74    // TODO: Optimize with an oldest block + `Vec<BlockHash>` instead
75    by_block_number: HashMap<BlockNumber, BlockRoot>,
76    pruning_depth: NonZeroU64,
77    authorization_history_depth: NonZeroU8,
78    limits: TransactionPoolLimits,
79}
80
81impl TransactionPool {
82    /// Create a new instance.
83    ///
84    /// `pruning_depth` defines how old (in blocks) should transaction be before it is automatically
85    /// removed from the transaction pool.
86    ///
87    /// `authorization_history_depth` defines a small number of recent blocks for which
88    /// authorization information is retained in each block.
89    ///
90    /// `limits` defines the limits of transaction pool.
91    pub fn new(
92        pruning_depth: NonZeroU64,
93        authorization_history_depth: NonZeroU8,
94        limits: TransactionPoolLimits,
95    ) -> Self {
96        Self {
97            transactions: HashMap::default(),
98            total_size: 0,
99            by_block_root: HashMap::default(),
100            by_block_number: HashMap::default(),
101            pruning_depth,
102            authorization_history_depth,
103            limits,
104        }
105    }
106
107    /// Add new transaction to the pool
108    pub fn add(&mut self, tx: OwnedTransaction) -> Result<(), TransactionAddError> {
109        let tx_hash = tx.hash();
110        if self.contains(&tx_hash) {
111            return Err(TransactionAddError::AlreadyExists);
112        }
113
114        let block_root = tx.transaction().header.block_root;
115        let Some(block_txs) = self.by_block_root.get_mut(&block_root) else {
116            return Err(TransactionAddError::BlockNotFound);
117        };
118
119        if self.transactions.len() == self.limits.count.get() {
120            return Err(TransactionAddError::TooManyTransactions);
121        }
122
123        let tx_size = tx.buffer().len() as usize;
124        if self.limits.size.get() - self.total_size < tx_size {
125            return Err(TransactionAddError::TotalSizeTooLarge);
126        }
127
128        self.total_size += tx_size;
129        self.transactions.insert(
130            tx_hash,
131            PoolTransaction {
132                tx,
133                state: TransactionState::New,
134            },
135        );
136        block_txs.txs.insert(tx_hash);
137
138        Ok(())
139    }
140
141    /// Mark transaction as authorized as of a specific block.
142    ///
143    /// Returns `false` if transaction is unknown.
144    pub fn mark_authorized(
145        &mut self,
146        tx_hash: &TransactionHash,
147        block_number: BlockNumber,
148        block_root: BlockRoot,
149    ) -> bool {
150        let Some(tx) = self.transactions.get_mut(tx_hash) else {
151            return false;
152        };
153
154        let authorized_details = TransactionAuthorizedDetails {
155            block_number,
156            block_root,
157        };
158
159        match &mut tx.state {
160            TransactionState::New => {
161                tx.state = TransactionState::Authorized {
162                    at: VecDeque::from([authorized_details]),
163                };
164            }
165            TransactionState::Authorized { at } => {
166                if at.len() == usize::from(self.authorization_history_depth.get()) {
167                    at.pop_back();
168                }
169                at.push_front(authorized_details);
170            }
171        }
172
173        true
174    }
175
176    /// Whether transaction pool contains a transaction
177    pub fn contains(&self, tx_hash: &TransactionHash) -> bool {
178        self.transactions.contains_key(tx_hash)
179    }
180
181    /// Get iterator over all transactions
182    pub fn iter(
183        &self,
184    ) -> impl ExactSizeIterator<Item = (&'_ TransactionHash, &'_ PoolTransaction)> + '_ {
185        self.transactions.iter()
186    }
187
188    /// Remove transactions from the pool
189    pub fn remove<'a, Txs>(&mut self, tx_hashes: Txs)
190    where
191        Txs: Iterator<Item = &'a TransactionHash>,
192    {
193        for tx_hash in tx_hashes {
194            self.remove_single_tx(tx_hash);
195        }
196    }
197
198    fn remove_single_tx(&mut self, tx_hash: &TransactionHash) {
199        if let Some(tx) = self.transactions.remove(tx_hash) {
200            self.total_size -= tx.tx.buffer().len() as usize;
201
202            let block_root = &tx.tx.transaction().header.block_root;
203            if let Some(set) = self.by_block_root.get_mut(block_root) {
204                set.txs.remove(tx_hash);
205                if set.txs.is_empty() {
206                    self.by_block_root.remove(block_root);
207                }
208            }
209        }
210    }
211
212    /// Add the new best block.
213    ///
214    /// If there is already an existing block with the same or higher block number, it will be
215    /// removed alongside all transactions. Blocks older than configured pruning depth will be
216    /// removed automatically as well.
217    ///
218    /// This allows accepting transactions created at specified block root.
219    pub fn add_best_block(&mut self, block_number: BlockNumber, block_root: BlockRoot) {
220        // Clean up old blocks or blocks that are at the same or higher block number
221        let allowed_blocks =
222            block_number.saturating_sub(BlockNumber::from(self.pruning_depth.get()))..block_number;
223        self.by_block_number
224            .retain(|existing_block_number, existing_block_root| {
225                if allowed_blocks.contains(existing_block_number) {
226                    return true;
227                }
228
229                if let Some(tx_hashes) = self.by_block_root.remove(existing_block_root) {
230                    for tx_hash in tx_hashes.txs {
231                        if let Some(tx) = self.transactions.remove(&tx_hash) {
232                            self.total_size -= tx.tx.buffer().len() as usize;
233                        }
234                    }
235                }
236                false
237            });
238
239        for tx in self.transactions.values_mut() {
240            if let TransactionState::Authorized { at } = &mut tx.state {
241                // Clean up verification status for blocks at the same or higher block number
242                while at
243                    .pop_front_if(|details| details.block_number >= block_number)
244                    .is_some()
245                {}
246            }
247        }
248
249        self.by_block_number.insert(block_number, block_root);
250        self.by_block_root.insert(
251            block_root,
252            BlockRootDetails {
253                txs: HashSet::new(),
254            },
255        );
256    }
257}