use std::{cmp::Ordering, collections::HashMap};
use zksync_types::{
fee::Fee, fee_model::BatchFeeInput, l2::L2Tx, Address, Nonce, Transaction, U256,
};
#[derive(Debug)]
pub(crate) struct AccountTransactions {
transactions: HashMap<Nonce, L2Tx>,
nonce: Nonce,
}
impl AccountTransactions {
pub fn new(nonce: Nonce) -> Self {
Self {
transactions: HashMap::new(),
nonce,
}
}
pub fn insert(&mut self, transaction: L2Tx) -> InsertionMetadata {
let mut metadata = InsertionMetadata::default();
let nonce = transaction.common_data.nonce;
if nonce < self.nonce {
return metadata;
}
let new_score = Self::score_for_transaction(&transaction);
let previous_score = self
.transactions
.insert(nonce, transaction)
.map(|tx| Self::score_for_transaction(&tx));
metadata.is_new = previous_score.is_none();
if nonce == self.nonce {
metadata.new_score = Some(new_score);
metadata.previous_score = previous_score;
}
metadata
}
pub fn next(&mut self) -> (L2Tx, Option<MempoolScore>) {
let transaction = self
.transactions
.remove(&self.nonce)
.expect("missing transaction in mempool");
self.nonce += 1;
let score = self
.transactions
.get(&self.nonce)
.map(Self::score_for_transaction);
(transaction, score)
}
pub fn reset(&mut self, transaction: &Transaction) -> Option<MempoolScore> {
let tx_nonce = transaction
.nonce()
.expect("nonce is not set for L2 transaction");
self.nonce = self.nonce.min(tx_nonce);
self.transactions
.get(&(tx_nonce + 1))
.map(Self::score_for_transaction)
}
pub fn len(&self) -> usize {
self.transactions.len()
}
fn score_for_transaction(transaction: &L2Tx) -> MempoolScore {
MempoolScore {
account: transaction.initiator_account(),
received_at_ms: transaction.received_timestamp_ms,
fee_data: transaction.common_data.fee.clone(),
}
}
}
#[derive(Eq, PartialEq, Clone, Debug, Hash)]
pub struct MempoolScore {
pub account: Address,
pub received_at_ms: u64,
pub fee_data: Fee,
}
impl MempoolScore {
pub fn matches_filter(&self, filter: &L2TxFilter) -> bool {
self.fee_data.max_fee_per_gas >= U256::from(filter.fee_per_gas)
&& self.fee_data.gas_per_pubdata_limit >= U256::from(filter.gas_per_pubdata)
}
}
impl Ord for MempoolScore {
fn cmp(&self, other: &MempoolScore) -> Ordering {
match self.received_at_ms.cmp(&other.received_at_ms).reverse() {
Ordering::Equal => {}
ordering => return ordering,
}
self.account.cmp(&other.account)
}
}
impl PartialOrd for MempoolScore {
fn partial_cmp(&self, other: &MempoolScore) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Default)]
pub(crate) struct InsertionMetadata {
pub new_score: Option<MempoolScore>,
pub previous_score: Option<MempoolScore>,
pub is_new: bool,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct L2TxFilter {
pub fee_input: BatchFeeInput,
pub fee_per_gas: u64,
pub gas_per_pubdata: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter() {
fn filter(fee_per_gas: u64, gas_per_pubdata: u32) -> L2TxFilter {
L2TxFilter {
fee_input: BatchFeeInput::sensible_l1_pegged_default(),
fee_per_gas,
gas_per_pubdata,
}
}
const MAX_FEE_PER_GAS: u64 = 100u64;
const MAX_PRIORITY_FEE_PER_GAS: u32 = 100u32;
const GAS_PER_PUBDATA_LIMIT: u32 = 100u32;
let score = MempoolScore {
account: Address::random(),
received_at_ms: Default::default(), fee_data: Fee {
gas_limit: Default::default(), max_fee_per_gas: U256::from(MAX_FEE_PER_GAS),
max_priority_fee_per_gas: U256::from(MAX_PRIORITY_FEE_PER_GAS),
gas_per_pubdata_limit: U256::from(GAS_PER_PUBDATA_LIMIT),
},
};
let noop_filter = filter(0, 0);
assert!(
score.matches_filter(&noop_filter),
"Noop filter should always match"
);
let max_gas_filter = filter(MAX_FEE_PER_GAS, 0);
assert!(
score.matches_filter(&max_gas_filter),
"Correct max gas should be accepted"
);
let pubdata_filter = filter(0, GAS_PER_PUBDATA_LIMIT);
assert!(
score.matches_filter(&pubdata_filter),
"Correct pubdata price should be accepted"
);
let decline_gas_filter = filter(MAX_FEE_PER_GAS + 1, 0);
assert!(
!score.matches_filter(&decline_gas_filter),
"Incorrect max gas should be rejected"
);
let decline_pubdata_filter = filter(0, GAS_PER_PUBDATA_LIMIT + 1);
assert!(
!score.matches_filter(&decline_pubdata_filter),
"Incorrect pubdata price should be rejected"
);
}
}