Skip to content

Commit cbb235e

Browse files
benthecarmanclaude
andcommitted
Add forwarded payment tracking and statistics
Routing nodes and LSPs want to track forwarded payments so they can run accounting on fees earned and track profitability across time. We now store these to make it easier to track and allows for future accounting utils in the future. To prevent potential privacy footguns, we only store the individual forwarding events for a given time period, and then we aggregate them into a single event for each channel pair, this prevents potential payment correlation. For lightweight nodes we have the `Stats` mode that only tracks the total forwarding stats per channel. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
1 parent a104e65 commit cbb235e

File tree

11 files changed

+1861
-38
lines changed

11 files changed

+1861
-38
lines changed

bindings/ldk_node.udl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dictionary Config {
1313
u64 probing_liquidity_limit_multiplier;
1414
AnchorChannelsConfig? anchor_channels_config;
1515
RouteParametersConfig? route_parameters;
16+
ForwardedPaymentTrackingMode forwarded_payment_tracking_mode;
1617
};
1718

1819
dictionary AnchorChannelsConfig {
@@ -189,6 +190,12 @@ interface Node {
189190
void remove_payment([ByRef]PaymentId payment_id);
190191
BalanceDetails list_balances();
191192
sequence<PaymentDetails> list_payments();
193+
ForwardedPaymentDetails? forwarded_payment([ByRef]ForwardedPaymentId forwarded_payment_id);
194+
sequence<ForwardedPaymentDetails> list_forwarded_payments();
195+
ForwardedPaymentTrackingMode forwarded_payment_tracking_mode();
196+
ChannelForwardingStats? channel_forwarding_stats([ByRef]ChannelId channel_id);
197+
sequence<ChannelForwardingStats> list_channel_forwarding_stats();
198+
sequence<ChannelPairForwardingStats> list_channel_pair_forwarding_stats();
192199
sequence<PeerDetails> list_peers();
193200
sequence<ChannelDetails> list_channels();
194201
NetworkGraph network_graph();
@@ -486,6 +493,12 @@ enum PaymentStatus {
486493
"Failed",
487494
};
488495

496+
[Enum]
497+
interface ForwardedPaymentTrackingMode {
498+
Detailed(u64 retention_minutes);
499+
Stats();
500+
};
501+
489502
dictionary LSPFeeLimits {
490503
u64? max_total_opening_fee_msat;
491504
u64? max_proportional_opening_fee_ppm_msat;
@@ -507,6 +520,55 @@ dictionary PaymentDetails {
507520
u64 latest_update_timestamp;
508521
};
509522

523+
dictionary ForwardedPaymentDetails {
524+
ForwardedPaymentId id;
525+
ChannelId prev_channel_id;
526+
ChannelId next_channel_id;
527+
UserChannelId? prev_user_channel_id;
528+
UserChannelId? next_user_channel_id;
529+
PublicKey? prev_node_id;
530+
PublicKey? next_node_id;
531+
u64? total_fee_earned_msat;
532+
u64? skimmed_fee_msat;
533+
boolean claim_from_onchain_tx;
534+
u64? outbound_amount_forwarded_msat;
535+
u64 forwarded_at_timestamp;
536+
};
537+
538+
dictionary ChannelForwardingStats {
539+
ChannelId channel_id;
540+
PublicKey? counterparty_node_id;
541+
u64 inbound_payments_forwarded;
542+
u64 outbound_payments_forwarded;
543+
u64 total_inbound_amount_msat;
544+
u64 total_outbound_amount_msat;
545+
u64 total_fee_earned_msat;
546+
u64 total_skimmed_fee_msat;
547+
u64 onchain_claims_count;
548+
u64 first_forwarded_at_timestamp;
549+
u64 last_forwarded_at_timestamp;
550+
};
551+
552+
dictionary ChannelPairForwardingStats {
553+
ChannelPairStatsId id;
554+
ChannelId prev_channel_id;
555+
ChannelId next_channel_id;
556+
u64 bucket_start_timestamp;
557+
PublicKey? prev_node_id;
558+
PublicKey? next_node_id;
559+
u64 payment_count;
560+
u64 total_inbound_amount_msat;
561+
u64 total_outbound_amount_msat;
562+
u64 total_fee_earned_msat;
563+
u64 total_skimmed_fee_msat;
564+
u64 onchain_claims_count;
565+
u64 avg_fee_msat;
566+
u64 avg_inbound_amount_msat;
567+
u64 first_forwarded_at_timestamp;
568+
u64 last_forwarded_at_timestamp;
569+
u64 aggregated_at_timestamp;
570+
};
571+
510572
dictionary RouteParametersConfig {
511573
u64? max_total_routing_fee_msat;
512574
u32 max_total_cltv_expiry_delta;
@@ -894,6 +956,9 @@ typedef string OfferId;
894956
[Custom]
895957
typedef string PaymentId;
896958

959+
[Custom]
960+
typedef string ForwardedPaymentId;
961+
897962
[Custom]
898963
typedef string PaymentHash;
899964

@@ -906,6 +971,9 @@ typedef string PaymentSecret;
906971
[Custom]
907972
typedef string ChannelId;
908973

974+
[Custom]
975+
typedef string ChannelPairStatsId;
976+
909977
[Custom]
910978
typedef string UserChannelId;
911979

src/builder.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,20 @@ 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_network_graph,
58+
read_channel_forwarding_stats, read_channel_pair_forwarding_stats, read_event_queue,
59+
read_external_pathfinding_scores_from_cache, read_forwarded_payments, read_network_graph,
5960
read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments,
6061
read_scorer, write_node_metrics,
6162
};
6263
use crate::io::vss_store::VssStoreBuilder;
6364
use crate::io::{
64-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
65+
self, CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
66+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
67+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
68+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
69+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
70+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
71+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6572
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6673
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6774
};
@@ -75,9 +82,10 @@ use crate::peer_store::PeerStore;
7582
use crate::runtime::{Runtime, RuntimeSpawner};
7683
use crate::tx_broadcaster::TransactionBroadcaster;
7784
use crate::types::{
78-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph,
79-
KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore,
80-
Persister, SyncAndAsyncKVStore,
85+
AsyncPersister, ChainMonitor, ChannelForwardingStatsStore, ChannelManager,
86+
ChannelPairForwardingStatsStore, DynStore, DynStoreWrapper, ForwardedPaymentStore, GossipSync,
87+
Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager,
88+
PendingPaymentStore, Persister, SyncAndAsyncKVStore,
8189
};
8290
use crate::wallet::persist::KVStoreWalletPersister;
8391
use crate::wallet::Wallet;
@@ -1060,14 +1068,23 @@ fn build_with_store_internal(
10601068

10611069
let kv_store_ref = Arc::clone(&kv_store);
10621070
let logger_ref = Arc::clone(&logger);
1063-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1064-
runtime.block_on(async move {
1065-
tokio::join!(
1066-
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1067-
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1068-
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
1069-
)
1070-
});
1071+
let (
1072+
payment_store_res,
1073+
forwarded_payment_store_res,
1074+
channel_forwarding_stats_res,
1075+
channel_pair_forwarding_stats_res,
1076+
node_metris_res,
1077+
pending_payment_store_res,
1078+
) = runtime.block_on(async move {
1079+
tokio::join!(
1080+
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1081+
read_forwarded_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1082+
read_channel_forwarding_stats(&*kv_store_ref, Arc::clone(&logger_ref)),
1083+
read_channel_pair_forwarding_stats(&*kv_store_ref, Arc::clone(&logger_ref)),
1084+
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1085+
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
1086+
)
1087+
});
10711088

10721089
// Initialize the status fields.
10731090
let node_metrics = match node_metris_res {
@@ -1096,6 +1113,48 @@ fn build_with_store_internal(
10961113
},
10971114
};
10981115

1116+
let forwarded_payment_store = match forwarded_payment_store_res {
1117+
Ok(forwarded_payments) => Arc::new(ForwardedPaymentStore::new(
1118+
forwarded_payments,
1119+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1120+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1121+
Arc::clone(&kv_store),
1122+
Arc::clone(&logger),
1123+
)),
1124+
Err(e) => {
1125+
log_error!(logger, "Failed to read forwarded payment data from store: {}", e);
1126+
return Err(BuildError::ReadFailed);
1127+
},
1128+
};
1129+
1130+
let channel_forwarding_stats_store = match channel_forwarding_stats_res {
1131+
Ok(stats) => Arc::new(ChannelForwardingStatsStore::new(
1132+
stats,
1133+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1134+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1135+
Arc::clone(&kv_store),
1136+
Arc::clone(&logger),
1137+
)),
1138+
Err(e) => {
1139+
log_error!(logger, "Failed to read channel forwarding stats from store: {}", e);
1140+
return Err(BuildError::ReadFailed);
1141+
},
1142+
};
1143+
1144+
let channel_pair_forwarding_stats_store = match channel_pair_forwarding_stats_res {
1145+
Ok(stats) => Arc::new(ChannelPairForwardingStatsStore::new(
1146+
stats,
1147+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1148+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1149+
Arc::clone(&kv_store),
1150+
Arc::clone(&logger),
1151+
)),
1152+
Err(e) => {
1153+
log_error!(logger, "Failed to read channel pair forwarding stats from store: {}", e);
1154+
return Err(BuildError::ReadFailed);
1155+
},
1156+
};
1157+
10991158
let (chain_source, chain_tip_opt) = match chain_data_source_config {
11001159
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
11011160
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -1782,6 +1841,9 @@ fn build_with_store_internal(
17821841
scorer,
17831842
peer_store,
17841843
payment_store,
1844+
forwarded_payment_store,
1845+
channel_forwarding_stats_store,
1846+
channel_pair_forwarding_stats_store,
17851847
is_running,
17861848
node_metrics,
17871849
om_mailbox,

src/config.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ 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 individual forwarded payments for the specified retention period (in minutes),
30+
/// then aggregate into channel-pair statistics.
31+
///
32+
/// Individual payments older than `retention_minutes` are aggregated by channel pair
33+
/// and removed. Set to 0 for unlimited retention.
34+
Detailed {
35+
/// The retention period for individual forwarded payment records, in minutes.
36+
/// Individual payments older than this period are aggregated into channel-pair statistics and removed.
37+
/// Set to 0 for unlimited retention.
38+
retention_minutes: u64,
39+
},
40+
/// Track only per-channel aggregate statistics without storing individual payment records.
41+
///
42+
/// This is the default mode. Use this to reduce storage requirements when you only need
43+
/// aggregate metrics like total fees earned per channel.
44+
#[default]
45+
Stats,
46+
}
47+
2448
// Config defaults
2549
const DEFAULT_NETWORK: Network = Network::Bitcoin;
2650
const DEFAULT_BDK_WALLET_SYNC_INTERVAL_SECS: u64 = 80;
@@ -127,9 +151,10 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5;
127151
/// | `probing_liquidity_limit_multiplier` | 3 |
128152
/// | `log_level` | Debug |
129153
/// | `anchor_channels_config` | Some(..) |
130-
/// | `route_parameters` | None |
154+
/// | `route_parameters` | None |
155+
/// | `forwarded_payment_tracking_mode` | Detailed |
131156
///
132-
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
157+
/// See [`AnchorChannelsConfig`], [`RouteParametersConfig`], and [`ForwardedPaymentTrackingMode`] for more information regarding their
133158
/// respective default values.
134159
///
135160
/// [`Node`]: crate::Node
@@ -192,6 +217,10 @@ pub struct Config {
192217
/// **Note:** If unset, default parameters will be used, and you will be able to override the
193218
/// parameters on a per-payment basis in the corresponding method calls.
194219
pub route_parameters: Option<RouteParametersConfig>,
220+
/// The mode used for tracking forwarded payments.
221+
///
222+
/// See [`ForwardedPaymentTrackingMode`] for more information on the available modes.
223+
pub forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode,
195224
}
196225

197226
impl Default for Config {
@@ -206,6 +235,7 @@ impl Default for Config {
206235
anchor_channels_config: Some(AnchorChannelsConfig::default()),
207236
route_parameters: None,
208237
node_alias: None,
238+
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
209239
}
210240
}
211241
}

0 commit comments

Comments
 (0)