Skip to content

Commit 9348e7b

Browse files
committed
Add forwarding operations benchmark
Add an operations bench target with a forwarding benchmark that compares sqlite, filesystem, and postgres stores over a settled multi-hop payment. AI-assisted-by: OpenAI Codex
1 parent 36d53b5 commit 9348e7b

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ check-cfg = [
143143
name = "payments"
144144
harness = false
145145

146+
[[bench]]
147+
name = "operations"
148+
harness = false
149+
146150
[[bench]]
147151
name = "database"
148152
harness = false

benches/operations.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
#[path = "../tests/common/mod.rs"]
9+
mod common;
10+
11+
use std::sync::Arc;
12+
use std::time::{Duration, Instant};
13+
14+
use bitcoin::Amount;
15+
use common::{
16+
expect_event, generate_blocks_and_wait, premine_and_distribute_funds, random_config,
17+
setup_bitcoind_and_electrsd, setup_node, store_bench_configs, wait_for_payment_success,
18+
};
19+
use criterion::{criterion_group, criterion_main, Criterion};
20+
use electrsd::corepc_node::Node as BitcoinD;
21+
use ldk_node::{Event, Node};
22+
use lightning::ln::channelmanager::PaymentId;
23+
use lightning_invoice::{Bolt11InvoiceDescription, Description};
24+
25+
use crate::common::{open_channel_push_amt, TestChainSource, TestStoreType};
26+
27+
fn operations_benchmark(c: &mut Criterion) {
28+
forwarding_benchmark(c);
29+
}
30+
31+
fn forwarding_benchmark(c: &mut Criterion) {
32+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
33+
let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind);
34+
let runtime = benchmark_runtime();
35+
36+
let mut group = c.benchmark_group("forwarding");
37+
group.sample_size(10);
38+
39+
for store_config in store_bench_configs() {
40+
if !should_register_bench("forwarding", store_config.name) {
41+
continue;
42+
}
43+
let nodes = setup_forwarding_nodes(
44+
&chain_source,
45+
&bitcoind,
46+
&electrsd,
47+
store_config.store_type,
48+
&runtime,
49+
);
50+
let nodes = Arc::new(nodes);
51+
52+
group.bench_function(store_config.name, |b| {
53+
b.to_async(&runtime).iter_custom(|iter| {
54+
let nodes = Arc::clone(&nodes);
55+
56+
async move {
57+
let mut total = Duration::ZERO;
58+
for _ in 0..iter {
59+
total += send_forwarded_payments(Arc::clone(&nodes)).await;
60+
}
61+
total
62+
}
63+
});
64+
});
65+
}
66+
}
67+
68+
fn benchmark_runtime() -> tokio::runtime::Runtime {
69+
let mut builder = tokio::runtime::Builder::new_multi_thread();
70+
builder.worker_threads(4).enable_all();
71+
#[cfg(tokio_unstable)]
72+
builder.enable_eager_driver_handoff();
73+
builder.build().unwrap()
74+
}
75+
76+
/// Returns whether the benchmark identified by `group/name` matches the CLI filters.
77+
///
78+
/// Criterion applies its own filters after benchmark registration, but these benches do expensive
79+
/// setup before registration. Pre-filtering here avoids setting up benchmark cases that cannot run.
80+
/// Only non-flag arguments are considered filters, matching either the full target substring or the
81+
/// group name.
82+
fn should_register_bench(group: &str, name: &str) -> bool {
83+
let target = format!("{}/{}", group, name);
84+
let filters: Vec<String> =
85+
std::env::args().skip(1).filter(|arg| !arg.starts_with('-')).collect();
86+
filters.is_empty()
87+
|| filters.iter().any(|filter| {
88+
target.contains(filter) || (filter == group && target.starts_with(&format!("{group}/")))
89+
})
90+
}
91+
92+
fn setup_forwarding_nodes(
93+
chain_source: &TestChainSource, bitcoind: &BitcoinD, electrsd: &electrsd::ElectrsD,
94+
store_type: TestStoreType, runtime: &tokio::runtime::Runtime,
95+
) -> Vec<Arc<Node>> {
96+
let mut nodes = Vec::new();
97+
for _ in 0..3 {
98+
let mut config = random_config(true);
99+
config.store_type = store_type;
100+
nodes.push(Arc::new(setup_node(chain_source, config)));
101+
}
102+
103+
runtime.block_on(async {
104+
let addresses =
105+
nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect();
106+
premine_and_distribute_funds(
107+
&bitcoind.client,
108+
&electrsd.client,
109+
addresses,
110+
Amount::from_sat(5_000_000),
111+
)
112+
.await;
113+
for node in &nodes {
114+
node.sync_wallets().unwrap();
115+
}
116+
117+
let funding_amount_sat = 1_000_000;
118+
let push_amount_msat = Some(funding_amount_sat * 1_000 / 2);
119+
open_channel_push_amt(
120+
&nodes[0],
121+
&nodes[1],
122+
funding_amount_sat,
123+
push_amount_msat,
124+
true,
125+
electrsd,
126+
)
127+
.await;
128+
open_channel_push_amt(
129+
&nodes[1],
130+
&nodes[2],
131+
funding_amount_sat,
132+
push_amount_msat,
133+
true,
134+
electrsd,
135+
)
136+
.await;
137+
138+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
139+
for node in &nodes {
140+
node.sync_wallets().unwrap();
141+
}
142+
143+
expect_event!(nodes[0], ChannelReady);
144+
expect_event!(nodes[1], ChannelReady);
145+
expect_event!(nodes[1], ChannelReady);
146+
expect_event!(nodes[2], ChannelReady);
147+
148+
tokio::time::sleep(Duration::from_secs(1)).await;
149+
wait_for_forwarding_path(&nodes).await;
150+
});
151+
152+
nodes
153+
}
154+
155+
async fn send_forwarded_payments(nodes: Arc<Vec<Arc<Node>>>) -> Duration {
156+
let total_payments = 25;
157+
let amount_msat = 5_000;
158+
159+
let mut total = Duration::ZERO;
160+
161+
for _ in 0..total_payments {
162+
let invoice_description =
163+
Bolt11InvoiceDescription::Direct(Description::new("forwarding".to_string()).unwrap());
164+
let invoice = nodes[2]
165+
.bolt11_payment()
166+
.receive(amount_msat, &invoice_description.into(), 9217)
167+
.unwrap();
168+
169+
let start = Instant::now();
170+
let payment_id = nodes[0].bolt11_payment().send(&invoice, None).unwrap();
171+
total += wait_for_forwarded_payment(&nodes, payment_id, start).await;
172+
}
173+
174+
// return funds and clean up for next run
175+
let invoice_description =
176+
Bolt11InvoiceDescription::Direct(Description::new("return".to_string()).unwrap());
177+
let invoice = nodes[0]
178+
.bolt11_payment()
179+
.receive(amount_msat * total_payments, &invoice_description.into(), 9217)
180+
.unwrap();
181+
let return_payment_id = nodes[2].bolt11_payment().send(&invoice, None).unwrap();
182+
wait_for_payment_success(&nodes[2], return_payment_id).await;
183+
tokio::time::sleep(Duration::from_millis(10)).await;
184+
for node in nodes.iter() {
185+
drain_events(node);
186+
}
187+
188+
total
189+
}
190+
191+
async fn wait_for_forwarded_payment(
192+
nodes: &[Arc<Node>], expected_payment_id: PaymentId, start: Instant,
193+
) -> Duration {
194+
let mut payment_successful = false;
195+
let mut payment_forwarded = false;
196+
197+
while !payment_successful || !payment_forwarded {
198+
tokio::select! {
199+
event = nodes[0].next_event_async(), if !payment_successful => {
200+
match event {
201+
Event::PaymentSuccessful { payment_id: Some(payment_id), .. }
202+
if payment_id == expected_payment_id =>
203+
{
204+
payment_successful = true;
205+
},
206+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
207+
nodes[0].event_handled().unwrap();
208+
panic!("Forwarded payment {payment_id:?} failed with hash {payment_hash:?}");
209+
},
210+
_ => {},
211+
}
212+
nodes[0].event_handled().unwrap();
213+
},
214+
event = nodes[1].next_event_async(), if !payment_forwarded => {
215+
if matches!(event, Event::PaymentForwarded { .. }) {
216+
payment_forwarded = true;
217+
}
218+
nodes[1].event_handled().unwrap();
219+
},
220+
}
221+
}
222+
223+
start.elapsed()
224+
}
225+
226+
/// Sends a payment across the benchmark path before measurements start.
227+
///
228+
/// Channel readiness events alone do not guarantee that the sender can immediately find and use the
229+
/// intended multi-hop path. Waiting for one successful payment keeps route-discovery first-use cost
230+
/// and transient graph propagation failures out of the timed forwarding loop.
231+
async fn wait_for_forwarding_path(nodes: &[Arc<Node>]) {
232+
for _ in 0..30 {
233+
let invoice_description =
234+
Bolt11InvoiceDescription::Direct(Description::new("".to_string()).unwrap());
235+
let invoice =
236+
nodes[2].bolt11_payment().receive(5_000, &invoice_description.into(), 9217).unwrap();
237+
if let Ok(payment_id) = nodes[0].bolt11_payment().send(&invoice, None) {
238+
wait_for_payment_success(&nodes[0], payment_id).await;
239+
tokio::time::sleep(Duration::from_millis(50)).await;
240+
for node in nodes {
241+
drain_events(node);
242+
}
243+
return;
244+
}
245+
tokio::time::sleep(Duration::from_secs(1)).await;
246+
}
247+
248+
panic!("Timed out waiting for forwarding path readiness");
249+
}
250+
251+
fn drain_events(node: &Node) {
252+
while node.next_event().is_some() {
253+
node.event_handled().unwrap();
254+
}
255+
}
256+
257+
criterion_group!(benches, operations_benchmark);
258+
criterion_main!(benches);

0 commit comments

Comments
 (0)