Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

O(k log n) mempool transaction sampler + Fee estimation API #513

Merged
merged 76 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
7736af3
initial fee estimation logic + a python notebook detailing a challenge
michaelsutton Jul 25, 2024
5b8b393
initial usage of btreeset for ready transactions
michaelsutton Jul 26, 2024
5aeb180
initial frontier + sampling logic
michaelsutton Jul 31, 2024
02a2f7a
mempool sampling benchmark (wip)
michaelsutton Aug 1, 2024
488d0cb
Use arc tx rather than tx id in order to save the indirect map access…
michaelsutton Aug 1, 2024
f191ae3
Modify mempool bmk and simnet settings
michaelsutton Aug 1, 2024
0eb70e8
Temp: rpc message initial
michaelsutton Aug 1, 2024
71a8609
Move sample to rand utils
michaelsutton Aug 1, 2024
415eef2
Fix top bucket sampling to match analysis
michaelsutton Aug 1, 2024
54bf205
Add outliers to the bmk
michaelsutton Aug 1, 2024
df7b229
sample comments and doc
michaelsutton Aug 1, 2024
f502c0a
use b plus tree with argument customization in order to implement a h…
michaelsutton Aug 1, 2024
e34f047
todo
michaelsutton Aug 1, 2024
78cfd96
keep a computed weight field
michaelsutton Aug 1, 2024
cc825ef
Test feerate weight queries + an implied fix (change <= to <)
michaelsutton Aug 2, 2024
44033d8
temp remove warns
michaelsutton Aug 2, 2024
5d596d3
1. use inner BPlusTree in order to allow access to iterator as double…
michaelsutton Aug 2, 2024
542f8ba
rename
michaelsutton Aug 2, 2024
6b0fee8
test btree rev iter
michaelsutton Aug 2, 2024
c3a8a38
clamp the query to the bounds (logically)
michaelsutton Aug 2, 2024
632008f
use a larger tree for tests, add checks for clamped search bounds
michaelsutton Aug 4, 2024
67eed1c
Add benchmarks for frontier insertions and removals
michaelsutton Aug 4, 2024
c66d67b
add item removal to the queries test
michaelsutton Aug 4, 2024
102043a
Important numeric stability improvement: use the visitor api to imple…
michaelsutton Aug 4, 2024
cc0877d
test highly irregular sampling
michaelsutton Aug 4, 2024
1beb861
Implement initial selectors + order the code a bit
michaelsutton Aug 4, 2024
0d0b513
Enhance and use the new selectors
michaelsutton Aug 4, 2024
398889f
rename
michaelsutton Aug 4, 2024
2683286
minor refactor
michaelsutton Aug 4, 2024
524e1a3
minor optimizations etc
michaelsutton Aug 4, 2024
ebda7d2
increase default devnet prealloc amount to 100 tkas
michaelsutton Aug 4, 2024
05be8bb
cleanup
michaelsutton Aug 4, 2024
7c6325e
cleanup
michaelsutton Aug 4, 2024
fc08f26
initial build_feerate_estimator
michaelsutton Aug 5, 2024
1729cbb
todos
michaelsutton Aug 5, 2024
3122e16
minor
michaelsutton Aug 5, 2024
e8238b5
Remove obsolete constant
michaelsutton Aug 5, 2024
6a28c58
Restructure search tree methods into an encapsulated struct
michaelsutton Aug 5, 2024
5159c09
Rename module
michaelsutton Aug 5, 2024
fd18ef8
documentation and comments
michaelsutton Aug 5, 2024
e5606f6
optimization: cmp with cached weight rather than compute feerate
michaelsutton Aug 5, 2024
d534398
minor
michaelsutton Aug 5, 2024
8040d49
Finalize build fee estimator and add tests
michaelsutton Aug 5, 2024
c931b89
updated notebook
michaelsutton Aug 5, 2024
e2024f9
fee estimator todos
michaelsutton Aug 5, 2024
25c2333
expose get_realtime_feerate_estimations from the mining manager
michaelsutton Aug 5, 2024
8e27b40
min feerate from config
michaelsutton Aug 6, 2024
8aec49b
sample_inplace doc
michaelsutton Aug 6, 2024
3109404
test_total_mass_tracking
michaelsutton Aug 6, 2024
0660596
test prefix weights
michaelsutton Aug 6, 2024
765c940
test sequence selector
michaelsutton Aug 6, 2024
bca599a
fix rpc feerate structs + comment
michaelsutton Aug 6, 2024
a3eda18
utils: expiring cache
michaelsutton Aug 6, 2024
d606c3b
rpc core fee estimate call
michaelsutton Aug 7, 2024
85bd72b
fee estimate verbose
michaelsutton Aug 7, 2024
797d5d8
grpc fee estimate calls
michaelsutton Aug 7, 2024
37cd40c
Merge branch 'dev' into feerate-est-ng
michaelsutton Aug 7, 2024
18a8fc4
Benchmark worst-case collision cases + an optimization addressing the…
michaelsutton Aug 11, 2024
40a1df5
Expose SearchTree
michaelsutton Aug 11, 2024
00509a5
cli support (with @coderofstuff)
michaelsutton Aug 11, 2024
715bcf2
addressing a few minor review comments
michaelsutton Aug 13, 2024
d74f831
feerate estimator - handle various edge cases (with @tiram88)
michaelsutton Aug 13, 2024
073d6b3
one more test (with @tiram88)
michaelsutton Aug 13, 2024
9faff73
build_feerate_estimator - fix edge case of not trying the estimator w…
michaelsutton Aug 14, 2024
fa5ea38
monitor feerate estimations (debug print every 10 secs)
michaelsutton Aug 15, 2024
ebe6f94
follow rpc naming conventions
michaelsutton Aug 15, 2024
abf30d3
proto leave blank index range
michaelsutton Aug 15, 2024
6731521
insert in correct abc location (keeping rest of the array as is for e…
michaelsutton Aug 15, 2024
c198189
fix comment to reflect the most updated final algo
michaelsutton Aug 15, 2024
79790d3
document feerate
michaelsutton Aug 15, 2024
095baab
update notebook
michaelsutton Aug 15, 2024
49759f1
add an additional point to normal feerate buckets (between normal and…
michaelsutton Aug 15, 2024
c7e60cb
enum order
michaelsutton Aug 15, 2024
58d70e2
Merge branch 'dev' into feerate-est-ng
michaelsutton Aug 15, 2024
ddc1271
with 1 sec there are rare cases where mempool size does not change an…
michaelsutton Aug 16, 2024
adbaf18
final stuff
michaelsutton Aug 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions cli/src/modules/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,15 @@ impl Rpc {
}
}
}
RpcApiOps::GetFeeEstimate => {
let result = rpc.get_fee_estimate_call(GetFeeEstimateRequest {}).await?;
self.println(&ctx, result);
}
RpcApiOps::GetFeeEstimateExperimental => {
let verbose = if argv.is_empty() { false } else { argv.remove(0).parse().unwrap_or(false) };
let result = rpc.get_fee_estimate_experimental_call(GetFeeEstimateExperimentalRequest { verbose }).await?;
self.println(&ctx, result);
}
_ => {
tprintln!(ctx, "rpc method exists but is not supported by the cli: '{op_str}'\r\n");
return Ok(());
Expand Down
3 changes: 2 additions & 1 deletion consensus/core/src/config/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,8 @@ pub const SIMNET_PARAMS: Params = Params {
target_time_per_block: Testnet11Bps::target_time_per_block(),
past_median_time_sample_rate: Testnet11Bps::past_median_time_sample_rate(),
difficulty_sample_rate: Testnet11Bps::difficulty_adjustment_sample_rate(),
max_block_parents: Testnet11Bps::max_block_parents(),
// For simnet, we deviate from TN11 configuration and allow at least 64 parents in order to support mempool benchmarks out of the box
max_block_parents: if Testnet11Bps::max_block_parents() > 64 { Testnet11Bps::max_block_parents() } else { 64 },
michaelsutton marked this conversation as resolved.
Show resolved Hide resolved
mergeset_size_limit: Testnet11Bps::mergeset_size_limit(),
merge_depth: Testnet11Bps::merge_depth_bound(),
finality_depth: Testnet11Bps::finality_depth(),
Expand Down
2 changes: 1 addition & 1 deletion kaspad/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl Default for Args {
#[cfg(feature = "devnet-prealloc")]
prealloc_address: None,
#[cfg(feature = "devnet-prealloc")]
prealloc_amount: 1_000_000,
prealloc_amount: 10_000_000_000,

disable_upnp: false,
disable_dns_seeding: false,
Expand Down
3 changes: 2 additions & 1 deletion mining/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ parking_lot.workspace = true
rand.workspace = true
serde.workspace = true
smallvec.workspace = true
sweep-bptree = "0.4.1"
thiserror.workspace = true
tokio = { workspace = true, features = [ "rt-multi-thread", "macros", "signal" ] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] }

[dev-dependencies]
kaspa-txscript.workspace = true
Expand Down
222 changes: 219 additions & 3 deletions mining/benches/bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use kaspa_mining::model::topological_index::TopologicalIndex;
use std::collections::{hash_set::Iter, HashMap, HashSet};
use itertools::Itertools;
use kaspa_consensus_core::{
subnets::SUBNETWORK_ID_NATIVE,
tx::{Transaction, TransactionInput, TransactionOutpoint},
};
use kaspa_hashes::{HasherBase, TransactionID};
use kaspa_mining::{model::topological_index::TopologicalIndex, FeerateTransactionKey, Frontier, Policy};
use rand::{thread_rng, Rng};
use std::{
collections::{hash_set::Iter, HashMap, HashSet},
sync::Arc,
};

#[derive(Default)]
pub struct Dag<T>
Expand Down Expand Up @@ -68,5 +78,211 @@ pub fn bench_compare_topological_index_fns(c: &mut Criterion) {
group.finish();
}

criterion_group!(benches, bench_compare_topological_index_fns);
fn generate_unique_tx(i: u64) -> Arc<Transaction> {
let mut hasher = TransactionID::new();
let prev = hasher.update(i.to_le_bytes()).clone().finalize();
let input = TransactionInput::new(TransactionOutpoint::new(prev, 0), vec![], 0, 0);
Arc::new(Transaction::new(0, vec![input], vec![], 0, SUBNETWORK_ID_NATIVE, 0, vec![]))
}

fn build_feerate_key(fee: u64, mass: u64, id: u64) -> FeerateTransactionKey {
FeerateTransactionKey::new(fee, mass, generate_unique_tx(id))
}

pub fn bench_mempool_sampling(c: &mut Criterion) {
let mut rng = thread_rng();
let mut group = c.benchmark_group("mempool sampling");
let cap = 1_000_000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let fee: u64 = if i % (cap as u64 / 100000) == 0 { 1000000 } else { rng.gen_range(1..10000) };
let mass: u64 = 1650;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}

let len = cap;
let mut frontier = Frontier::default();
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}
group.bench_function("mempool one-shot sample", |b| {
b.iter(|| {
black_box({
let selected = frontier.sample_inplace(&mut rng, &Policy::new(500_000), &mut 0);
selected.iter().map(|k| k.mass).sum::<u64>()
})
})
});

// Benchmark frontier insertions and removals (see comparisons below)
let remove = map.values().take(map.len() / 10).cloned().collect_vec();
group.bench_function("frontier remove/add", |b| {
b.iter(|| {
black_box({
for r in remove.iter() {
frontier.remove(r).then_some(()).unwrap();
}
for r in remove.iter().cloned() {
frontier.insert(r).then_some(()).unwrap();
}
0
})
})
});

// Benchmark hashmap insertions and removals for comparison
let remove = map.iter().take(map.len() / 10).map(|(&k, v)| (k, v.clone())).collect_vec();
group.bench_function("map remove/add", |b| {
b.iter(|| {
black_box({
for r in remove.iter() {
map.remove(&r.0).unwrap();
}
for r in remove.iter().cloned() {
map.insert(r.0, r.1.clone());
}
0
})
})
});

// Benchmark std btree set insertions and removals for comparison
// Results show that frontier (sweep bptree) and std btree set are roughly the same.
// The slightly higher cost for sweep bptree should be attributed to subtree weight
// maintenance (see FeerateWeight)
#[allow(clippy::mutable_key_type)]
let mut std_btree = std::collections::BTreeSet::from_iter(map.values().cloned());
let remove = map.iter().take(map.len() / 10).map(|(&k, v)| (k, v.clone())).collect_vec();
group.bench_function("std btree remove/add", |b| {
b.iter(|| {
black_box({
for (_, key) in remove.iter() {
std_btree.remove(key).then_some(()).unwrap();
}
for (_, key) in remove.iter() {
std_btree.insert(key.clone());
}
0
})
})
});
group.finish();
}

