Skip to content

Commit 8e9fa64

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 22d7511 commit 8e9fa64

2 files changed

Lines changed: 350 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: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
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::routing::router::RouteParametersConfig;
24+
use lightning_invoice::{Bolt11InvoiceDescription, Description};
25+
26+
use crate::common::{open_channel_push_amt, TestChainSource, TestStoreType};
27+
28+
#[derive(Clone, Copy)]
29+
struct StoreBenchConfig {
30+
name: &'static str,
31+
store_type: TestStoreType,
32+
}
33+
34+
fn operations_benchmark(c: &mut Criterion) {
35+
dotenvy::dotenv().ok();
36+
37+
forwarding_benchmark(c);
38+
}
39+
40+
fn forwarding_benchmark(c: &mut Criterion) {
41+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
42+
let chain_source = TestChainSource::Esplora(&electrsd);
43+
let runtime =
44+
tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap();
45+
46+
let mut group = c.benchmark_group("forwarding");
47+
group.sample_size(10);
48+
49+
for store_config in store_bench_configs() {
50+
if !should_register_bench("forwarding", store_config.name) {
51+
continue;
52+
}
53+
let nodes = setup_forwarding_nodes(
54+
&chain_source,
55+
&bitcoind,
56+
&electrsd,
57+
store_config.store_type,
58+
&runtime,
59+
);
60+
let nodes = Arc::new(nodes);
61+
62+
group.bench_function(store_config.name, |b| {
63+
b.to_async(&runtime).iter_custom(|iter| {
64+
let nodes = Arc::clone(&nodes);
65+
66+
async move {
67+
let mut total = Duration::ZERO;
68+
for _ in 0..iter {
69+
total += send_forwarded_payments(Arc::clone(&nodes)).await;
70+
}
71+
total
72+
}
73+
});
74+
});
75+
}
76+
}
77+
78+
fn should_register_bench(group: &str, name: &str) -> bool {
79+
let target = format!("{}/{}", group, name);
80+
let filters: Vec<String> =
81+
std::env::args().skip(1).filter(|arg| !arg.starts_with('-')).collect();
82+
filters.is_empty()
83+
|| filters.iter().any(|filter| {
84+
target.contains(filter) || (filter == group && target.starts_with(&format!("{group}/")))
85+
})
86+
}
87+
88+
fn setup_forwarding_nodes(
89+
chain_source: &TestChainSource, bitcoind: &BitcoinD, electrsd: &electrsd::ElectrsD,
90+
store_type: TestStoreType, runtime: &tokio::runtime::Runtime,
91+
) -> Vec<Arc<Node>> {
92+
let mut nodes = Vec::new();
93+
for _ in 0..5 {
94+
let mut config = random_config(true);
95+
config.store_type = store_type;
96+
nodes.push(Arc::new(setup_node(chain_source, config)));
97+
}
98+
99+
runtime.block_on(async {
100+
let addresses =
101+
nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect();
102+
premine_and_distribute_funds(
103+
&bitcoind.client,
104+
&electrsd.client,
105+
addresses,
106+
Amount::from_sat(5_000_000),
107+
)
108+
.await;
109+
for node in &nodes {
110+
node.sync_wallets().unwrap();
111+
}
112+
113+
let funding_amount_sat = 1_000_000;
114+
let push_amount_msat = None;
115+
open_channel_push_amt(
116+
&nodes[0],
117+
&nodes[1],
118+
funding_amount_sat,
119+
push_amount_msat,
120+
true,
121+
electrsd,
122+
)
123+
.await;
124+
open_channel_push_amt(
125+
&nodes[1],
126+
&nodes[2],
127+
funding_amount_sat,
128+
push_amount_msat,
129+
true,
130+
electrsd,
131+
)
132+
.await;
133+
nodes[1].sync_wallets().unwrap();
134+
open_channel_push_amt(
135+
&nodes[1],
136+
&nodes[3],
137+
funding_amount_sat,
138+
push_amount_msat,
139+
true,
140+
electrsd,
141+
)
142+
.await;
143+
open_channel_push_amt(
144+
&nodes[2],
145+
&nodes[4],
146+
funding_amount_sat,
147+
push_amount_msat,
148+
true,
149+
electrsd,
150+
)
151+
.await;
152+
open_channel_push_amt(
153+
&nodes[3],
154+
&nodes[4],
155+
funding_amount_sat,
156+
push_amount_msat,
157+
true,
158+
electrsd,
159+
)
160+
.await;
161+
162+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
163+
for node in &nodes {
164+
node.sync_wallets().unwrap();
165+
}
166+
167+
expect_event!(nodes[0], ChannelReady);
168+
expect_event!(nodes[1], ChannelReady);
169+
expect_event!(nodes[1], ChannelReady);
170+
expect_event!(nodes[1], ChannelReady);
171+
expect_event!(nodes[2], ChannelReady);
172+
expect_event!(nodes[2], ChannelReady);
173+
expect_event!(nodes[3], ChannelReady);
174+
expect_event!(nodes[3], ChannelReady);
175+
expect_event!(nodes[4], ChannelReady);
176+
expect_event!(nodes[4], ChannelReady);
177+
178+
tokio::time::sleep(Duration::from_secs(1)).await;
179+
warm_up_forwarding_route(&nodes).await;
180+
});
181+
182+
nodes
183+
}
184+
185+
async fn send_forwarded_payments(nodes: Arc<Vec<Arc<Node>>>) -> Duration {
186+
let start = Instant::now();
187+
188+
let total_payments = 1;
189+
let amount_msat = 2_500_000;
190+
let route_params = route_parameters();
191+
192+
for _ in 0..total_payments {
193+
let invoice_description =
194+
Bolt11InvoiceDescription::Direct(Description::new("forwarding".to_string()).unwrap());
195+
let invoice = nodes[4]
196+
.bolt11_payment()
197+
.receive(amount_msat, &invoice_description.into(), 9217)
198+
.unwrap();
199+
let payment_id =
200+
nodes[0].bolt11_payment().send(&invoice, Some(route_params.clone())).unwrap();
201+
wait_for_forwarded_payment(&nodes, payment_id).await;
202+
}
203+
204+
let duration = start.elapsed();
205+
206+
for _ in 0..total_payments {
207+
let invoice_description =
208+
Bolt11InvoiceDescription::Direct(Description::new("return".to_string()).unwrap());
209+
let invoice = nodes[0]
210+
.bolt11_payment()
211+
.receive(amount_msat - 100_000, &invoice_description.into(), 9217)
212+
.unwrap();
213+
match nodes[4].bolt11_payment().send(&invoice, Some(route_params.clone())) {
214+
Ok(return_payment_id) => wait_for_payment_success(&nodes[4], return_payment_id).await,
215+
Err(_) => break,
216+
}
217+
}
218+
tokio::time::sleep(Duration::from_millis(10)).await;
219+
for node in nodes.iter() {
220+
drain_events(node);
221+
}
222+
223+
duration
224+
}
225+
226+
async fn wait_for_forwarded_payment(nodes: &[Arc<Node>], expected_payment_id: PaymentId) {
227+
let mut payment_successful = false;
228+
let mut first_hop_forwarded = false;
229+
let mut second_hop_forwarded = false;
230+
231+
while !payment_successful || !first_hop_forwarded || !second_hop_forwarded {
232+
tokio::select! {
233+
event = nodes[0].next_event_async(), if !payment_successful => {
234+
match event {
235+
Event::PaymentSuccessful { payment_id: Some(payment_id), .. }
236+
if payment_id == expected_payment_id =>
237+
{
238+
payment_successful = true;
239+
},
240+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
241+
nodes[0].event_handled().unwrap();
242+
panic!("Forwarded payment {:?} failed with hash {:?}", payment_id, payment_hash);
243+
},
244+
_ => {},
245+
}
246+
nodes[0].event_handled().unwrap();
247+
},
248+
event = nodes[1].next_event_async(), if !first_hop_forwarded => {
249+
if matches!(event, Event::PaymentForwarded { .. }) {
250+
first_hop_forwarded = true;
251+
}
252+
nodes[1].event_handled().unwrap();
253+
},
254+
event = nodes[2].next_event_async(), if !second_hop_forwarded => {
255+
if matches!(event, Event::PaymentForwarded { .. }) {
256+
second_hop_forwarded = true;
257+
}
258+
nodes[2].event_handled().unwrap();
259+
},
260+
event = nodes[3].next_event_async(), if !second_hop_forwarded => {
261+
if matches!(event, Event::PaymentForwarded { .. }) {
262+
second_hop_forwarded = true;
263+
}
264+
nodes[3].event_handled().unwrap();
265+
},
266+
}
267+
}
268+
}
269+
270+
async fn warm_up_forwarding_route(nodes: &[Arc<Node>]) {
271+
for _ in 0..30 {
272+
let invoice_description = Bolt11InvoiceDescription::Direct(
273+
Description::new("forwarding warmup".to_string()).unwrap(),
274+
);
275+
let invoice = nodes[4]
276+
.bolt11_payment()
277+
.receive(2_500_000, &invoice_description.into(), 9217)
278+
.unwrap();
279+
if let Ok(payment_id) = nodes[0].bolt11_payment().send(&invoice, Some(route_parameters())) {
280+
wait_for_payment_success(&nodes[0], payment_id).await;
281+
tokio::time::sleep(Duration::from_millis(50)).await;
282+
for node in nodes {
283+
drain_events(node);
284+
}
285+
return;
286+
}
287+
tokio::time::sleep(Duration::from_secs(1)).await;
288+
}
289+
290+
panic!("Timed out warming up forwarding route");
291+
}
292+
293+
fn route_parameters() -> RouteParametersConfig {
294+
RouteParametersConfig {
295+
max_total_routing_fee_msat: Some(75_000),
296+
max_total_cltv_expiry_delta: 1000,
297+
max_path_count: 10,
298+
max_channel_saturation_power_of_half: 2,
299+
}
300+
}
301+
302+
async fn wait_for_payment_success(node: &Node, expected_payment_id: PaymentId) {
303+
loop {
304+
match node.next_event_async().await {
305+
Event::PaymentSuccessful { payment_id: Some(payment_id), .. }
306+
if payment_id == expected_payment_id =>
307+
{
308+
node.event_handled().unwrap();
309+
break;
310+
},
311+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
312+
node.event_handled().unwrap();
313+
panic!("Return payment {:?} failed with hash {:?}", payment_id, payment_hash);
314+
},
315+
_ => node.event_handled().unwrap(),
316+
}
317+
}
318+
}
319+
320+
fn drain_events(node: &Node) {
321+
while node.next_event().is_some() {
322+
node.event_handled().unwrap();
323+
}
324+
}
325+
326+
fn store_bench_configs() -> Vec<StoreBenchConfig> {
327+
#[cfg(not(feature = "postgres"))]
328+
{
329+
vec![
330+
StoreBenchConfig { name: "sqlite", store_type: TestStoreType::Sqlite },
331+
StoreBenchConfig { name: "filesystem", store_type: TestStoreType::FilesystemStore },
332+
]
333+
}
334+
335+
#[cfg(feature = "postgres")]
336+
{
337+
vec![
338+
StoreBenchConfig { name: "sqlite", store_type: TestStoreType::Sqlite },
339+
StoreBenchConfig { name: "filesystem", store_type: TestStoreType::FilesystemStore },
340+
StoreBenchConfig { name: "postgres", store_type: TestStoreType::Postgres },
341+
]
342+
}
343+
}
344+
345+
criterion_group!(benches, operations_benchmark);
346+
criterion_main!(benches);

0 commit comments

Comments
 (0)