zebra_rpc/methods/types/get_block_template/
zip317.rs1use std::collections::{HashMap, HashSet};
10
11use rand::{
12 distributions::{Distribution, WeightedIndex},
13 prelude::thread_rng,
14};
15
16use zcash_keys::address::Address;
17
18use zebra_chain::{
19 amount::NegativeOrZero,
20 block::{Height, MAX_BLOCK_BYTES},
21 parameters::Network,
22 transaction::{self, zip317::BLOCK_UNPAID_ACTION_LIMIT, Transaction, VerifiedUnminedTx},
23};
24use zebra_consensus::MAX_BLOCK_SIGOPS;
25use zebra_node_services::mempool::TransactionDependencies;
26
27use crate::methods::types::transaction::TransactionTemplate;
28
29#[cfg(test)]
30mod tests;
31
32#[cfg(test)]
33use crate::methods::types::get_block_template::InBlockTxDependenciesDepth;
34
35use super::standard_coinbase_outputs;
36
37#[cfg(test)]
39type SelectedMempoolTx = (InBlockTxDependenciesDepth, VerifiedUnminedTx);
40
41#[cfg(not(test))]
43type SelectedMempoolTx = VerifiedUnminedTx;
44
45pub fn select_mempool_transactions(
56 network: &Network,
57 next_block_height: Height,
58 miner_address: &Address,
59 mempool_txs: Vec<VerifiedUnminedTx>,
60 mempool_tx_deps: TransactionDependencies,
61 extra_coinbase_data: Vec<u8>,
62) -> Vec<SelectedMempoolTx> {
63 let fake_coinbase_tx = fake_coinbase_transaction(
66 network,
67 next_block_height,
68 miner_address,
69 extra_coinbase_data,
70 );
71
72 let tx_dependencies = mempool_tx_deps.dependencies();
73 let (independent_mempool_txs, mut dependent_mempool_txs): (HashMap<_, _>, HashMap<_, _>) =
74 mempool_txs
75 .into_iter()
76 .map(|tx| (tx.transaction.id.mined_id(), tx))
77 .partition(|(tx_id, _tx)| !tx_dependencies.contains_key(tx_id));
78
79 let (mut conventional_fee_txs, mut low_fee_txs): (Vec<_>, Vec<_>) = independent_mempool_txs
81 .into_values()
82 .partition(VerifiedUnminedTx::pays_conventional_fee);
83
84 let mut selected_txs = Vec::new();
85
86 let mut remaining_block_bytes: usize = MAX_BLOCK_BYTES.try_into().expect("fits in memory");
88 let mut remaining_block_sigops = MAX_BLOCK_SIGOPS;
89 let mut remaining_block_unpaid_actions: u32 = BLOCK_UNPAID_ACTION_LIMIT;
90
91 remaining_block_bytes -= fake_coinbase_tx.data.as_ref().len();
93 remaining_block_sigops -= fake_coinbase_tx.sigops;
94
95 let mut conventional_fee_tx_weights = setup_fee_weighted_index(&conventional_fee_txs);
98
99 while let Some(tx_weights) = conventional_fee_tx_weights {
100 conventional_fee_tx_weights = checked_add_transaction_weighted_random(
101 &mut conventional_fee_txs,
102 &mut dependent_mempool_txs,
103 tx_weights,
104 &mut selected_txs,
105 &mempool_tx_deps,
106 &mut remaining_block_bytes,
107 &mut remaining_block_sigops,
108 &mut remaining_block_unpaid_actions,
111 );
112 }
113
114 let mut low_fee_tx_weights = setup_fee_weighted_index(&low_fee_txs);
116
117 while let Some(tx_weights) = low_fee_tx_weights {
118 low_fee_tx_weights = checked_add_transaction_weighted_random(
119 &mut low_fee_txs,
120 &mut dependent_mempool_txs,
121 tx_weights,
122 &mut selected_txs,
123 &mempool_tx_deps,
124 &mut remaining_block_bytes,
125 &mut remaining_block_sigops,
126 &mut remaining_block_unpaid_actions,
127 );
128 }
129
130 selected_txs
131}
132
133pub fn fake_coinbase_transaction(
141 net: &Network,
142 height: Height,
143 miner_address: &Address,
144 extra_coinbase_data: Vec<u8>,
145) -> TransactionTemplate<NegativeOrZero> {
146 let miner_fee = 1.try_into().expect("amount is valid and non-negative");
156 let outputs = standard_coinbase_outputs(net, height, miner_address, miner_fee);
157 let coinbase = Transaction::new_v5_coinbase(net, height, outputs, extra_coinbase_data).into();
158
159 TransactionTemplate::from_coinbase(&coinbase, miner_fee)
160}
161
162fn setup_fee_weighted_index(transactions: &[VerifiedUnminedTx]) -> Option<WeightedIndex<f32>> {
166 if transactions.is_empty() {
167 return None;
168 }
169
170 let tx_weights: Vec<f32> = transactions.iter().map(|tx| tx.fee_weight_ratio).collect();
171
172 WeightedIndex::new(tx_weights).ok()
174}
175
176fn has_direct_dependencies(
180 candidate_tx_deps: Option<&HashSet<transaction::Hash>>,
181 selected_txs: &Vec<SelectedMempoolTx>,
182) -> bool {
183 let Some(deps) = candidate_tx_deps else {
184 return true;
185 };
186
187 if selected_txs.len() < deps.len() {
188 return false;
189 }
190
191 let mut num_available_deps = 0;
192 for tx in selected_txs {
193 #[cfg(test)]
194 let (_, tx) = tx;
195 if deps.contains(&tx.transaction.id.mined_id()) {
196 num_available_deps += 1;
197 } else {
198 continue;
199 }
200
201 if num_available_deps == deps.len() {
202 return true;
203 }
204 }
205
206 false
207}
208
209#[cfg(test)]
212fn dependencies_depth(
213 dependent_tx_id: &transaction::Hash,
214 mempool_tx_deps: &TransactionDependencies,
215) -> InBlockTxDependenciesDepth {
216 let mut current_level = 0;
217 let mut current_level_deps = mempool_tx_deps.direct_dependencies(dependent_tx_id);
218 while !current_level_deps.is_empty() {
219 current_level += 1;
220 current_level_deps = current_level_deps
221 .iter()
222 .flat_map(|dep_id| mempool_tx_deps.direct_dependencies(dep_id))
223 .collect();
224 }
225
226 current_level
227}
228
229#[allow(clippy::too_many_arguments)]
241fn checked_add_transaction_weighted_random(
242 candidate_txs: &mut Vec<VerifiedUnminedTx>,
243 dependent_txs: &mut HashMap<transaction::Hash, VerifiedUnminedTx>,
244 tx_weights: WeightedIndex<f32>,
245 selected_txs: &mut Vec<SelectedMempoolTx>,
246 mempool_tx_deps: &TransactionDependencies,
247 remaining_block_bytes: &mut usize,
248 remaining_block_sigops: &mut u32,
249 remaining_block_unpaid_actions: &mut u32,
250) -> Option<WeightedIndex<f32>> {
251 let (new_tx_weights, candidate_tx) =
254 choose_transaction_weighted_random(candidate_txs, tx_weights);
255
256 if !candidate_tx.try_update_block_template_limits(
257 remaining_block_bytes,
258 remaining_block_sigops,
259 remaining_block_unpaid_actions,
260 ) {
261 return new_tx_weights;
262 }
263
264 let tx_dependencies = mempool_tx_deps.dependencies();
265 let selected_tx_id = &candidate_tx.transaction.id.mined_id();
266 debug_assert!(
267 !tx_dependencies.contains_key(selected_tx_id),
268 "all candidate transactions should be independent"
269 );
270
271 #[cfg(not(test))]
272 selected_txs.push(candidate_tx);
273
274 #[cfg(test)]
275 selected_txs.push((0, candidate_tx));
276
277 let mut current_level_dependents = mempool_tx_deps.direct_dependents(selected_tx_id);
280 while !current_level_dependents.is_empty() {
281 let mut next_level_dependents = HashSet::new();
282
283 for dependent_tx_id in ¤t_level_dependents {
284 if has_direct_dependencies(tx_dependencies.get(dependent_tx_id), selected_txs) {
291 let Some(candidate_tx) = dependent_txs.remove(dependent_tx_id) else {
292 continue;
293 };
294
295 if !candidate_tx.pays_conventional_fee() {
298 continue;
299 }
300
301 if !candidate_tx.try_update_block_template_limits(
302 remaining_block_bytes,
303 remaining_block_sigops,
304 remaining_block_unpaid_actions,
305 ) {
306 continue;
307 }
308
309 #[cfg(not(test))]
310 selected_txs.push(candidate_tx);
311
312 #[cfg(test)]
313 selected_txs.push((
314 dependencies_depth(dependent_tx_id, mempool_tx_deps),
315 candidate_tx,
316 ));
317
318 next_level_dependents.extend(mempool_tx_deps.direct_dependents(dependent_tx_id));
319 }
320 }
321
322 current_level_dependents = next_level_dependents;
323 }
324
325 new_tx_weights
326}
327
328trait TryUpdateBlockLimits {
329 fn try_update_block_template_limits(
335 &self,
336 remaining_block_bytes: &mut usize,
337 remaining_block_sigops: &mut u32,
338 remaining_block_unpaid_actions: &mut u32,
339 ) -> bool;
340}
341
342impl TryUpdateBlockLimits for VerifiedUnminedTx {
343 fn try_update_block_template_limits(
344 &self,
345 remaining_block_bytes: &mut usize,
346 remaining_block_sigops: &mut u32,
347 remaining_block_unpaid_actions: &mut u32,
348 ) -> bool {
349 if self.transaction.size <= *remaining_block_bytes
357 && self.sigops <= *remaining_block_sigops
358 && self.unpaid_actions <= *remaining_block_unpaid_actions
359 {
360 *remaining_block_bytes -= self.transaction.size;
361 *remaining_block_sigops -= self.sigops;
362
363 *remaining_block_unpaid_actions -= self.unpaid_actions;
366
367 true
368 } else {
369 false
370 }
371 }
372}
373
374fn choose_transaction_weighted_random(
379 candidate_txs: &mut Vec<VerifiedUnminedTx>,
380 weighted_index: WeightedIndex<f32>,
381) -> (Option<WeightedIndex<f32>>, VerifiedUnminedTx) {
382 let candidate_position = weighted_index.sample(&mut thread_rng());
383 let candidate_tx = candidate_txs.swap_remove(candidate_position);
384
385 (setup_fee_weighted_index(candidate_txs), candidate_tx)
389}