pub fn bench_mempool_selectors(c: &mut Criterion) {
let mut rng = thread_rng();
let mut group = c.benchmark_group("mempool selectors");
let cap = 1_000_000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let fee: u64 = rng.gen_range(1..1000000);
let mass: u64 = 1650;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}

for len in [100, 300, 350, 500, 1000, 2000, 5000, 10_000, 100_000, 500_000, 1_000_000].into_iter().rev() {
let mut frontier = Frontier::default();
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}

group.bench_function(format!("rebalancing selector ({})", len), |b| {
b.iter(|| {
black_box({
let mut selector = frontier.build_rebalancing_selector();
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>()
})
})
});

let mut collisions = 0;
let mut n = 0;

group.bench_function(format!("sample inplace selector ({})", len), |b| {
b.iter(|| {
black_box({
let mut selector = frontier.build_selector_sample_inplace(&mut collisions);
n += 1;
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>()
})
})
});

if n > 0 {
println!("---------------------- \n Avg collisions: {}", collisions / n);
}

if frontier.total_mass() <= 500_000 {
group.bench_function(format!("take all selector ({})", len), |b| {
b.iter(|| {
black_box({
let mut selector = frontier.build_selector_take_all();
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>()
})
})
});
}

group.bench_function(format!("dynamic selector ({})", len), |b| {
b.iter(|| {
black_box({
let mut selector = frontier.build_selector(&Policy::new(500_000));
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>()
})
})
});
}

