Skip to content

Commit 64444a0

Browse files
committed
Add forwarded payment tracking
Store unambiguous single-HTLC forwarding events. Aggregate them into per-channel and channel-pair statistics. Use fixed one-hour buckets for detailed records. Keep details out of the payment LRU cache. Use one persistence namespace for forwarding data. Expose analytics through Rust and UniFFI. Preserve raw details if an aggregate marker cannot be decoded. AI-assisted-by: OpenAI Codex and Anthropic Fable
1 parent e87f7b2 commit 64444a0

12 files changed

Lines changed: 2172 additions & 105 deletions

File tree

bindings/ldk_node.udl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ typedef dictionary ElectrumSyncConfig;
1111

1212
typedef dictionary TorConfig;
1313

14+
typedef enum ForwardedPaymentTrackingMode;
15+
1416
typedef interface NodeEntropy;
1517

1618
typedef interface ProbingConfig;
@@ -113,6 +115,7 @@ interface Node {
113115
OnchainPayment onchain_payment();
114116
UnifiedPayment unified_payment();
115117
Liquidity liquidity();
118+
ForwardingAnalytics forwarding_analytics();
116119
[Throws=NodeError]
117120
void lnurl_auth(string lnurl);
118121
[Throws=NodeError]
@@ -186,6 +189,8 @@ typedef interface UnifiedPayment;
186189

187190
typedef interface Liquidity;
188191

192+
typedef interface ForwardingAnalytics;
193+
189194
[Error]
190195
enum NodeError {
191196
"AlreadyRunning",
@@ -442,3 +447,15 @@ typedef enum Event;
442447
typedef interface HRNResolverConfig;
443448

444449
typedef dictionary HumanReadableNamesConfig;
450+
451+
typedef dictionary ForwardedPaymentDetails;
452+
453+
typedef dictionary ChannelForwardingStats;
454+
455+
typedef dictionary ChannelPairForwardingStats;
456+
457+
typedef dictionary ForwardedPaymentDetailsPage;
458+
459+
typedef dictionary ChannelForwardingStatsPage;
460+
461+
typedef dictionary ChannelPairForwardingStatsPage;

src/builder.rs

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ use crate::config::{
5454
PAYMENT_CACHE_WARMUP_COUNT,
5555
};
5656
use crate::connection::ConnectionManager;
57-
use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed};
57+
use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed, KeepNoEntries};
5858
use crate::entropy::NodeEntropy;
5959
use crate::event::EventQueue;
6060
use crate::fee_estimator::OnchainFeeEstimator;
@@ -67,7 +67,11 @@ use crate::io::utils::{
6767
};
6868
use crate::io::vss_store::VssStoreBuilder;
6969
use crate::io::{
70-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
70+
self, CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
71+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
72+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
73+
FORWARDED_PAYMENT_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
74+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7175
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
7276
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7377
};
@@ -84,7 +88,8 @@ use crate::probing::{
8488
use crate::runtime::{Runtime, RuntimeSpawner};
8589
use crate::tx_broadcaster::TransactionBroadcaster;
8690
use crate::types::{
87-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
91+
AsyncPersister, ChainMonitor, ChannelForwardingStatsStore, ChannelManager,
92+
ChannelPairForwardingStatsStore, DynStore, DynStoreRef, DynStoreWrapper, ForwardedPaymentStore,
8893
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
8994
PeerManager, PendingPaymentStore,
9095
};
@@ -1457,26 +1462,37 @@ fn build_with_store_internal(
14571462

14581463
let kv_store_ref = Arc::clone(&kv_store);
14591464
let logger_ref = Arc::clone(&logger);
1460-
let (payment_store_res, node_metris_res, pending_payment_store_res, address_pool_res) = runtime
1461-
.block_on(async move {
1462-
tokio::join!(
1463-
read_n_objects(
1464-
&*kv_store_ref,
1465-
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1466-
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1467-
PAYMENT_CACHE_WARMUP_COUNT,
1468-
Arc::clone(&logger_ref),
1469-
),
1470-
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1471-
read_all_objects(
1472-
&*kv_store_ref,
1473-
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1474-
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1475-
Arc::clone(&logger_ref),
1476-
),
1477-
read_address_pool(&*kv_store_ref, &*logger_ref)
1478-
)
1479-
});
1465+
let (
1466+
payment_store_res,
1467+
channel_forwarding_stats_res,
1468+
node_metris_res,
1469+
pending_payment_store_res,
1470+
address_pool_res,
1471+
) = runtime.block_on(async move {
1472+
tokio::join!(
1473+
read_n_objects(
1474+
&*kv_store_ref,
1475+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1476+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1477+
PAYMENT_CACHE_WARMUP_COUNT,
1478+
Arc::clone(&logger_ref),
1479+
),
1480+
read_all_objects(
1481+
&*kv_store_ref,
1482+
FORWARDED_PAYMENT_PERSISTENCE_PRIMARY_NAMESPACE,
1483+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
1484+
Arc::clone(&logger_ref),
1485+
),
1486+
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1487+
read_all_objects(
1488+
&*kv_store_ref,
1489+
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1490+
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1491+
Arc::clone(&logger_ref),
1492+
),
1493+
read_address_pool(&*kv_store_ref, &*logger_ref),
1494+
)
1495+
});
14801496

14811497
// Initialize the status fields.
14821498
let node_metrics = match node_metris_res {
@@ -1509,6 +1525,39 @@ fn build_with_store_internal(
15091525
},
15101526
};
15111527

1528+
let forwarded_payment_store = Arc::new(ForwardedPaymentStore::new(
1529+
Vec::new(),
1530+
KeepNoEntries,
1531+
FORWARDED_PAYMENT_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1532+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1533+
Arc::clone(&kv_store),
1534+
Arc::clone(&logger),
1535+
));
1536+
1537+
let channel_forwarding_stats_store = match channel_forwarding_stats_res {
1538+
Ok(stats) => Arc::new(ChannelForwardingStatsStore::new(
1539+
stats,
1540+
KeepAllEntries,
1541+
FORWARDED_PAYMENT_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1542+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1543+
Arc::clone(&kv_store),
1544+
Arc::clone(&logger),
1545+
)),
1546+
Err(e) => {
1547+
log_error!(logger, "Failed to read channel forwarding stats from store: {}", e);
1548+
return Err(BuildError::ReadFailed);
1549+
},
1550+
};
1551+
1552+
let channel_pair_forwarding_stats_store = Arc::new(ChannelPairForwardingStatsStore::new(
1553+
Vec::new(),
1554+
KeepNoEntries,
1555+
FORWARDED_PAYMENT_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1556+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1557+
Arc::clone(&kv_store),
1558+
Arc::clone(&logger),
1559+
));
1560+
15121561
let (chain_source, chain_tip_opt) = match chain_data_source_config {
15131562
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
15141563
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -2359,6 +2408,16 @@ fn build_with_store_internal(
23592408
_leak_checker.0.push(Arc::downgrade(&wallet) as Weak<dyn Any + Send + Sync>);
23602409
}
23612410

2411+
// How long detail records are kept before being folded into channel-pair buckets. `Stats` keeps
2412+
// none of its own, and only drains records a previous `Detailed` configuration left behind.
2413+
let forwarded_payment_aggregation_retention_secs = match config.forwarded_payment_tracking_mode
2414+
{
2415+
crate::config::ForwardedPaymentTrackingMode::Detailed => {
2416+
crate::payment::forwarding_store::FORWARDED_PAYMENT_AGGREGATION_BUCKET_SIZE_SECS
2417+
},
2418+
crate::config::ForwardedPaymentTrackingMode::Stats => 0,
2419+
};
2420+
23622421
Ok(Node {
23632422
runtime,
23642423
stop_sender,
@@ -2386,6 +2445,10 @@ fn build_with_store_internal(
23862445
scorer,
23872446
peer_store,
23882447
payment_store,
2448+
forwarded_payment_store,
2449+
channel_forwarding_stats_store,
2450+
channel_pair_forwarding_stats_store,
2451+
forwarded_payment_aggregation_retention_secs,
23892452
lnurl_auth,
23902453
is_running,
23912454
node_metrics,

src/config.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY: Duration = Duration::f
169169
// thereafter until every configured LSP has been discovered.
170170
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60);
171171

172+
/// The mode used for tracking forwarded payments.
173+
///
174+
/// In either mode, a forward is tracked only when it has exactly one incoming HTLC and one outgoing
175+
/// HTLC, and LDK reports both the outbound amount and total fee.
176+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177+
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
178+
pub enum ForwardedPaymentTrackingMode {
179+
/// Track eligible new forwarded payments only as per-channel aggregate statistics.
180+
///
181+
/// Any detailed records left by a previous configuration are aggregated and removed after their
182+
/// current one-hour bucket closes.
183+
Stats,
184+
/// Store eligible individual forwarded payments for the current and previous one-hour buckets.
185+
///
186+
/// Payments from older buckets are aggregated into channel-pair statistics and removed.
187+
Detailed,
188+
}
189+
190+
impl Default for ForwardedPaymentTrackingMode {
191+
fn default() -> Self {
192+
Self::Stats
193+
}
194+
}
195+
172196
#[derive(Debug, Clone)]
173197
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
174198
/// Represents the configuration of an [`Node`] instance.
@@ -189,9 +213,10 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_
189213
/// | `tor_config` | None |
190214
/// | `hrn_config` | HumanReadableNamesConfig::default() |
191215
/// | `manually_handle_unknown_bolt11_payments` | false |
216+
/// | `forwarded_payment_tracking_mode` | Stats |
192217
///
193-
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
194-
/// respective default values.
218+
/// See [`AnchorChannelsConfig`], [`RouteParametersConfig`], and
219+
/// [`ForwardedPaymentTrackingMode`] for more information regarding their respective default values.
195220
///
196221
/// [`Node`]: crate::Node
197222
pub struct Config {
@@ -264,6 +289,8 @@ pub struct Config {
264289
///
265290
/// [`Event::PaymentClaimable`]: crate::Event::PaymentClaimable
266291
pub manually_handle_unknown_bolt11_payments: bool,
292+
/// The mode used for tracking forwarded payments.
293+
pub forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode,
267294
}
268295

269296
impl Default for Config {
@@ -281,6 +308,7 @@ impl Default for Config {
281308
node_alias: None,
282309
hrn_config: HumanReadableNamesConfig::default(),
283310
manually_handle_unknown_bolt11_payments: false,
311+
forwarded_payment_tracking_mode: ForwardedPaymentTrackingMode::default(),
284312
}
285313
}
286314
}

0 commit comments

Comments
 (0)