Skip to content

Commit 2cebf35

Browse files
authored
Merge pull request #1024 from tnull/2026-08-data-store-cache-policy
Stop keeping all `DataStore` entries in-memory, add pagination
2 parents aa6d051 + b0c8b56 commit 2cebf35

26 files changed

Lines changed: 2698 additions & 529 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Pending
22

33
## Compatibility Notes
4+
- Migrating between storage backends does not preserve the relative creation order of
5+
pre-existing payments, as the generic KV store migration copies entries in an unspecified
6+
order. Expect the order in which `Node::list_payments` returns pre-existing payments to
7+
change once after such a migration. Payment contents and completeness are unaffected.
48
- Pending JIT-channel payments created before upgrading may fail after upgrade because the
59
prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated.
610
- Upgrading from LDK Node v0.1 is no longer supported if the event queue still contains
@@ -19,6 +23,13 @@
1923
`Event::PaymentClaimable`.
2024

2125
## Feature and API updates
26+
- `Node::list_payments` is now paginated: it takes an optional `PageToken` and returns a
27+
`PaymentDetailsPage` holding one page of payments, ordered from most recently created to
28+
least recently created, plus the token for the next page. Ordering and page tokens come
29+
from the configured storage backend, and token lifetime follows that backend's guarantees.
30+
This replaces the previous unpaginated `Node::list_payments`, and
31+
`Node::list_payments_with_filter` has been removed; filter the returned pages instead.
32+
- `Node::payment` now returns a `Result`, as retrieving a payment may fail.
2233
- The Bitcoin Core RPC and REST chain-source builder methods now accept an optional
2334
`wallet_rescan_from_height` argument. Passing a height lets fresh wallets rescan from a known
2435
birthday block instead of checkpointing at the current tip, which is useful when restoring a

bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,12 @@ class LibraryTest {
315315
assert(paymentReceivedEvent is Event.PaymentReceived)
316316
node2.eventHandled()
317317

318-
assert(node1.listPayments().size == 3)
319-
assert(node2.listPayments().size == 2)
318+
assert(node1.listPayments(null).payments.size == 3)
319+
assert(node2.listPayments(null).payments.size == 2)
320+
321+
// A page token has to survive a round trip through a string, so that an app can persist
322+
// one and resume paginating after a restart.
323+
assert(PageToken("some-page-token").toString() == "some-page-token")
320324

321325
closeChannelWithRetry { node2.closeChannel(userChannelId, nodeId1) }
322326

bindings/ldk_node.udl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,13 @@ interface Node {
147147
void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config);
148148
[Throws=NodeError]
149149
void sync_wallets();
150+
[Throws=NodeError]
150151
PaymentDetails? payment([ByRef]PaymentId payment_id);
151152
[Throws=NodeError]
152153
void remove_payment([ByRef]PaymentId payment_id);
153154
BalanceDetails list_balances();
154-
sequence<PaymentDetails> list_payments();
155+
[Throws=NodeError]
156+
PaymentDetailsPage list_payments(PageToken? page_token);
155157
sequence<PeerDetails> list_peers();
156158
sequence<ChannelDetails> list_channels();
157159
NetworkGraph network_graph();
@@ -236,6 +238,7 @@ enum NodeError {
236238
"InvalidDateTime",
237239
"InvalidFeeRate",
238240
"InvalidScriptPubKey",
241+
"InvalidPageToken",
239242
"DuplicatePayment",
240243
"UnsupportedCurrency",
241244
"InsufficientFunds",
@@ -279,6 +282,10 @@ enum PaymentFailureReason {
279282

280283
typedef dictionary PaymentDetails;
281284

285+
typedef dictionary PaymentDetailsPage;
286+
287+
typedef interface PageToken;
288+
282289
[Remote]
283290
dictionary RouteParametersConfig {
284291
u64? max_total_routing_fee_msat;

src/builder.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,20 @@ use crate::config::{
5050
default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole,
5151
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
5252
TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
53-
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT,
53+
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT, PAYMENT_CACHE_CAPACITY,
54+
PAYMENT_CACHE_WARMUP_COUNT,
5455
};
5556
use crate::connection::ConnectionManager;
57+
use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed};
5658
use crate::entropy::NodeEntropy;
5759
use crate::event::EventQueue;
5860
use crate::fee_estimator::OnchainFeeEstimator;
5961
use crate::gossip::GossipSource;
6062
use crate::io::sqlite_store::SqliteStore;
6163
use crate::io::utils::{
6264
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,
65+
read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph,
66+
read_node_metrics, read_output_sweeper, read_peer_info, read_scorer,
6567
};
6668
use crate::io::vss_store::VssStoreBuilder;
6769
use crate::io::{
@@ -1458,10 +1460,11 @@ fn build_with_store_internal(
14581460
let (payment_store_res, node_metris_res, pending_payment_store_res, address_pool_res) = runtime
14591461
.block_on(async move {
14601462
tokio::join!(
1461-
read_all_objects(
1463+
read_n_objects(
14621464
&*kv_store_ref,
14631465
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
14641466
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1467+
PAYMENT_CACHE_WARMUP_COUNT,
14651468
Arc::clone(&logger_ref),
14661469
),
14671470
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
@@ -1490,7 +1493,11 @@ fn build_with_store_internal(
14901493

14911494
let payment_store = match payment_store_res {
14921495
Ok(payments) => Arc::new(PaymentStore::new(
1493-
payments,
1496+
// The read hands us the newest payments first, while the cache treats the objects it
1497+
// is seeded with as increasingly recently used. Reverse them, so that the newest
1498+
// payment is the last one to be evicted rather than the first.
1499+
payments.into_iter().rev().collect(),
1500+
KeepLeastRecentlyUsed::new(PAYMENT_CACHE_CAPACITY),
14941501
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
14951502
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
14961503
Arc::clone(&kv_store),
@@ -1745,8 +1752,12 @@ fn build_with_store_internal(
17451752
};
17461753

17471754
let pending_payment_store = match pending_payment_store_res {
1755+
// NOTE: This store must keep all its entries in memory: the wallet scans it in full on
1756+
// every chain tip change and to resolve replaced transactions. It stays bounded anyway,
1757+
// as entries are removed once a payment is no longer pending.
17481758
Ok(pending_payments) => Arc::new(PendingPaymentStore::new(
17491759
pending_payments,
1760+
KeepAllEntries,
17501761
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
17511762
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
17521763
Arc::clone(&kv_store),

src/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! Objects for configuring the node.
99
1010
use std::fmt;
11+
use std::num::NonZeroUsize;
1112
use std::str::FromStr;
1213
use std::time::Duration;
1314

@@ -48,6 +49,22 @@ pub(crate) const DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS: u64 = 10;
4849
// The default timeout after which we abort a transaction broadcast operation.
4950
pub(crate) const DEFAULT_TX_BROADCAST_TIMEOUT_SECS: u64 = 10;
5051

52+
// The number of payments we keep in memory.
53+
//
54+
// The payment history grows for the lifetime of a node, so we cache only the most recently used
55+
// payments and read the rest back from the store as they are needed. At roughly 400 to 500 bytes
56+
// per cached payment, this bounds the payment store's share of memory at well under a megabyte,
57+
// while still covering the recent payments a node actually works with.
58+
pub(crate) const PAYMENT_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(1000).unwrap();
59+
60+
// The number of payments we read into the cache when starting up.
61+
//
62+
// This matches the built-in storage backends' page size, so warming the cache costs a single page
63+
// listing and one batch of reads. Immediately after startup, a first-page `Node::list_payments`
64+
// call reads only its keys from storage; the payment bodies come from the cache. Later activity
65+
// may displace those entries.
66+
pub(crate) const PAYMENT_CACHE_WARMUP_COUNT: NonZeroUsize = NonZeroUsize::new(50).unwrap();
67+
5168
// The default {Esplora,Electrum} client timeout we're using.
5269
const DEFAULT_PER_REQUEST_TIMEOUT_SECS: u8 = 10;
5370

0 commit comments

Comments
 (0)