group.finish();
}

pub fn bench_inplace_sampling_worst_case(c: &mut Criterion) {
let mut group = c.benchmark_group("mempool inplace sampling");
let max_fee = u64::MAX;
let fee_steps = (0..10).map(|i| max_fee / 100u64.pow(i)).collect_vec();
for subgroup_size in [300, 200, 100, 80, 50, 30] {
let cap = 1_000_000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let fee: u64 = if i < 300 { fee_steps[i as usize / subgroup_size] } else { 1 };
let mass: u64 = 1650;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}

let mut frontier = Frontier::default();
for item in map.values().cloned() {
frontier.insert(item).then_some(()).unwrap();
}

let mut collisions = 0;
let mut n = 0;

group.bench_function(format!("inplace sampling worst case (subgroup size: {})", subgroup_size), |b| {
b.iter(|| {
black_box({
let mut selector = frontier.build_selector_sample_inplace(&mut collisions);
n += 1;
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>()
})
})
});

if n > 0 {
println!("---------------------- \n Avg collisions: {}", collisions / n);
}
}

group.finish();
}

criterion_group!(
benches,
bench_mempool_sampling,
bench_mempool_selectors,
bench_inplace_sampling_worst_case,
bench_compare_topological_index_fns
);
criterion_main!(benches);
23 changes: 7 additions & 16 deletions mining/src/block_template/builder.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
use super::{errors::BuilderResult, policy::Policy};
use crate::{block_template::selector::TransactionsSelector, model::candidate_tx::CandidateTransaction};
use super::errors::BuilderResult;
use kaspa_consensus_core::{
api::ConsensusApi,
block::{BlockTemplate, TemplateBuildMode},
block::{BlockTemplate, TemplateBuildMode, TemplateTransactionSelector},
coinbase::MinerData,
merkle::calc_hash_merkle_root,
tx::COINBASE_TRANSACTION_INDEX,
};
use kaspa_core::{
debug,
time::{unix_now, Stopwatch},
};
use kaspa_core::time::{unix_now, Stopwatch};

