#![warn(clippy::all)] #![warn(clippy::pedantic)] #![warn(clippy::nursery)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::float_cmp)] use napi::bindgen_prelude::{Result, Uint8Array}; use napi_derive::napi; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState}; mod audit_transaction; mod gbt; mod thread_transaction; mod u32_hasher_types; mod utils; use thread_transaction::ThreadTransaction; /// This is the starting capacity for HashMap/Vec/etc. that deal with transactions. /// `HashMap` doubles capacity when it hits it, so 2048 is a decent tradeoff between /// not wasting too much memory when it's below this, and allowing for less re-allocations /// by virtue of starting with such a large capacity. /// /// Note: This doesn't *have* to be a power of 2. (uwu) const STARTING_CAPACITY: usize = 2048; type ThreadTransactionsMap = HashMap; #[napi] pub struct GbtGenerator { thread_transactions: Arc>, } #[napi] impl GbtGenerator { #[napi(constructor)] #[allow(clippy::new_without_default)] #[must_use] pub fn new() -> Self { Self { thread_transactions: Arc::new(Mutex::new(u32hashmap_with_capacity(STARTING_CAPACITY))), } } /// # Errors /// /// Rejects if the thread panics or if the Mutex is poisoned. #[napi] pub async fn make(&self, mempool_buffer: Uint8Array) -> Result { run_task(Arc::clone(&self.thread_transactions), move |map| { for tx in ThreadTransaction::batch_from_buffer(&mempool_buffer) { map.insert(tx.uid, tx); } }) .await } /// # Errors /// /// Rejects if the thread panics or if the Mutex is poisoned. #[napi] pub async fn update(&self, new_txs: Uint8Array, remove_txs: Uint8Array) -> Result { run_task(Arc::clone(&self.thread_transactions), move |map| { for tx in ThreadTransaction::batch_from_buffer(&new_txs) { map.insert(tx.uid, tx); } for txid in &utils::txids_from_buffer(&remove_txs) { map.remove(txid); } }) .await } } /// The result from calling the gbt function. /// /// This tuple contains the following: /// blocks: A 2D Vector of transaction IDs (u32), the inner Vecs each represent a block. /// clusters: A 2D Vector of transaction IDs representing clusters of dependent mempool transactions /// rates: A Vector of tuples containing transaction IDs (u32) and effective fee per vsize (f64) #[napi(constructor)] pub struct GbtResult { pub blocks: Vec>, pub clusters: Vec>, pub rates: Vec>, // Tuples not supported. u32 fits inside f64 } /// All on another thread, this runs an arbitrary task in between /// taking the lock and running gbt. /// /// Rather than filling / updating the `HashMap` on the main thread, /// this allows for `HashMap` modifying tasks to be run before running and returning gbt results. /// /// `thread_transactions` is a cloned `Arc` of the `Mutex` for the `HashMap` state. /// `callback` is a `'static + Send` `FnOnce` closure/function that takes a mutable reference /// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds) async fn run_task( thread_transactions: Arc>, callback: F, ) -> Result where F: FnOnce(&mut ThreadTransactionsMap) + Send + 'static, { let handle = napi::tokio::task::spawn_blocking(move || { let mut map = thread_transactions .lock() .map_err(|_| napi::Error::from_reason("THREAD_TRANSACTIONS Mutex poisoned"))?; callback(&mut map); Ok(gbt::gbt(&mut map)) }); handle .await .map_err(|_| napi::Error::from_reason("thread panicked"))? }