kaspa_mining/block_template/
selector.rs1use kaspa_core::{time::Stopwatch, trace};
2use rand::Rng;
3use std::collections::HashMap;
4
5use crate::model::candidate_tx::CandidateTransaction;
6
7use super::{
8 model::tx::{CandidateList, SelectableTransaction, SelectableTransactions, TransactionIndex},
9 policy::Policy,
10};
11use kaspa_consensus_core::{
12 block::TemplateTransactionSelector,
13 subnets::SubnetworkId,
14 tx::{Transaction, TransactionId},
15};
16
17pub(crate) const ALPHA: i32 = 3;
22
23const REBALANCE_THRESHOLD: f64 = 0.95;
30
31pub struct RebalancingWeightedTransactionSelector {
32 policy: Policy,
33 transactions: Vec<CandidateTransaction>,
35 selectable_txs: SelectableTransactions,
37
38 selected_txs: Vec<TransactionIndex>,
40
41 selected_txs_map: Option<HashMap<TransactionId, TransactionIndex>>,
44
45 candidate_list: CandidateList,
47 overall_rejections: usize,
48 used_count: usize,
49 used_p: f64,
50 total_mass: u64,
51 total_fees: u64,
52 gas_usage_map: HashMap<SubnetworkId, u64>,
53}
54
55impl RebalancingWeightedTransactionSelector {
56 pub fn new(policy: Policy, mut transactions: Vec<CandidateTransaction>) -> Self {
57 let _sw = Stopwatch::<100>::with_threshold("TransactionsSelector::new op");
58 transactions.sort_by(|a, b| a.tx.subnetwork_id.cmp(&b.tx.subnetwork_id));
60
61 let mut selector = Self {
63 policy,
64 transactions,
65 selectable_txs: Default::default(),
66 selected_txs: Default::default(),
67 selected_txs_map: None,
68 candidate_list: Default::default(),
69 overall_rejections: 0,
70 used_count: 0,
71 used_p: 0.0,
72 total_mass: 0,
73 total_fees: 0,
74 gas_usage_map: Default::default(),
75 };
76
77 selector.selectable_txs =
79 selector.transactions.iter().map(|x| SelectableTransaction::new(selector.calc_tx_value(x), 0, ALPHA)).collect();
80 selector.candidate_list = CandidateList::new(&selector.selectable_txs);
82
83 selector
84 }
85
86 pub fn select_transactions(&mut self) -> Vec<Transaction> {
107 let _sw = Stopwatch::<15>::with_threshold("select_transaction op");
108 let mut rng = rand::thread_rng();
109
110 self.reset_selection();
111
112 while self.candidate_list.candidates.len() - self.used_count > 0 {
113 if self.used_p >= REBALANCE_THRESHOLD * self.candidate_list.total_p {
115 self.candidate_list = self.candidate_list.rebalanced(&self.selectable_txs);
116 self.used_count = 0;
117 self.used_p = 0.0;
118
119 if self.candidate_list.is_empty() {
121 break;
122 }
123 }
124
125 let r = rng.gen::<f64>() * self.candidate_list.total_p;
127 let selected_candidate_idx = self.candidate_list.find(r);
128 let selected_candidate = self.candidate_list.candidates.get_mut(selected_candidate_idx).unwrap();
129
130 if selected_candidate.is_marked_for_deletion {
133 continue;
134 }
135 let selected_tx = &self.transactions[selected_candidate.index];
136
137 let next_total_mass = self.total_mass.checked_add(selected_tx.calculated_mass);
140 if next_total_mass.is_none() || next_total_mass.unwrap() > self.policy.max_block_mass {
141 trace!("Tx {0} would exceed the max block mass. As such, stopping.", selected_tx.tx.id());
142 break;
143 }
144
145 if !selected_tx.tx.subnetwork_id.is_builtin_or_native() {
148 let subnetwork_id = selected_tx.tx.subnetwork_id.clone();
149 let gas_usage = self.gas_usage_map.entry(subnetwork_id.clone()).or_insert(0);
150 let tx_gas = selected_tx.tx.gas;
151 let next_gas_usage = (*gas_usage).checked_add(tx_gas);
152 if next_gas_usage.is_none() || next_gas_usage.unwrap() > self.selectable_txs[selected_candidate.index].gas_limit {
153 trace!(
154 "Tx {0} would exceed the gas limit in subnetwork {1}. Removing all remaining txs from this subnetwork.",
155 selected_tx.tx.id(),
156 subnetwork_id
157 );
158 for i in selected_candidate_idx..self.candidate_list.candidates.len() {
159 let transaction_index = self.candidate_list.candidates[i].index;
160 if subnetwork_id < self.transactions[transaction_index].tx.subnetwork_id {
163 break;
164 }
165 let current = self.candidate_list.candidates.get_mut(i).unwrap();
166
167 current.is_marked_for_deletion = true;
169 self.used_count += 1;
170 self.used_p += self.selectable_txs[transaction_index].p;
171 }
172 continue;
173 }
174 *gas_usage = next_gas_usage.unwrap();
176 }
177
178 self.selected_txs.push(selected_candidate.index);
182 self.total_mass += selected_tx.calculated_mass;
183 self.total_fees += selected_tx.calculated_fee;
184
185 trace!("Adding tx {0} (fee per gram: {1})", selected_tx.tx.id(), selected_tx.calculated_fee / selected_tx.calculated_mass);
186
187 selected_candidate.is_marked_for_deletion = true;
189 self.used_count += 1;
190 self.used_p += self.selectable_txs[selected_candidate.index].p;
191 }
192
193 self.selected_txs.sort();
194
195 self.get_transactions()
196 }
197
198 fn get_transactions(&self) -> Vec<Transaction> {
199 self.selected_txs.iter().map(|x| self.transactions[*x].tx.as_ref().clone()).collect()
201 }
202
203 fn reset_selection(&mut self) {
204 assert_eq!(self.transactions.len(), self.selectable_txs.len());
205 self.selected_txs.clear();
206 self.selected_txs.reserve_exact(self.transactions.len());
208 self.selected_txs_map = None;
209 }
210
211 fn calc_tx_value(&self, transaction: &CandidateTransaction) -> f64 {
215 let mass_limit = self.policy.max_block_mass as f64;
216 let mass = transaction.calculated_mass as f64;
217 let fee = transaction.calculated_fee as f64;
218 if transaction.tx.subnetwork_id.is_builtin_or_native() {
219 fee / mass / mass_limit
220 } else {
221 let gas_limit = u64::MAX as f64;
223 fee / mass / mass_limit + transaction.tx.gas as f64 / gas_limit
224 }
225 }
226}
227
228impl TemplateTransactionSelector for RebalancingWeightedTransactionSelector {
229 fn select_transactions(&mut self) -> Vec<Transaction> {
230 self.select_transactions()
231 }
232
233 fn reject_selection(&mut self, tx_id: TransactionId) {
234 let selected_txs_map = self
235 .selected_txs_map
236 .get_or_insert_with(|| self.selected_txs.iter().map(|&x| (self.transactions[x].tx.id(), x)).collect());
238 let tx_index = selected_txs_map.remove(&tx_id).expect("only previously selected txs can be rejected (and only once)");
239 let tx = &self.transactions[tx_index];
240 self.total_mass -= tx.calculated_mass;
241 self.total_fees -= tx.calculated_fee;
242 if !tx.tx.subnetwork_id.is_builtin_or_native() {
243 *self.gas_usage_map.get_mut(&tx.tx.subnetwork_id).expect("previously selected txs have an entry") -= tx.tx.gas;
244 }
245 self.overall_rejections += 1;
246 }
247
248 fn is_successful(&self) -> bool {
249 const SUFFICIENT_MASS_THRESHOLD: f64 = 0.8;
250 const LOW_REJECTION_FRACTION: f64 = 0.2;
251
252 self.overall_rejections == 0
254 || (self.total_mass as f64) > self.policy.max_block_mass as f64 * SUFFICIENT_MASS_THRESHOLD
255 || (self.overall_rejections as f64) < self.transactions.len() as f64 * LOW_REJECTION_FRACTION
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use itertools::Itertools;
263 use kaspa_consensus_core::{
264 constants::{MAX_TX_IN_SEQUENCE_NUM, SOMPI_PER_KASPA, TX_VERSION},
265 mass::transaction_estimated_serialized_size,
266 subnets::SUBNETWORK_ID_NATIVE,
267 tx::{Transaction, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput},
268 };
269 use kaspa_txscript::{pay_to_script_hash_signature_script, test_helpers::op_true_script};
270 use std::{collections::HashSet, sync::Arc};
271
272 use crate::{
273 mempool::{
274 config::DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
275 model::frontier::selectors::{SequenceSelector, SequenceSelectorInput, SequenceSelectorTransaction},
276 },
277 model::candidate_tx::CandidateTransaction,
278 };
279
280 #[test]
281 fn test_reject_transaction() {
282 const TX_INITIAL_COUNT: usize = 1_000;
283
284 let transactions = (0..TX_INITIAL_COUNT).map(|i| create_transaction(SOMPI_PER_KASPA * (i + 1) as u64)).collect_vec();
286 let masses: HashMap<_, _> = transactions.iter().map(|tx| (tx.tx.id(), tx.calculated_mass)).collect();
287 let sequence: SequenceSelectorInput =
288 transactions.iter().map(|tx| SequenceSelectorTransaction::new(tx.tx.clone(), tx.calculated_mass)).collect();
289
290 let policy = Policy::new(100_000);
291 let selectors: [Box<dyn TemplateTransactionSelector>; 2] = [
292 Box::new(RebalancingWeightedTransactionSelector::new(policy.clone(), transactions)),
293 Box::new(SequenceSelector::new(sequence, policy.clone())),
294 ];
295
296 for mut selector in selectors {
297 let (mut kept, mut rejected) = (HashSet::new(), HashSet::new());
298 let mut reject_count = 32;
299 let mut total_mass = 0;
300 for i in 0..10 {
301 let selected_txs = selector.select_transactions();
302 if i > 0 {
303 assert_eq!(
304 selected_txs.len(),
305 reject_count,
306 "subsequent select calls are expected to only refill the previous rejections"
307 );
308 reject_count /= 2;
309 }
310 for tx in selected_txs.iter() {
311 total_mass += masses[&tx.id()];
312 kept.insert(tx.id()).then_some(()).expect("selected txs should never repeat themselves");
313 assert!(!rejected.contains(&tx.id()), "selected txs should never repeat themselves");
314 }
315 assert!(total_mass <= policy.max_block_mass);
316 selected_txs.iter().take(reject_count).for_each(|x| {
317 total_mass -= masses[&x.id()];
318 selector.reject_selection(x.id());
319 kept.remove(&x.id()).then_some(()).expect("was just inserted");
320 rejected.insert(x.id()).then_some(()).expect("was just verified");
321 });
322 }
323 }
324 }
325
326 fn create_transaction(value: u64) -> CandidateTransaction {
327 let previous_outpoint = TransactionOutpoint::new(TransactionId::default(), 0);
328 let (script_public_key, redeem_script) = op_true_script();
329 let signature_script = pay_to_script_hash_signature_script(redeem_script, vec![]).expect("the redeem script is canonical");
330
331 let input = TransactionInput::new(previous_outpoint, signature_script, MAX_TX_IN_SEQUENCE_NUM, 1);
332 let output = TransactionOutput::new(value - DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE, script_public_key);
333 let tx = Arc::new(Transaction::new(TX_VERSION, vec![input], vec![output], 0, SUBNETWORK_ID_NATIVE, 0, vec![]));
334 let calculated_mass = transaction_estimated_serialized_size(&tx);
335 let calculated_fee = DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE;
336
337 CandidateTransaction { tx, calculated_fee, calculated_mass }
338 }
339}