use std::collections::{hash_map, BTreeSet, HashMap, HashSet};
use zksync_types::{
l1::L1Tx, l2::L2Tx, Address, ExecuteTransactionCommon, Nonce, PriorityOpId, Transaction,
};
use crate::types::{AccountTransactions, L2TxFilter, MempoolScore};
#[derive(Debug)]
pub struct MempoolInfo {
pub stashed_accounts: Vec<Address>,
pub purged_accounts: Vec<Address>,
}
#[derive(Debug)]
pub struct MempoolStats {
pub l1_transaction_count: usize,
pub l2_transaction_count: u64,
pub l2_priority_queue_size: usize,
}
#[derive(Debug)]
pub struct MempoolStore {
l1_transactions: HashMap<PriorityOpId, L1Tx>,
l2_transactions_per_account: HashMap<Address, AccountTransactions>,
l2_priority_queue: BTreeSet<MempoolScore>,
next_priority_id: PriorityOpId,
stashed_accounts: Vec<Address>,
size: u64,
capacity: u64,
}
impl MempoolStore {
pub fn new(next_priority_id: PriorityOpId, capacity: u64) -> Self {
Self {
l1_transactions: HashMap::new(),
l2_transactions_per_account: HashMap::new(),
l2_priority_queue: BTreeSet::new(),
next_priority_id,
stashed_accounts: vec![],
size: 0,
capacity,
}
}
pub fn insert(
&mut self,
transactions: Vec<Transaction>,
initial_nonces: HashMap<Address, Nonce>,
) {
for transaction in transactions {
let Transaction {
common_data,
execute,
received_timestamp_ms,
raw_bytes,
} = transaction;
match common_data {
ExecuteTransactionCommon::L1(data) => {
tracing::trace!("inserting L1 transaction {}", data.serial_id);
self.l1_transactions.insert(
data.serial_id,
L1Tx {
execute,
common_data: data,
received_timestamp_ms,
},
);
}
ExecuteTransactionCommon::L2(data) => {
tracing::trace!("inserting L2 transaction {}", data.nonce);
self.insert_l2_transaction(
L2Tx {
execute,
common_data: data,
received_timestamp_ms,
raw_bytes,
},
&initial_nonces,
);
}
ExecuteTransactionCommon::ProtocolUpgrade(_) => {
panic!("Protocol upgrade tx is not supposed to be inserted into mempool");
}
}
}
}
fn insert_l2_transaction(
&mut self,
transaction: L2Tx,
initial_nonces: &HashMap<Address, Nonce>,
) {
let account = transaction.initiator_account();
let metadata = match self.l2_transactions_per_account.entry(account) {
hash_map::Entry::Occupied(mut txs) => txs.get_mut().insert(transaction),
hash_map::Entry::Vacant(entry) => {
let account_nonce = initial_nonces.get(&account).cloned().unwrap_or(Nonce(0));
entry
.insert(AccountTransactions::new(account_nonce))
.insert(transaction)
}
};
if let Some(score) = metadata.previous_score {
self.l2_priority_queue.remove(&score);
}
if let Some(score) = metadata.new_score {
self.l2_priority_queue.insert(score);
}
if metadata.is_new {
self.size += 1;
}
}
pub fn has_next(&self, filter: &L2TxFilter) -> bool {
self.l1_transactions.contains_key(&self.next_priority_id)
|| self
.l2_priority_queue
.iter()
.rfind(|el| el.matches_filter(filter))
.is_some()
}
pub fn next_transaction(&mut self, filter: &L2TxFilter) -> Option<Transaction> {
if let Some(transaction) = self.l1_transactions.remove(&self.next_priority_id) {
self.next_priority_id += 1;
return Some(transaction.into());
}
let mut removed = 0;
let tx_pointer = self
.l2_priority_queue
.iter()
.rfind(|el| el.matches_filter(filter))?
.clone();
for stashed_pointer in self
.l2_priority_queue
.split_off(&tx_pointer)
.into_iter()
.skip(1)
{
removed += self
.l2_transactions_per_account
.remove(&stashed_pointer.account)
.expect("mempool: dangling pointer in priority queue")
.len();
self.stashed_accounts.push(stashed_pointer.account);
}
let (transaction, score) = self
.l2_transactions_per_account
.get_mut(&tx_pointer.account)
.expect("mempool: dangling pointer in priority queue")
.next();
if let Some(score) = score {
self.l2_priority_queue.insert(score);
}
self.size = self
.size
.checked_sub((removed + 1) as u64)
.expect("mempool size can't be negative");
Some(transaction.into())
}
pub fn rollback(&mut self, tx: &Transaction) {
match &tx.common_data {
ExecuteTransactionCommon::L1(data) => {
self.next_priority_id = self.next_priority_id.min(data.serial_id);
}
ExecuteTransactionCommon::L2(_) => {
if let Some(score) = self
.l2_transactions_per_account
.get_mut(&tx.initiator_account())
.expect("account is not available in mempool")
.reset(tx)
{
self.l2_priority_queue.remove(&score);
}
}
ExecuteTransactionCommon::ProtocolUpgrade(_) => {
panic!("Protocol upgrade tx is not supposed to be in mempool");
}
}
}
pub fn get_mempool_info(&mut self) -> MempoolInfo {
MempoolInfo {
stashed_accounts: std::mem::take(&mut self.stashed_accounts),
purged_accounts: self.gc(),
}
}
pub fn stats(&self) -> MempoolStats {
MempoolStats {
l1_transaction_count: self.l1_transactions.len(),
l2_transaction_count: self.size,
l2_priority_queue_size: self.l2_priority_queue.len(),
}
}
fn gc(&mut self) -> Vec<Address> {
if self.size >= self.capacity {
let index: HashSet<_> = self
.l2_priority_queue
.iter()
.map(|pointer| pointer.account)
.collect();
let transactions = std::mem::take(&mut self.l2_transactions_per_account);
let (kept, drained) = transactions
.into_iter()
.partition(|(address, _)| index.contains(address));
self.l2_transactions_per_account = kept;
self.size = self
.l2_transactions_per_account
.iter()
.fold(0, |agg, (_, tnxs)| agg + tnxs.len() as u64);
return drained.into_keys().collect();
}
vec![]
}
}