Skip to content

Commit 22ae4d2

Browse files
authored
Merge pull request #664 from joostjager/payment-benchmark
Payment benchmark
2 parents 8705214 + 14c1cff commit 22ae4d2

File tree

4 files changed

+283
-3
lines changed

4 files changed

+283
-3
lines changed

.github/workflows/benchmarks.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: CI Checks - Benchmarks
2+
3+
on: [push, pull_request]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
benchmark:
11+
runs-on: ubuntu-latest
12+
env:
13+
TOOLCHAIN: stable
14+
steps:
15+
- name: Checkout source code
16+
uses: actions/checkout@v3
17+
- name: Install Rust toolchain
18+
run: |
19+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
20+
rustup override set stable
21+
- name: Enable caching for bitcoind
22+
id: cache-bitcoind
23+
uses: actions/cache@v4
24+
with:
25+
path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
26+
key: bitcoind-${{ runner.os }}-${{ runner.arch }}
27+
- name: Enable caching for electrs
28+
id: cache-electrs
29+
uses: actions/cache@v4
30+
with:
31+
path: bin/electrs-${{ runner.os }}-${{ runner.arch }}
32+
key: electrs-${{ runner.os }}-${{ runner.arch }}
33+
- name: Download bitcoind/electrs
34+
if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')"
35+
run: |
36+
source ./scripts/download_bitcoind_electrs.sh
37+
mkdir bin
38+
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
39+
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
40+
- name: Set bitcoind/electrs environment variables
41+
run: |
42+
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
43+
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
44+
- name: Run benchmarks
45+
run: |
46+
cargo bench

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ lightning = { version = "0.2.0-rc1", features = ["std", "_test_utils"] }
114114
#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] }
115115
proptest = "1.0.0"
116116
regex = "1.5.6"
117+
criterion = { version = "0.7.0", features = ["async_tokio"] }
117118

118119
[target.'cfg(not(no_download))'.dev-dependencies]
119120
electrsd = { version = "0.36.1", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] }
@@ -148,3 +149,7 @@ check-cfg = [
148149
"cfg(cln_test)",
149150
"cfg(lnd_test)",
150151
]
152+
153+
[[bench]]
154+
name = "payments"
155+
harness = false

