Skip to content

Commit 81249d0

Browse files
committed
Add forwarded payment tracking
Store individual forwarding events and aggregate them into channel and channel-pair statistics for fee and profitability tracking. Infer channel-pair allocations for multi-HTLC forwards by matching incoming and outgoing amounts in FIFO order. AI-assisted-by: OpenAI Codex
1 parent 999db7c commit 81249d0

13 files changed

Lines changed: 2938 additions & 41 deletions

File tree

bindings/ldk_node.udl

Lines changed: 26 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+
Forwarding forwarding();
116119
[Throws=NodeError]
117120
void lnurl_auth(string lnurl);
118121
[Throws=NodeError]
@@ -184,6 +187,8 @@ typedef interface UnifiedPayment;
184187

185188
typedef interface Liquidity;
186189

190+
typedef interface Forwarding;
191+
187192
[Error]
188193
enum NodeError {
189194
"AlreadyRunning",
@@ -394,6 +399,9 @@ typedef string OfferId;
394399
[Custom]
395400
typedef string PaymentId;
396401

402+
[Custom]
403+
typedef string ForwardedPaymentId;
404+
397405
[Custom]
398406
typedef string PaymentHash;
399407

@@ -406,6 +414,12 @@ typedef string PaymentSecret;
406414
[Custom]
407415
typedef string ChannelId;
408416

417+
[Custom]
418+
typedef string ChannelPairStatsId;
419+
420+
[Custom]
421+
typedef string PageToken;
422+
409423
[Custom]
410424
typedef string UserChannelId;
411425

@@ -432,3 +446,15 @@ typedef enum Event;
432446
typedef interface HRNResolverConfig;
433447

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

src/builder.rs

Lines changed: 151 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ use crate::chain::ChainSource;
4949
use crate::config::{
5050
default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole,
5151
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
52-
TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
53-
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT,
52+
ResolvedForwardedPaymentBucketConfig, TorConfig, DEFAULT_ESPLORA_SERVER_URL,
53+
DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, DEFAULT_MAX_PROBE_AMOUNT_MSAT,
54+
DEFAULT_MIN_PROBE_AMOUNT_MSAT,
5455
};
5556
use crate::connection::ConnectionManager;
5657
use crate::entropy::NodeEntropy;
@@ -60,12 +61,19 @@ use crate::gossip::GossipSource;
6061
use crate::io::sqlite_store::SqliteStore;
6162
use crate::io::utils::{
6263
open_or_migrate_fs_store, read_all_objects, read_event_queue,
63-
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
64-
read_output_sweeper, read_peer_info, read_scorer,
64+
read_external_pathfinding_scores_from_cache, read_forwarded_payment_bucket_size,
65+
read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info, read_scorer,
66+
write_forwarded_payment_bucket_size,
6567
};
6668
use crate::io::vss_store::VssStoreBuilder;
6769
use crate::io::{
68-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
70+
self, CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
71+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
72+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
73+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
74+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
75+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
76+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6977
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
7078
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7179
};
@@ -82,7 +90,8 @@ use crate::probing::{
8290
use crate::runtime::{Runtime, RuntimeSpawner};
8391
use crate::tx_broadcaster::TransactionBroadcaster;
8492
use crate::types::{
85-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
93+
AsyncPersister, ChainMonitor, ChannelForwardingStatsStore, ChannelManager,
94+
ChannelPairForwardingStatsStore, DynStore, DynStoreRef, DynStoreWrapper, ForwardedPaymentStore,
8695
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
8796
PeerManager, PendingPaymentStore,
8897
};
@@ -174,6 +183,10 @@ pub enum BuildError {
174183
InvalidTorProxyAddress,
175184
/// The provided alias is invalid.
176185
InvalidNodeAlias,
186+
/// The forwarded-payment bucket length is zero or too large to represent in seconds.
187+
InvalidForwardedPaymentBucketLength,
188+
/// The forwarded-payment bucket length differs from the value previously persisted.
189+
ForwardedPaymentBucketLengthMismatch,
177190
/// An attempt to setup a runtime has failed.
178191
RuntimeSetupFailed,
179192
/// We failed to read data from the [`KVStore`].
@@ -236,6 +249,12 @@ impl fmt::Display for BuildError {
236249
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
237250
Self::ChainSourceSetupFailed => write!(f, "Failed to setup the chain source."),
238251
Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."),
252+
Self::InvalidForwardedPaymentBucketLength => {
253+
write!(f, "Forwarded-payment bucket length is invalid.")
254+
},
255+
Self::ForwardedPaymentBucketLengthMismatch => {
256+
write!(f, "Forwarded-payment bucket length does not match persisted data.")
257+
},
239258
Self::NetworkMismatch => {
240259
write!(f, "Given network does not match the node's previously configured network.")
241260
},
@@ -1409,6 +1428,12 @@ fn build_with_store_internal(
14091428
) -> Result<Node, BuildError> {
14101429
optionally_install_rustls_cryptoprovider();
14111430

1431+
let configured_forwarded_payment_bucket_size_secs =
1432+
config.forwarded_payment_tracking_mode.bucket_size_secs().map_err(|_| {
1433+
log_error!(logger, "Forwarded-payment bucket length is zero or too large");
1434+
BuildError::InvalidForwardedPaymentBucketLength
1435+
})?;
1436+
14121437
if let Err(err) = may_announce_channel(&config) {
14131438
if config.announcement_addresses.is_some() {
14141439
log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err);
@@ -1440,24 +1465,72 @@ fn build_with_store_internal(
14401465

14411466
let kv_store_ref = Arc::clone(&kv_store);
14421467
let logger_ref = Arc::clone(&logger);
1443-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1444-
runtime.block_on(async move {
1445-
tokio::join!(
1446-
read_all_objects(
1447-
&*kv_store_ref,
1448-
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1449-
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1450-
Arc::clone(&logger_ref),
1451-
),
1452-
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1453-
read_all_objects(
1454-
&*kv_store_ref,
1455-
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1456-
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1457-
Arc::clone(&logger_ref),
1458-
)
1468+
let (
1469+
payment_store_res,
1470+
forwarded_payment_store_res,
1471+
channel_forwarding_stats_res,
1472+
channel_pair_forwarding_stats_res,
1473+
forwarded_payment_bucket_size_res,
1474+
node_metris_res,
1475+
pending_payment_store_res,
1476+
) = runtime.block_on(async move {
1477+
tokio::join!(
1478+
read_all_objects(
1479+
&*kv_store_ref,
1480+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1481+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1482+
Arc::clone(&logger_ref),
1483+
),
1484+
read_all_objects(
1485+
&*kv_store_ref,
1486+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1487+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1488+
Arc::clone(&logger_ref),
1489+
),
1490+
read_all_objects(
1491+
&*kv_store_ref,
1492+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
1493+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
1494+
Arc::clone(&logger_ref),
1495+
),
1496+
read_all_objects(
1497+
&*kv_store_ref,
1498+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE,
1499+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE,
1500+
Arc::clone(&logger_ref),
1501+
),
1502+
read_forwarded_payment_bucket_size(&*kv_store_ref, Arc::clone(&logger_ref)),
1503+
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1504+
read_all_objects(
1505+
&*kv_store_ref,
1506+
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1507+
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1508+
Arc::clone(&logger_ref),
14591509
)
1460-
});
1510+
)
1511+
});
1512+
1513+
let persisted_forwarded_payment_bucket_size_secs = match forwarded_payment_bucket_size_res {
1514+
Ok(bucket_size_secs) => Some(bucket_size_secs),
1515+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
1516+
Err(e) => {
1517+
log_error!(logger, "Failed to read forwarded-payment bucket size: {}", e);
1518+
return Err(BuildError::ReadFailed);
1519+
},
1520+
};
1521+
let forwarded_payment_bucket_config = ResolvedForwardedPaymentBucketConfig::resolve(
1522+
configured_forwarded_payment_bucket_size_secs,
1523+
persisted_forwarded_payment_bucket_size_secs,
1524+
)
1525+
.map_err(|_| {
1526+
log_error!(
1527+
logger,
1528+
"Configured forwarded-payment bucket size {:?} does not match persisted size {:?}",
1529+
configured_forwarded_payment_bucket_size_secs,
1530+
persisted_forwarded_payment_bucket_size_secs
1531+
);
1532+
BuildError::ForwardedPaymentBucketLengthMismatch
1533+
})?;
14611534

14621535
// Initialize the status fields.
14631536
let node_metrics = match node_metris_res {
@@ -1486,6 +1559,48 @@ fn build_with_store_internal(
14861559
},
14871560
};
14881561

1562+
let forwarded_payment_store = match forwarded_payment_store_res {
1563+
Ok(forwarded_payments) => Arc::new(ForwardedPaymentStore::new(
1564+
forwarded_payments,
1565+
FORWARDED_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1566+
FORWARDED_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1567+
Arc::clone(&kv_store),
1568+
Arc::clone(&logger),
1569+
)),
1570+
Err(e) => {
1571+
log_error!(logger, "Failed to read forwarded payment data from store: {}", e);
1572+
return Err(BuildError::ReadFailed);
1573+
},
1574+
};
1575+
1576+
let channel_forwarding_stats_store = match channel_forwarding_stats_res {
1577+
Ok(stats) => Arc::new(ChannelForwardingStatsStore::new(
1578+
stats,
1579+
CHANNEL_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1580+
CHANNEL_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1581+
Arc::clone(&kv_store),
1582+
Arc::clone(&logger),
1583+
)),
1584+
Err(e) => {
1585+
log_error!(logger, "Failed to read channel forwarding stats from store: {}", e);
1586+
return Err(BuildError::ReadFailed);
1587+
},
1588+
};
1589+
1590+
let channel_pair_forwarding_stats_store = match channel_pair_forwarding_stats_res {
1591+
Ok(stats) => Arc::new(ChannelPairForwardingStatsStore::new(
1592+
stats,
1593+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1594+
CHANNEL_PAIR_FORWARDING_STATS_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1595+
Arc::clone(&kv_store),
1596+
Arc::clone(&logger),
1597+
)),
1598+
Err(e) => {
1599+
log_error!(logger, "Failed to read channel pair forwarding stats from store: {}", e);
1600+
return Err(BuildError::ReadFailed);
1601+
},
1602+
};
1603+
14891604
let (chain_source, chain_tip_opt) = match chain_data_source_config {
14901605
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
14911606
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -2307,6 +2422,15 @@ fn build_with_store_internal(
23072422
})
23082423
});
23092424

2425+
if let Some(bucket_size_secs) = forwarded_payment_bucket_config.new_bucket_size_secs() {
2426+
runtime
2427+
.block_on(write_forwarded_payment_bucket_size(&*kv_store, bucket_size_secs))
2428+
.map_err(|e| {
2429+
log_error!(logger, "Failed to persist forwarded-payment bucket size: {}", e);
2430+
BuildError::WriteFailed
2431+
})?;
2432+
}
2433+
23102434
Ok(Node {
23112435
runtime,
23122436
stop_sender,
@@ -2334,6 +2458,10 @@ fn build_with_store_internal(
23342458
scorer,
23352459
peer_store,
23362460
payment_store,
2461+
forwarded_payment_store,
2462+
channel_forwarding_stats_store,
2463+
channel_pair_forwarding_stats_store,
2464+
forwarded_payment_bucket_size_secs: forwarded_payment_bucket_config.bucket_size_secs(),
23372465
lnurl_auth,
23382466
is_running,
23392467
node_metrics,

0 commit comments

Comments
 (0)