Skip to content

Commit 6319183

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 76a5294 commit 6319183

13 files changed

Lines changed: 2940 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",
@@ -395,6 +400,9 @@ typedef string OfferId;
395400
[Custom]
396401
typedef string PaymentId;
397402

403+
[Custom]
404+
typedef string ForwardedPaymentId;
405+
398406
[Custom]
399407
typedef string PaymentHash;
400408

@@ -407,6 +415,12 @@ typedef string PaymentSecret;
407415
[Custom]
408416
typedef string ChannelId;
409417

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

@@ -433,3 +447,15 @@ typedef enum Event;
433447
typedef interface HRNResolverConfig;
434448

435449
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: 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
},
@@ -1408,6 +1427,12 @@ fn build_with_store_internal(
14081427
) -> Result<Node, BuildError> {
14091428
optionally_install_rustls_cryptoprovider();
14101429

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

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

14611534
// Initialize the status fields.
14621535
let node_metrics = match node_metris_res {
@@ -1485,6 +1558,48 @@ fn build_with_store_internal(
14851558
},
14861559
};
14871560

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

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

0 commit comments

Comments
 (0)