benches/payments.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#[path = "../tests/common/mod.rs"]
2+
mod common;
3+
4+
use std::time::Instant;
5+
use std::{sync::Arc, time::Duration};
6+
7+
use bitcoin::hex::DisplayHex;
8+
use bitcoin::Amount;
9+
use common::{
10+
expect_channel_ready_event, generate_blocks_and_wait, premine_and_distribute_funds,
11+
setup_bitcoind_and_electrsd, setup_two_nodes_with_store, TestChainSource,
12+
};
13+
use criterion::{criterion_group, criterion_main, Criterion};
14+
use ldk_node::{Event, Node};
15+
use lightning_types::payment::{PaymentHash, PaymentPreimage};
16+
use rand::RngCore;
17+
use tokio::task::{self};
18+
19+
use crate::common::open_channel_push_amt;
20+
21+
fn spawn_payment(node_a: Arc<Node>, node_b: Arc<Node>, amount_msat: u64) {
22+
let mut preimage_bytes = [0u8; 32];
23+
rand::thread_rng().fill_bytes(&mut preimage_bytes);
24+
let preimage = PaymentPreimage(preimage_bytes);
25+
let payment_hash: PaymentHash = preimage.into();
26+
27+
// Spawn each payment as a separate async task
28+
task::spawn(async move {
29+
println!("{}: Starting payment", payment_hash.0.as_hex());
30+
31+
loop {
32+
// Pre-check the HTLC slots to try to avoid the performance impact of a failed payment.
33+
while node_a.list_channels()[0].next_outbound_htlc_limit_msat == 0 {
34+
println!("{}: Waiting for HTLC slots to free up", payment_hash.0.as_hex());
35+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
36+
}
37+
38+
let payment_id = node_a.spontaneous_payment().send_with_preimage(
39+
amount_msat,
40+
node_b.node_id(),
41+
preimage,
42+
None,
43+
);
44+
45+
match payment_id {
46+
Ok(payment_id) => {
47+
println!(
48+
"{}: Awaiting payment with id {}",
49+
payment_hash.0.as_hex(),
50+
payment_id
51+
);
52+
break;
53+
},
54+
Err(e) => {
55+
println!("{}: Payment attempt failed: {:?}", payment_hash.0.as_hex(), e);
56+
57+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
58+
},
59+
}
60+
}
61+
});
62+
}
63+
64+
async fn send_payments(node_a: Arc<Node>, node_b: Arc<Node>) -> std::time::Duration {
65+
let start = Instant::now();
66+
67+
let total_payments = 1000;
68+
let amount_msat = 10_000_000;
69+
70+
let mut success_count = 0;
71+
for _ in 0..total_payments {
72+
spawn_payment(node_a.clone(), node_b.clone(), amount_msat);
73+
}
74+
75+
while success_count < total_payments {
76+
match node_a.next_event_async().await {
77+
Event::PaymentSuccessful { payment_id, payment_hash, .. } => {
78+
if let Some(id) = payment_id {
79+
success_count += 1;
80+
println!("{}: Payment with id {:?} completed", payment_hash.0.as_hex(), id);
81+
} else {
82+
println!("Payment completed (no payment_id)");
83+
}
84+
},
85+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
86+
println!("{}: Payment {:?} failed", payment_hash.unwrap().0.as_hex(), payment_id);
87+
88+
// The payment failed, so we need to respawn it.
89+
spawn_payment(node_a.clone(), node_b.clone(), amount_msat);
90+
},
91+
ref e => {
92+
println!("Received non-payment event: {:?}", e);
93+
},
94+
}
95+
96+
node_a.event_handled().unwrap();
97+
}
98+
99+
let duration = start.elapsed();
100+
println!("Time elapsed: {:?}", duration);
101+
102+
// Send back the money for the next iteration.
103+
let mut preimage_bytes = [0u8; 32];
104+
rand::thread_rng().fill_bytes(&mut preimage_bytes);
105+
node_b
106+
.spontaneous_payment()
107+
.send_with_preimage(
108+
amount_msat * total_payments,
109+
node_a.node_id(),
110+
PaymentPreimage(preimage_bytes),
111+
None,
112+
)
113+
.ok()
114+
.unwrap();
115+
116+
duration
117+
}
118+
119+
fn payment_benchmark(c: &mut Criterion) {
120+
// Set up two nodes. Because this is slow, we reuse the same nodes for each sample.
121+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
122+
let chain_source = TestChainSource::Esplora(&electrsd);
123+
124+
let (node_a, node_b) = setup_two_nodes_with_store(
125+
&chain_source,
126+
false,
127+
true,
128+
false,
129+
common::TestStoreType::Sqlite,
130+
);
131+
132+
let runtime =
133+
tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap();
134+
135+
let node_a = Arc::new(node_a);
136+
let node_b = Arc::new(node_b);
137+
138+
// Fund the nodes and setup a channel between them. The criterion function cannot be async, so we need to execute
139+
// the setup using a runtime.
140+
let node_a_cloned = Arc::clone(&node_a);
141+
let node_b_cloned = Arc::clone(&node_b);
142+
runtime.block_on(async move {
143+
let address_a = node_a_cloned.onchain_payment().new_address().unwrap();
144+
let premine_sat = 25_000_000;
145+
premine_and_distribute_funds(
146+
&bitcoind.client,
147+
&electrsd.client,
148+
vec![address_a],
149+
Amount::from_sat(premine_sat),
150+
)
151+
.await;
152+
node_a_cloned.sync_wallets().unwrap();
153+
node_b_cloned.sync_wallets().unwrap();
154+
open_channel_push_amt(
155+
&node_a_cloned,
156+
&node_b_cloned,
157+
16_000_000,
158+
Some(1_000_000_000),
159+
false,
160+
&electrsd,
161+
)
162+
.await;
163+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
164+
node_a_cloned.sync_wallets().unwrap();
165+
node_b_cloned.sync_wallets().unwrap();
166+
expect_channel_ready_event!(node_a_cloned, node_b_cloned.node_id());
167+
expect_channel_ready_event!(node_b_cloned, node_a_cloned.node_id());
168+
});
169+
170+
let mut group = c.benchmark_group("payments");
171+
group.sample_size(10);
172+
173+
group.bench_function("payments", |b| {
174+
// Use custom timing so that sending back the money at the end of each iteration isn't included in the
175+
// measurement.
176+
b.to_async(&runtime).iter_custom(|iter| {
177+
let node_a = Arc::clone(&node_a);
178+
let node_b = Arc::clone(&node_b);
179+
180+
async move {
181+
let mut total = Duration::ZERO;
182+
for _i in 0..iter {
183+
let node_a = Arc::clone(&node_a);
184+
let node_b = Arc::clone(&node_b);
185+
186+
total += send_payments(node_a, node_b).await;
187+
}
188+
total
189+
}
190+
});
191+
});
192+
}
193+
194+
criterion_group!(benches, payment_benchmark);
195+
criterion_main!(benches);