pub(crate) struct BlockTemplateBuilder {
policy: Policy,
}
pub(crate) struct BlockTemplateBuilder {}

impl BlockTemplateBuilder {
pub(crate) fn new(max_block_mass: u64) -> Self {
let policy = Policy::new(max_block_mass);
Self { policy }
pub(crate) fn new() -> Self {
Self {}
}

/// BuildBlockTemplate creates a block template for a miner to consume
Expand Down Expand Up @@ -89,12 +82,10 @@ impl BlockTemplateBuilder {
&self,
consensus: &dyn ConsensusApi,
miner_data: &MinerData,
transactions: Vec<CandidateTransaction>,
selector: Box<dyn TemplateTransactionSelector>,
build_mode: TemplateBuildMode,
) -> BuilderResult<BlockTemplate> {
let _sw = Stopwatch::<20>::with_threshold("build_block_template op");
debug!("Considering {} transactions for a new block template", transactions.len());
let selector = Box::new(TransactionsSelector::new(self.policy.clone(), transactions));
Ok(consensus.build_block_template(miner_data.clone(), selector, build_mode)?)
}

Expand Down
6 changes: 3 additions & 3 deletions mining/src/block_template/policy.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/// Policy houses the policy (configuration parameters) which is used to control
/// the generation of block templates. See the documentation for
/// NewBlockTemplate for more details on each of these parameters are used.
/// NewBlockTemplate for more details on how each of these parameters are used.
#[derive(Clone)]
pub(crate) struct Policy {
pub struct Policy {
/// max_block_mass is the maximum block mass to be used when generating a block template.
pub(crate) max_block_mass: u64,
}

impl Policy {
pub(crate) fn new(max_block_mass: u64) -> Self {
pub fn new(max_block_mass: u64) -> Self {
Self { max_block_mass }
}
}
Loading