use crate::chaindef::OutPoint;
use crate::chaindef::{OutPointHash, Transaction};
use crate::daemon::Daemon;
use crate::index::index_transaction;
use crate::metrics::{self, Metrics};
use crate::query::tx::TxQuery;
use crate::store::{DBContents, DBStore};
use crate::ttorsort::ttor_sorted_mempool_entries;
use crate::writebatch::{SpentUpdateBatch, WriteBatch};
use anyhow::{Context, Result};
use bitcoin_hashes::hex::ToHex;
use bitcoincash::hash_types::Txid;
use futures_util::{stream, StreamExt};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
const VSIZE_BIN_WIDTH: u32 = 100_000;
pub const MEMPOOL_HEIGHT: u32 = 0x7FFF_FFFF;
pub enum ConfirmationState {
Indeterminate,
Confirmed,
InMempool,
UnconfirmedParent,
}
pub struct MempoolEntry {
fee: u64, size: usize, fee_per_byte: f32,
tx: Transaction,
}
impl MempoolEntry {
pub(crate) fn new(fee: u64, tx: Transaction) -> MempoolEntry {
let size = tx.size();
MempoolEntry {
fee,
size,
fee_per_byte: fee as f32 / size as f32,
tx,
}
}
pub fn fee(&self) -> u64 {
self.fee
}
pub fn size(&self) -> usize {
self.size
}
pub fn fee_per_byte(&self) -> f32 {
self.fee_per_byte
}
pub fn tx(&self) -> &Transaction {
&self.tx
}
}
struct Stats {
count: prometheus::IntGauge,
update: prometheus::HistogramVec,
vsize: prometheus::GaugeVec,
max_fee_rate: Mutex<f32>,
}
impl Stats {
fn start_timer(&self, step: &str) -> prometheus::HistogramTimer {
self.update.with_label_values(&[step]).start_timer()
}
async fn update(&self, entries: &[&MempoolEntry]) {
let mut bands: Vec<(f32, u32)> = vec![];
let mut fee_rate = 1.0f32; let mut vsize = 0u32; for e in entries {
while fee_rate < e.fee_per_byte() {
bands.push((fee_rate, vsize));
fee_rate *= 2.0;
}
vsize += e.size() as u32;
}
let mut max_fee_rate = self.max_fee_rate.lock().await;
loop {
bands.push((fee_rate, vsize));
if fee_rate < *max_fee_rate {
fee_rate *= 2.0;
continue;
}
*max_fee_rate = fee_rate;
break;
}
drop(max_fee_rate);
for (fee_rate, vsize) in bands {
let label = format!("≤{:10.0}", fee_rate);
self.vsize
.with_label_values(&[&label])
.set(f64::from(vsize));
}
}
}
pub struct Tracker {
items: RwLock<HashMap<Txid, MempoolEntry>>,
index: Arc<DBStore>,
histogram: Mutex<Vec<(f32, u32)>>,
stats: Stats,
}
impl Tracker {
pub fn new(mempool_path: &std::path::Path, metrics: &Metrics) -> Tracker {
let store = {
DBStore::destroy(mempool_path);
DBStore::open(DBContents::MempoolIndex, mempool_path, metrics, false)
.expect("creating mempool db faled")
};
Tracker {
items: RwLock::new(HashMap::new()),
index: Arc::new(store),
histogram: Mutex::new(vec![]),
stats: Stats {
count: metrics.gauge_int(prometheus::Opts::new(
"rostrum_mempool_count",
"# of mempool transactions",
)),
update: metrics.histogram_vec(
"rostrum_mempool_update",
"Time to update mempool (in seconds)",
&["step"],
metrics::default_duration_buckets(),
),
vsize: metrics.gauge_float_vec(
prometheus::Opts::new(
"rostrum_mempool_vsize",
"Total vsize of transactions paying at most given fee rate",
),
&["fee_rate"],
),
max_fee_rate: Mutex::new(1.0),
},
}
}
pub async fn get_txn(&self, txid: &Txid) -> Option<Transaction> {
self.items
.read()
.await
.get(txid)
.map(|stats| stats.tx.clone())
}
pub async fn get_all_txn(&self) -> Vec<Txid> {
self.items.read().await.keys().copied().collect()
}
pub async fn get_txn_partially_matching_scriptsig(&self, scriptsig: &[u8]) -> Vec<Txid> {
self.items
.read()
.await
.iter()
.filter_map(|(txid, entry)| {
if entry.tx.input.iter().any(|i| {
i.script_sig
.to_bytes()
.windows(scriptsig.len())
.any(|window| window == scriptsig)
}) {
Some(*txid)
} else {
None
}
})
.collect()
}
pub async fn get_txn_partially_matching_scriptpubkey(&self, scriptpubkey: &[u8]) -> Vec<Txid> {
self.items
.read()
.await
.iter()
.filter_map(|(txid, entry)| {
if entry.tx.output.iter().any(|o| {
o.script_pubkey
.to_bytes()
.windows(scriptpubkey.len())
.any(|window| window == scriptpubkey)
}) {
Some(*txid)
} else {
None
}
})
.collect()
}
pub async fn has_txn(&self, txid: &Txid) -> bool {
self.items.read().await.contains_key(txid)
}
pub async fn get_fee(&self, txid: &Txid) -> Option<u64> {
self.items.read().await.get(txid).map(|stats| stats.fee())
}
pub async fn get_count(&self) -> usize {
self.items.read().await.len()
}
#[cfg(bch)]
pub async fn contains_funding_tx(&self, prevout: &OutPoint) -> bool {
self.items.read().await.contains_key(&prevout.txid)
}
#[cfg(nexa)]
pub async fn contains_funding_tx(&self, prevout: &OutPoint) -> bool {
use bitcoin_hashes::Hash;
use crate::encode::outpoint_hash;
use crate::indexes::outputindex::OutputIndexRow;
use crate::indexes::DBRow;
let key = outpoint_hash(prevout);
self.index
.exists_32bit_key(OutputIndexRow::CF, key.into_inner())
.await
}
pub async fn fee_histogram(&self) -> Vec<(f32, u32)> {
self.histogram.lock().await.clone()
}
pub fn index(&self) -> &Arc<DBStore> {
&self.index
}
async fn find_mempool_difference_daemon(
&self,
daemon: &Daemon,
) -> Result<(HashSet<Txid>, HashSet<Txid>)> {
let daemon_txids = daemon
.getmempooltxids()
.await
.context("failed to update mempool from daemon")?;
let our: HashSet<Txid> = self.items.read().await.keys().cloned().collect();
let new_txs = daemon_txids.difference(&our).cloned().collect();
let removed_txs = our.difference(&daemon_txids).cloned().collect();
Ok((new_txs, removed_txs))
}
pub async fn update_from_tx(&self, tx: Transaction, txquery: &TxQuery) -> Result<()> {
let dummy_cache: HashMap<OutPointHash, u64> = HashMap::default();
let txid = tx.txid();
if self.items.read().await.contains_key(&txid) {
return Ok(());
}
let entry = txquery.mempool_entry_from(self, tx, &dummy_cache).await?;
self.add(&txid, entry, txquery).await;
Ok(())
}
async fn add_transactions_ttor_sorted(
&self,
new_txs: HashSet<Txid>,
txquery: &TxQuery,
error_prefix: &str,
) -> HashSet<Txid> {
if new_txs.is_empty() {
return HashSet::new();
}
let input_fee_cache: Arc<HashMap<OutPointHash, u64>> =
Arc::new(txquery.create_input_cache(&new_txs).await);
let entries: Vec<MempoolEntry> = stream::iter(new_txs)
.filter_map(|txid| {
let cache = input_fee_cache.clone();
async move {
match txquery.get_mempool_entry(self, &txid, &cache).await {
Ok(entry) => {
assert_eq!(entry.tx.txid(), txid);
Some(entry)
}
Err(err) => {
debug!("failed to get {} {}: {}", error_prefix, txid, err);
None
}
}
}
})
.collect()
.await;
let sorted_entries = ttor_sorted_mempool_entries(entries);
let mut added: HashSet<Txid> = HashSet::new();
for entry in sorted_entries {
let txid = entry.tx.txid();
self.add(&txid, entry, txquery).await;
added.insert(txid);
}
added
}
pub async fn update_from_txids(
&self,
tx_invs: HashSet<Txid>,
txquery: &TxQuery,
) -> HashSet<Txid> {
let new_txs: HashSet<_> = stream::iter(tx_invs)
.filter(|tx| {
let tx = *tx;
async move { !self.items.read().await.contains_key(&tx) }
})
.collect()
.await;
let added = self
.add_transactions_ttor_sorted(new_txs, txquery, "p2p mempool entry")
.await;
self.update_fee_histogram().await;
self.stats.count.set(self.items.read().await.len() as i64);
added
}
pub async fn update_from_daemon(
&self,
daemon: &Daemon,
txquery: &TxQuery,
) -> Result<HashSet<Txid>> {
let timer = self.stats.start_timer("fetch");
let (new_txs, removed_txs) = self.find_mempool_difference_daemon(daemon).await?;
timer.observe_duration();
if new_txs.is_empty() && removed_txs.is_empty() {
return Ok(HashSet::new());
}
let timer = self.stats.start_timer("add");
let mut changed_txs = self
.add_transactions_ttor_sorted(new_txs, txquery, "mempool entry")
.await;
timer.observe_duration();
let timer = self.stats.start_timer("remove");
let mut removed_transactions: Vec<Transaction> = Vec::new();
for txid in &removed_txs {
if let Some(tx) = self.get_txn(txid).await {
removed_transactions.push(tx);
}
}
let sorted_removed = ttor_sorted_mempool_entries(
removed_transactions
.into_iter()
.map(|tx| MempoolEntry::new(0, tx)) .collect(),
);
let reverse_sorted: Vec<_> = sorted_removed.into_iter().rev().collect();
for entry in reverse_sorted {
let txid = entry.tx.txid();
self.remove(&txid, txquery).await;
changed_txs.insert(txid);
}
timer.observe_duration();
let timer = self.stats.start_timer("fees");
self.update_fee_histogram().await;
timer.observe_duration();
self.stats.count.set(self.items.read().await.len() as i64);
Ok(changed_txs)
}
pub async fn tx_confirmation_state(
&self,
txid: &Txid,
height: Option<u32>,
) -> ConfirmationState {
if let Some(height) = height {
if height != MEMPOOL_HEIGHT {
return ConfirmationState::Confirmed;
}
}
if let Some(tx) = self.get_txn(txid).await {
for input in tx.input.iter() {
if self.contains_funding_tx(&input.previous_output).await {
return ConfirmationState::UnconfirmedParent;
}
}
ConfirmationState::InMempool
} else {
ConfirmationState::Indeterminate
}
}
async fn add(&self, txid: &Txid, entry: MempoolEntry, txquery: &TxQuery) {
let writebatch = WriteBatch::new();
let prefilled_spent_data = match txquery.create_prefilled_spent_data(&entry.tx).await {
Ok(data) => data,
Err(e) => {
debug!(
"mempool add: failed to create prefilled spent data for tx (orphan?) {}: {}",
txid.to_hex(),
e
);
return;
}
};
let spentupdatebatch =
SpentUpdateBatch::new().add_prefilled_spent_data(prefilled_spent_data.clone());
index_transaction(
&writebatch,
&spentupdatebatch,
&entry.tx,
MEMPOOL_HEIGHT as usize,
);
self.index.write_batch(&writebatch);
self.index.update_spends(&spentupdatebatch);
{
let mut items = self.items.write().await;
items.insert(*txid, entry);
}
}
async fn remove(&self, txid: &Txid, txquery: &TxQuery) {
let tx = {
let items = self.items.read().await;
match items.get(txid) {
Some(entry) => entry.tx.clone(),
None => {
return;
}
}
};
let writebatch = WriteBatch::new();
let spentupdatebatch = SpentUpdateBatch::new().set_erase_only();
let spentupdatebatch = match txquery.create_prefilled_spent_data(&tx).await {
Ok(data) => spentupdatebatch.add_prefilled_spent_data(data),
Err(e) => {
debug!(
"mempool remove: failed to create prefilled spent data for tx (orphan?) {}: {}",
txid.to_hex(),
e
);
spentupdatebatch
}
};
index_transaction(&writebatch, &spentupdatebatch, &tx, MEMPOOL_HEIGHT as usize);
self.index.erase_batch(&writebatch);
self.index.update_spends(&spentupdatebatch);
{
let mut items = self.items.write().await;
items.remove(txid);
}
}
async fn update_fee_histogram(&self) {
let items = self.items.read().await;
let mut histogram = self.histogram.lock().await;
let mut entries: Vec<&MempoolEntry> = items.values().collect();
entries
.sort_unstable_by(|e1, e2| e1.fee_per_byte().partial_cmp(&e2.fee_per_byte()).unwrap());
*histogram = electrum_fees(&entries);
self.stats.update(&entries).await;
}
}
fn electrum_fees(entries: &[&MempoolEntry]) -> Vec<(f32, u32)> {
let mut histogram = vec![];
let mut bin_size = 0;
let mut last_fee_rate = 0.0;
let error_margin = f32::EPSILON;
for e in entries.iter().rev() {
let fee_rate = e.fee_per_byte();
if bin_size > VSIZE_BIN_WIDTH && (last_fee_rate - fee_rate).abs() > error_margin {
histogram.push((last_fee_rate, bin_size));
bin_size = 0;
}
last_fee_rate = fee_rate;
bin_size += e.size() as u32;
}
if bin_size > 0 {
histogram.push((last_fee_rate, bin_size));
}
histogram
}
#[cfg(test)]
mod tests {
use bitcoincash::blockdata::script::Builder;
use crate::chaindef::{Transaction, TxOut};
#[cfg(nexa)]
fn one_output_tx() -> Transaction {
use crate::nexa::transaction::TxOutType;
let out = TxOut {
txout_type: TxOutType::SATOSHI as u8,
value: 0,
script_pubkey: vec![].into(),
};
Transaction {
version: 2,
lock_time: 0,
input: vec![],
output: vec![out],
}
}
#[cfg(bch)]
fn one_output_tx() -> Transaction {
let out = TxOut {
value: 0,
script_pubkey: vec![].into(),
token: None,
};
Transaction {
version: 2,
lock_time: bitcoincash::PackedLockTime(0),
input: vec![],
output: vec![out],
}
}
fn dummy_tx(desired_size: usize) -> Transaction {
let mut tx = one_output_tx();
let size = tx.size();
let mut extra_bytes_size = desired_size.saturating_sub(size);
let mut extra_bytes: Vec<u8> = vec![0; extra_bytes_size];
tx.output[0].script_pubkey = Builder::from(extra_bytes).into_script();
while tx.size() > desired_size {
extra_bytes_size -= 1;
extra_bytes = vec![0; extra_bytes_size];
tx.output[0].script_pubkey = Builder::from(extra_bytes).into_script();
}
assert_eq!(tx.size(), desired_size);
tx
}
#[test]
fn test_fakestore() {
use super::MempoolEntry;
use crate::mempool::electrum_fees;
let entries = [
&MempoolEntry::new(1_000, dummy_tx(1_000)),
&MempoolEntry::new(10_000, dummy_tx(10_000)),
&MempoolEntry::new(50_000, dummy_tx(50_000)),
&MempoolEntry::new(120_000, dummy_tx(60_000)),
&MempoolEntry::new(210_000, dummy_tx(70_000)),
&MempoolEntry::new(320_000, dummy_tx(80_000)),
];
assert_eq!(
electrum_fees(&entries[..]),
vec![(3.0, 150_000), (1.0, 121_000)]
);
}
}