Skip to content

Commit 5414667

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 cddc5ec commit 5414667

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

Cargo.toml

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

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

benches/operations.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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,
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+
#[derive(Clone, Copy)]
28+
struct StoreBenchConfig {
29+
name: &'static str,
30+
store_type: TestStoreType,
31+
}
32+
33+
fn operations_benchmark(c: &mut Criterion) {
34+
dotenvy::dotenv().ok();
35+
36+
forwarding_benchmark(c);
37+
}
38+
39+
fn forwarding_benchmark(c: &mut Criterion) {
40+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
41+
let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind);
42+
let runtime = benchmark_runtime();
43+
44+
let mut group = c.benchmark_group("forwarding");
45+
group.sample_size(10);
46+
47+
for store_config in store_bench_configs() {
48+
if !should_register_bench("forwarding", store_config.name) {
49+
continue;
50+
}
51+
let nodes = setup_forwarding_nodes(
52+
&chain_source,
53+
&bitcoind,
54+
&electrsd,
55+
store_config.store_type,
56+
&runtime,
57+
);
58+
let nodes = Arc::new(nodes);
59+
60+
group.bench_function(store_config.name, |b| {
61+
b.to_async(&runtime).iter_custom(|iter| {
62+
let nodes = Arc::clone(&nodes);
63+
64+
async move {
65+
let mut total = Duration::ZERO;
66+
for _ in 0..iter {
67+
total += send_forwarded_payments(Arc::clone(&nodes)).await;
68+
}
69+
total
70+
}
71+
});
72+
});
73+
}
74+
}
75+
76+
fn benchmark_runtime() -> tokio::runtime::Runtime {
77+
let mut builder = tokio::runtime::Builder::new_multi_thread();
78+
builder.worker_threads(4).enable_all();
79+
#[cfg(tokio_unstable)]
80+
builder.enable_eager_driver_handoff();
81+
builder.build().unwrap()
82+
}
83+
84+
/// Returns whether the benchmark identified by `group/name` matches the CLI filters.
85+
///
86+
/// Criterion applies its own filters after benchmark registration, but these benches do expensive
87+
/// setup before registration. Pre-filtering here avoids setting up benchmark cases that cannot run.
88+
/// Only non-flag arguments are considered filters, matching either the full target substring or the
89+
/// group name.
90+
fn should_register_bench(group: &str, name: &str) -> bool {
91+
let target = format!("{}/{}", group, name);
92+
let filters: Vec<String> =
93+
std::env::args().skip(1).filter(|arg| !arg.starts_with('-')).collect();
94+
filters.is_empty()
95+
|| filters.iter().any(|filter| {
96+
target.contains(filter) || (filter == group && target.starts_with(&format!("{group}/")))
97+
})
98+
}
99+
100+
fn setup_forwarding_nodes(
101+
chain_source: &TestChainSource, bitcoind: &BitcoinD, electrsd: &electrsd::ElectrsD,
102+
store_type: TestStoreType, runtime: &tokio::runtime::Runtime,
103+
) -> Vec<Arc<Node>> {
104+
let mut nodes = Vec::new();
105+
for _ in 0..3 {
106+
let mut config = random_config(true);
107+
config.store_type = store_type;
108+
nodes.push(Arc::new(setup_node(chain_source, config)));
109+
}
110+
111+
runtime.block_on(async {
112+
let addresses =
113+
nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect();
114+
premine_and_distribute_funds(
115+
&bitcoind.client,
116+
&electrsd.client,
117+
addresses,
118+
Amount::from_sat(5_000_000),
119+
)
120+
.await;
121+
for node in &nodes {
122+
node.sync_wallets().unwrap();
123+
}
124+
125+
let funding_amount_sat = 1_000_000;
126+
let push_amount_msat = None;
127+
open_channel_push_amt(
128+
&nodes[0],
129+
&nodes[1],
130+
funding_amount_sat,
131+
push_amount_msat,
132+
true,
133+
electrsd,
134+
)
135+
.await;
136+
open_channel_push_amt(
137+
&nodes[1],
138+
&nodes[2],
139+
funding_amount_sat,
140+
push_amount_msat,
141+
true,
142+
electrsd,
143+
)
144+
.await;
145+
146+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
147+
for node in &nodes {
148+
node.sync_wallets().unwrap();
149+
}
150+
151+
expect_event!(nodes[0], ChannelReady);
152+
expect_event!(nodes[1], ChannelReady);
153+
expect_event!(nodes[1], ChannelReady);
154+
expect_event!(nodes[2], ChannelReady);
155+
156+
tokio::time::sleep(Duration::from_secs(1)).await;
157+
wait_for_forwarding_path(&nodes).await;
158+
});
159+
160+
nodes
161+
}
162+
163+
async fn send_forwarded_payments(nodes: Arc<Vec<Arc<Node>>>) -> Duration {
164+
let total_payments = 25;
165+
let amount_msat = 5_000;
166+
167+
let mut total = Duration::ZERO;
168+
169+
for _ in 0..total_payments {
170+
let invoice_description =
171+
Bolt11InvoiceDescription::Direct(Description::new("forwarding".to_string()).unwrap());
172+
let invoice = nodes[2]
173+
.bolt11_payment()
174+
.receive(amount_msat, &invoice_description.into(), 9217)
175+
.unwrap();
176+
177+
let start = Instant::now();
178+
let payment_id = nodes[0].bolt11_payment().send(&invoice, None).unwrap();
179+
total += wait_for_forwarded_payment(&nodes, payment_id, start).await;
180+
}
181+
182+
// return funds and clean up for next run
183+
let invoice_description =
184+
Bolt11InvoiceDescription::Direct(Description::new("return".to_string()).unwrap());
185+
let invoice = nodes[0]
186+
.bolt11_payment()
187+
.receive(amount_msat * total_payments, &invoice_description.into(), 9217)
188+
.unwrap();
189+
if let Ok(return_payment_id) = nodes[2].bolt11_payment().send(&invoice, None) {
190+
wait_for_payment_success(&nodes[2], return_payment_id).await
191+
}
192+
tokio::time::sleep(Duration::from_millis(10)).await;
193+
for node in nodes.iter() {
194+
drain_events(node);
195+
}
196+
197+
total
198+
}
199+
200+
async fn wait_for_forwarded_payment(
201+
nodes: &[Arc<Node>], expected_payment_id: PaymentId, start: Instant,
202+
) -> Duration {
203+
let mut payment_successful = false;
204+
let mut payment_forwarded = false;
205+
206+
while !payment_successful || !payment_forwarded {
207+
tokio::select! {
208+
event = nodes[0].next_event_async(), if !payment_successful => {
209+
match event {
210+
Event::PaymentSuccessful { payment_id: Some(payment_id), .. }
211+
if payment_id == expected_payment_id =>
212+
{
213+
payment_successful = true;
214+
},
215+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
216+
nodes[0].event_handled().unwrap();
217+
panic!("Forwarded payment {payment_id:?} failed with hash {payment_hash:?}");
218+
},
219+
_ => {},
220+
}
221+
nodes[0].event_handled().unwrap();
222+
},
223+
event = nodes[1].next_event_async(), if !payment_forwarded => {
224+
if matches!(event, Event::PaymentForwarded { .. }) {
225+
payment_forwarded = true;
226+
}
227+
nodes[1].event_handled().unwrap();
228+
},
229+
}
230+
}
231+
232+
start.elapsed()
233+
}
234+
235+
/// Sends a payment across the benchmark path before measurements start.
236+
///
237+
/// Channel readiness events alone do not guarantee that the sender can immediately find and use the
238+
/// intended multi-hop path. Waiting for one successful payment keeps route-discovery first-use cost
239+
/// and transient graph propagation failures out of the timed forwarding loop.
240+
async fn wait_for_forwarding_path(nodes: &[Arc<Node>]) {
241+
for _ in 0..30 {
242+
let invoice_description =
243+
Bolt11InvoiceDescription::Direct(Description::new("".to_string()).unwrap());
244+
let invoice =
245+
nodes[2].bolt11_payment().receive(5_000, &invoice_description.into(), 9217).unwrap();
246+
if let Ok(payment_id) = nodes[0].bolt11_payment().send(&invoice, None) {
247+
wait_for_payment_success(&nodes[0], payment_id).await;
248+
tokio::time::sleep(Duration::from_millis(50)).await;
249+
for node in nodes {
250+
drain_events(node);
251+
}
252+
return;
253+
}
254+
tokio::time::sleep(Duration::from_secs(1)).await;
255+
}
256+
257+
panic!("Timed out waiting for forwarding path readiness");
258+
}
259+
260+
async fn wait_for_payment_success(node: &Node, expected_payment_id: PaymentId) {
261+
loop {
262+
match node.next_event_async().await {
263+
Event::PaymentSuccessful { payment_id: Some(payment_id), .. }
264+
if payment_id == expected_payment_id =>
265+
{
266+
node.event_handled().unwrap();
267+
break;
268+
},
269+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
270+
node.event_handled().unwrap();
271+
panic!("Return payment {:?} failed with hash {:?}", payment_id, payment_hash);
272+
},
273+
_ => node.event_handled().unwrap(),
274+
}
275+
}
276+
}
277+
278+
fn drain_events(node: &Node) {
279+
while node.next_event().is_some() {
280+
node.event_handled().unwrap();
281+
}
282+
}
283+
284+
fn store_bench_configs() -> Vec<StoreBenchConfig> {
285+
#[cfg(not(feature = "postgres"))]
286+
{
287+
vec![
288+
StoreBenchConfig { name: "sqlite", store_type: TestStoreType::Sqlite },
289+
StoreBenchConfig { name: "filesystem", store_type: TestStoreType::FilesystemStore },
290+
]
291+
}
292+
293+
#[cfg(feature = "postgres")]
294+
{
295+
vec![
296+
StoreBenchConfig { name: "sqlite", store_type: TestStoreType::Sqlite },
297+
StoreBenchConfig { name: "filesystem", store_type: TestStoreType::FilesystemStore },
298+
StoreBenchConfig { name: "postgres", store_type: TestStoreType::Postgres },
299+
]
300+
}
301+
}
302+
303+
criterion_group!(benches, operations_benchmark);
304+
criterion_main!(benches);

0 commit comments

Comments
 (0)