Skip to content

Commit 970a395

Browse files
benthecarmanclaude
andcommitted
Add per-channel forwarding statistics
Track aggregate stats (fees earned, payment counts, amounts) per channel. Add ForwardedPaymentTrackingMode config: Stats (default) for lightweight metrics only, or Detailed to also store individual payment records. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 06aee72 commit 970a395

File tree

9 files changed

+382
-45
lines changed

9 files changed

+382
-45
lines changed

src/builder.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ use crate::fee_estimator::OnchainFeeEstimator;
5555
use crate::gossip::GossipSource;
5656
use crate::io::sqlite_store::SqliteStore;
5757
use crate::io::utils::{
58-
read_event_queue, read_external_pathfinding_scores_from_cache, read_forwarded_payments,
59-
read_network_graph, read_node_metrics, read_output_sweeper, read_payments, read_peer_info,
60-
read_pending_payments, read_scorer, write_node_metrics,
58+
read_channel_forwarding_stats, read_event_queue, read_external_pathfinding_scores_from_cache,
59+
read_forwarded_payments, read_network_graph, read_node_metrics, read_output_sweeper,
60+
read_payments, read_peer_info, read_pending_payments, read_scorer, write_node_metrics,
6161
};
6262
use crate::io::vss_store::VssStoreBuilder;
6363
use crate::io::{
64-
self, FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
64+
self, CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
65+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
66+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6567
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6668
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6769
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
@@ -77,9 +79,10 @@ use crate::peer_store::PeerStore;
7779
use crate::runtime::{Runtime, RuntimeSpawner};
7880
use crate::tx_broadcaster::TransactionBroadcaster;
7981
use crate::types::{
80-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, ForwardedPaymentStore,
81-
GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager,
82-
PendingPaymentStore, Persister, SyncAndAsyncKVStore,
82+
AsyncPersister, ChainMonitor, ChannelForwardingStatsStore, ChannelManager, DynStore,
83+
DynStoreWrapper, ForwardedPaymentStore, GossipSync, Graph, KeysManager, MessageRouter,
84+
OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore, Persister,
85+
SyncAndAsyncKVStore,
8386
};
8487
use crate::wallet::persist::KVStoreWalletPersister;
8588
use crate::wallet::Wallet;
@@ -1065,12 +1068,14 @@ fn build_with_store_internal(
10651068
let (
10661069
payment_store_res,
10671070
forwarded_payment_store_res,
1071+
channel_forwarding_stats_res,
10681072
node_metris_res,
10691073
pending_payment_store_res,
10701074
) = runtime.block_on(async move {
10711075
tokio::join!(
10721076
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
10731077
read_forwarded_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1078+
read_channel_forwarding_stats(&*kv_store_ref, Arc::clone(&logger_ref)),
10741079
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
10751080
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
10761081
)
@@ -1117,6 +1122,20 @@ fn build_with_store_internal(
11171122
},
11181123
};
11191124

1125+
let channel_forwarding_stats_store = match channel_forwarding_stats_res {
1126+
Ok(stats) => Arc::new(ChannelForwardingStatsStore::new(
1127+
stats,
1128+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1129+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1130+
Arc::clone(&kv_store),
1131+
Arc::clone(&logger),
1132+
)),
1133+
Err(e) => {
1134+
log_error!(logger, "Failed to read channel forwarding stats from store: {}", e);
1135+
return Err(BuildError::ReadFailed);
1136+
},
1137+
};
1138+
11201139
let (chain_source, chain_tip_opt) = match chain_data_source_config {
11211140
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
11221141
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -1804,6 +1823,7 @@ fn build_with_store_internal(
18041823
peer_store,
18051824
payment_store,
18061825
forwarded_payment_store,
1826+
channel_forwarding_stats_store,
18071827
is_running,
18081828
node_metrics,
18091829
om_mailbox,

src/config.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ use lightning::util::config::{
2121

2222
use crate::logger::LogLevel;
2323

24+
/// The mode used for tracking forwarded payments.
25+
///
26+
/// This determines how much detail is stored about payment forwarding activity.
27+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28+
pub enum ForwardedPaymentTrackingMode {
29+
/// Store every individual forwarded payment AND track per-channel aggregate statistics.
30+
///
31+
/// Use this when you need full history of forwarded payments for accounting, debugging,
32+
/// or detailed analytics.
33+
Detailed,
34+
/// Track only per-channel aggregate statistics without storing individual payment records.
35+
///
36+
/// This is the default mode. Use this to reduce storage requirements when you only need
37+
/// aggregate metrics like total fees earned per channel.
38+
#[default]
39+
Stats,
40+
}
41+
2442
// Config defaults
2543
const DEFAULT_NETWORK: Network = Network::Bitcoin;
2644
const DEFAULT_BDK_WALLET_SYNC_INTERVAL_SECS: u64 = 80;
@@ -127,9 +145,10 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5;
127145
/// | `probing_liquidity_limit_multiplier` | 3 |
128146
/// | `log_level` | Debug |
129147
/// | `anchor_channels_config` | Some(..) |
130-
/// | `route_parameters` | None |
148+
/// | `route_parameters` | None |
149+
/// | `forwarded_payment_tracking_mode` | Detailed |
131150
///
132-
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
151+
/// See [`AnchorChannelsConfig`], [`RouteParametersConfig`], and [`ForwardedPaymentTrackingMode`] for more information regarding their
133152
/// respective default values.
134153
///
135154
/// [`Node`]: crate::Node
@@ -192,6 +211,10 @@ pub struct Config {
192211
/// **Note:** If unset, default parameters will be used, and you will be able to override the
193212
/// parameters on a per-payment basis in the corresponding method calls.
194213
pub route_parameters: Option<RouteParametersConfig>,
214+
/// The mode used for tracking forwarded payments.
215+
///
216+
/// See [`ForwardedPaymentTrackingMode`] for more information on the available modes.
217+
pub forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode,
195218
}
196219

197220
impl Default for Config {
@@ -206,6 +229,7 @@ impl Default for Config {
206229
anchor_channels_config: Some(AnchorChannelsConfig::default()),
207230
route_parameters: None,
208231
node_alias: None,
232+
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
209233
}
210234
}
211235
}

src/event.rs

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use lightning_liquidity::lsps2::utils::compute_opening_fee;
3333
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3434
use rand::{rng, Rng};
3535

36-
use crate::config::{may_announce_channel, Config};
36+
use crate::config::{may_announce_channel, Config, ForwardedPaymentTrackingMode};
3737
use crate::connection::ConnectionManager;
3838
use crate::data_store::DataStoreUpdateResult;
3939
use crate::fee_estimator::ConfirmationTarget;
@@ -46,12 +46,13 @@ use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger
4646
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
4747
use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
4848
use crate::payment::store::{
49-
ForwardedPaymentDetails, ForwardedPaymentId, PaymentDetails, PaymentDetailsUpdate,
50-
PaymentDirection, PaymentKind, PaymentStatus,
49+
ChannelForwardingStats, ForwardedPaymentDetails, ForwardedPaymentId, PaymentDetails,
50+
PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
5151
};
5252
use crate::runtime::Runtime;
5353
use crate::types::{
54-
CustomTlvRecord, DynStore, ForwardedPaymentStore, OnionMessenger, PaymentStore, Sweeper, Wallet,
54+
ChannelForwardingStatsStore, CustomTlvRecord, DynStore, ForwardedPaymentStore, OnionMessenger,
55+
PaymentStore, Sweeper, Wallet,
5556
};
5657
use crate::{
5758
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
@@ -492,6 +493,7 @@ where
492493
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
493494
payment_store: Arc<PaymentStore>,
494495
forwarded_payment_store: Arc<ForwardedPaymentStore>,
496+
channel_forwarding_stats_store: Arc<ChannelForwardingStatsStore>,
495497
peer_store: Arc<PeerStore<L>>,
496498
runtime: Arc<Runtime>,
497499
logger: L,
@@ -512,6 +514,7 @@ where
512514
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
513515
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
514516
payment_store: Arc<PaymentStore>, forwarded_payment_store: Arc<ForwardedPaymentStore>,
517+
channel_forwarding_stats_store: Arc<ChannelForwardingStatsStore>,
515518
peer_store: Arc<PeerStore<L>>, static_invoice_store: Option<StaticInvoiceStore>,
516519
onion_messenger: Arc<OnionMessenger>, om_mailbox: Option<Arc<OnionMessageMailbox>>,
517520
runtime: Arc<Runtime>, logger: L, config: Arc<Config>,
@@ -527,6 +530,7 @@ where
527530
liquidity_source,
528531
payment_store,
529532
forwarded_payment_store,
533+
channel_forwarding_stats_store,
530534
peer_store,
531535
logger,
532536
runtime,
@@ -1370,40 +1374,99 @@ where
13701374
.await;
13711375
}
13721376

1373-
// Store the forwarded payment details
13741377
let prev_channel_id_value = prev_channel_id
13751378
.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107.");
13761379
let next_channel_id_value = next_channel_id
13771380
.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107.");
13781381

1379-
// PaymentForwarded does not have a unique id, so we generate a random one here.
1380-
let mut id_bytes = [0u8; 32];
1381-
rng().fill(&mut id_bytes);
1382-
13831382
let forwarded_at_timestamp = SystemTime::now()
13841383
.duration_since(UNIX_EPOCH)
13851384
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH")
13861385
.as_secs();
13871386

1388-
let forwarded_payment = ForwardedPaymentDetails {
1389-
id: ForwardedPaymentId(id_bytes),
1390-
prev_channel_id: prev_channel_id_value,
1391-
next_channel_id: next_channel_id_value,
1392-
prev_user_channel_id: prev_user_channel_id.map(UserChannelId),
1393-
next_user_channel_id: next_user_channel_id.map(UserChannelId),
1394-
prev_node_id,
1395-
next_node_id,
1396-
total_fee_earned_msat,
1397-
skimmed_fee_msat,
1398-
claim_from_onchain_tx,
1399-
outbound_amount_forwarded_msat,
1400-
forwarded_at_timestamp,
1387+
// Calculate inbound amount (outbound + fee)
1388+
let inbound_amount_msat = outbound_amount_forwarded_msat
1389+
.unwrap_or(0)
1390+
.saturating_add(total_fee_earned_msat.unwrap_or(0));
1391+
1392+
// Update per-channel forwarding stats for the inbound channel (prev_channel)
1393+
// For new entries, this becomes the initial value; for existing entries,
1394+
// these values are used as increments via the to_update() -> update() pattern.
1395+
let inbound_stats = ChannelForwardingStats {
1396+
channel_id: prev_channel_id_value,
1397+
counterparty_node_id: prev_node_id,
1398+
inbound_payments_forwarded: 1,
1399+
outbound_payments_forwarded: 0,
1400+
total_inbound_amount_msat: inbound_amount_msat,
1401+
total_outbound_amount_msat: 0,
1402+
total_fee_earned_msat: total_fee_earned_msat.unwrap_or(0),
1403+
total_skimmed_fee_msat: skimmed_fee_msat.unwrap_or(0),
1404+
onchain_claims_count: if claim_from_onchain_tx { 1 } else { 0 },
1405+
first_forwarded_at_timestamp: forwarded_at_timestamp,
1406+
last_forwarded_at_timestamp: forwarded_at_timestamp,
1407+
};
1408+
self.channel_forwarding_stats_store
1409+
.insert_or_update(inbound_stats)
1410+
.map_err(|e| {
1411+
log_error!(
1412+
self.logger,
1413+
"Failed to update inbound channel forwarding stats: {e}"
1414+
);
1415+
ReplayEvent()
1416+
})?;
1417+
1418+
// Update per-channel forwarding stats for the outbound channel (next_channel)
1419+
let outbound_stats = ChannelForwardingStats {
1420+
channel_id: next_channel_id_value,
1421+
counterparty_node_id: next_node_id,
1422+
inbound_payments_forwarded: 0,
1423+
outbound_payments_forwarded: 1,
1424+
total_inbound_amount_msat: 0,
1425+
total_outbound_amount_msat: outbound_amount_forwarded_msat.unwrap_or(0),
1426+
total_fee_earned_msat: 0,
1427+
total_skimmed_fee_msat: 0,
1428+
onchain_claims_count: 0,
1429+
first_forwarded_at_timestamp: forwarded_at_timestamp,
1430+
last_forwarded_at_timestamp: forwarded_at_timestamp,
14011431
};
1432+
self.channel_forwarding_stats_store
1433+
.insert_or_update(outbound_stats)
1434+
.map_err(|e| {
1435+
log_error!(
1436+
self.logger,
1437+
"Failed to update outbound channel forwarding stats: {e}"
1438+
);
1439+
ReplayEvent()
1440+
})?;
14021441

1403-
self.forwarded_payment_store.insert(forwarded_payment).map_err(|e| {
1404-
log_error!(self.logger, "Failed to store forwarded payment: {e}");
1405-
ReplayEvent()
1406-
})?;
1442+
// Only store individual forwarded payment details in Detailed mode
1443+
if self.config.forwarded_payment_tracking_mode
1444+
== ForwardedPaymentTrackingMode::Detailed
1445+
{
1446+
// PaymentForwarded does not have a unique id, so we generate a random one here.
1447+
let mut id_bytes = [0u8; 32];
1448+
rng().fill(&mut id_bytes);
1449+
1450+
let forwarded_payment = ForwardedPaymentDetails {
1451+
id: ForwardedPaymentId(id_bytes),
1452+
prev_channel_id: prev_channel_id_value,
1453+
next_channel_id: next_channel_id_value,
1454+
prev_user_channel_id: prev_user_channel_id.map(UserChannelId),
1455+
next_user_channel_id: next_user_channel_id.map(UserChannelId),
1456+
prev_node_id,
1457+
next_node_id,
1458+
total_fee_earned_msat,
1459+
skimmed_fee_msat,
1460+
claim_from_onchain_tx,
1461+
outbound_amount_forwarded_msat,
1462+
forwarded_at_timestamp,
1463+
};
1464+
1465+
self.forwarded_payment_store.insert(forwarded_payment).map_err(|e| {
1466+
log_error!(self.logger, "Failed to store forwarded payment: {e}");
1467+
ReplayEvent()
1468+
})?;
1469+
}
14071470

14081471
let event = Event::PaymentForwarded {
14091472
prev_channel_id: prev_channel_id_value,

src/io/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
3131
pub(crate) const FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "forwarded_payments";
3232
pub(crate) const FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
3333

34+
/// The channel forwarding stats will be persisted under this prefix.
35+
pub(crate) const CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE: &str =
36+
"channel_forwarding_stats";
37+
pub(crate) const CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
38+
3439
/// The node metrics will be persisted under this key.
3540
pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = "";
3641
pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = "";

src/io/utils.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ use rand::rngs::OsRng;
3939
use rand::TryRngCore;
4040

4141
use super::*;
42+
use crate::io::{
43+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
44+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
45+
};
4246
use crate::chain::ChainSource;
4347
use crate::config::WALLET_KEYS_SEED_LEN;
4448
use crate::fee_estimator::OnchainFeeEstimator;
4549
use crate::io::{
4650
NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE,
4751
};
4852
use crate::logger::{log_error, LdkLogger, Logger};
49-
use crate::payment::{ForwardedPaymentDetails, PendingPaymentDetails};
53+
use crate::payment::{ChannelForwardingStats, ForwardedPaymentDetails, PendingPaymentDetails};
5054
use crate::peer_store::PeerStore;
5155
use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper};
5256
use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper};
@@ -320,6 +324,22 @@ where
320324
.await
321325
}
322326

327+
/// Read previously persisted channel forwarding stats from the store.
328+
pub(crate) async fn read_channel_forwarding_stats<L: Deref>(
329+
kv_store: &DynStore, logger: L,
330+
) -> Result<Vec<ChannelForwardingStats>, std::io::Error>
331+
where
332+
L::Target: LdkLogger,
333+
{
334+
read_objects_from_store(
335+
kv_store,
336+
logger,
337+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
338+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
339+
)
340+
.await
341+
}
342+
323343
/// Read `OutputSweeper` state from the store.
324344
pub(crate) async fn read_output_sweeper(
325345
broadcaster: Arc<Broadcaster>, fee_estimator: Arc<OnchainFeeEstimator>,

0 commit comments

Comments
 (0)