tests/common/mod.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,23 @@ pub(crate) enum TestChainSource<'a> {
262262
BitcoindRestSync(&'a BitcoinD),
263263
}
264264

265+
#[derive(Clone, Copy)]
266+
pub(crate) enum TestStoreType {
267+
TestSyncStore,
268+
Sqlite,
269+
}
270+
271+
impl Default for TestStoreType {
272+
fn default() -> Self {
273+
TestStoreType::TestSyncStore
274+
}
275+
}
276+
265277
#[derive(Clone, Default)]
266278
pub(crate) struct TestConfig {
267279
pub node_config: Config,
268280
pub log_writer: TestLogWriter,
281+
pub store_type: TestStoreType,
269282
}
270283

271284
macro_rules! setup_builder {
@@ -282,13 +295,28 @@ pub(crate) use setup_builder;
282295
pub(crate) fn setup_two_nodes(
283296
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
284297
anchors_trusted_no_reserve: bool,
298+
) -> (TestNode, TestNode) {
299+
setup_two_nodes_with_store(
300+
chain_source,
301+
allow_0conf,
302+
anchor_channels,
303+
anchors_trusted_no_reserve,
304+
TestStoreType::TestSyncStore,
305+
)
306+
}
307+
308+
pub(crate) fn setup_two_nodes_with_store(
309+
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
310+
anchors_trusted_no_reserve: bool, store_type: TestStoreType,
285311
) -> (TestNode, TestNode) {
286312
println!("== Node A ==");
287-
let config_a = random_config(anchor_channels);
313+
let mut config_a = random_config(anchor_channels);
314+
config_a.store_type = store_type;
288315
let node_a = setup_node(chain_source, config_a, None);
289316

290317
println!("\n== Node B ==");
291318
let mut config_b = random_config(anchor_channels);
319+
config_b.store_type = store_type;
292320
if allow_0conf {
293321
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
294322
}
@@ -381,8 +409,14 @@ pub(crate) fn setup_node_for_async_payments(
381409

382410
builder.set_async_payments_role(async_payments_role).unwrap();
383411

384-
let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into()));
385-
let node = builder.build_with_store(test_sync_store).unwrap();
412+
let node = match config.store_type {
413+
TestStoreType::TestSyncStore => {
414+
let kv_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into()));
415+
builder.build_with_store(kv_store).unwrap()
416+
},
417+
TestStoreType::Sqlite => builder.build().unwrap(),
418+
};
419+
386420
node.start().unwrap();
387421
assert!(node.status().is_running);
388422
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());

0 commit comments

Comments
 (0)