Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1e38d24
data_store: Share mutation implementation
tnull Aug 13, 2026
957212b
wallet: Deduplicate payment lookup on TxReplaced
tnull Aug 6, 2026
b5b8130
data_store: Make readers async and fallible
tnull Aug 6, 2026
6696f2f
f Adapt upstream asynchronous reads
tnull Aug 12, 2026
352188b
f Do not replay sent payments for logging
tnull Aug 12, 2026
83624ed
f Adapt later asynchronous reads
tnull Aug 13, 2026
b891bab
data_store: Let readers wait for in-flight writes
tnull Aug 6, 2026
e33a4ae
data_store: Add a per-store cache policy
tnull Aug 6, 2026
ce435be
f Adapt upstream store construction
tnull Aug 12, 2026
a968238
f Avoid reads before inserts
tnull Aug 12, 2026
ffcd9a9
f Avoid reads before removals
tnull Aug 12, 2026
2700cee
f Simplify cache policy internals
tnull Aug 13, 2026
2ada5f7
Add an `InvalidPageToken` error variant
tnull Aug 6, 2026
41ae88b
tests: Introduce a payment listing helper
tnull Aug 6, 2026
2633a38
f Adapt later payment listing tests
tnull Aug 13, 2026
241b3d8
io: Share bounded store reads
tnull Aug 12, 2026
1807a9c
Paginate `Node::list_payments`
tnull Aug 6, 2026
c14cb71
f Document pagination consistency
tnull Aug 11, 2026
e918649
f Qualify payment page token lifetime
tnull Aug 11, 2026
320ec0b
f Preallocate missing payment reads
tnull Aug 13, 2026
d569490
Bound the payment store's in-memory cache
tnull Aug 6, 2026
0bd0246
f Adapt upstream payment store tests
tnull Aug 12, 2026
b61d043
f Adapt later bounded-store tests
tnull Aug 13, 2026
a386723
io: Read only as many objects as a store needs
tnull Aug 6, 2026
3b51b96
f Qualify payment cache warmup
tnull Aug 13, 2026
4a13f71
f Bail out on a page token that does not advance
tnull Aug 13, 2026
17fbb7a
Report invalid VSS page tokens
tnull Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Pending

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

## Feature and API updates
- `Node::list_payments` is now paginated: it takes an optional `PageToken` and returns a
`PaymentDetailsPage` holding one page of payments, ordered from most recently created to
least recently created, plus the token for the next page. Ordering and page tokens come
from the configured storage backend, and token lifetime follows that backend's guarantees.
This replaces the previous unpaginated `Node::list_payments`, and
`Node::list_payments_with_filter` has been removed; filter the returned pages instead.
- `Node::payment` now returns a `Result`, as retrieving a payment may fail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also async now

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still sync? Or maybe misunderstanding?

- The Bitcoin Core RPC and REST chain-source builder methods now accept an optional
`wallet_rescan_from_height` argument. Passing a height lets fresh wallets rescan from a known
birthday block instead of checkpointing at the current tip, which is useful when restoring a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,12 @@ class LibraryTest {
assert(paymentReceivedEvent is Event.PaymentReceived)
node2.eventHandled()

assert(node1.listPayments().size == 3)
assert(node2.listPayments().size == 2)
assert(node1.listPayments(null).payments.size == 3)
assert(node2.listPayments(null).payments.size == 2)

// A page token has to survive a round trip through a string, so that an app can persist
// one and resume paginating after a restart.
assert(PageToken("some-page-token").toString() == "some-page-token")

node2.closeChannel(userChannelId, nodeId1)

Expand Down
9 changes: 8 additions & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,13 @@ interface Node {
void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config);
[Throws=NodeError]
void sync_wallets();
[Throws=NodeError]
PaymentDetails? payment([ByRef]PaymentId payment_id);
[Throws=NodeError]
void remove_payment([ByRef]PaymentId payment_id);
BalanceDetails list_balances();
sequence<PaymentDetails> list_payments();
[Throws=NodeError]
PaymentDetailsPage list_payments(PageToken? page_token);
sequence<PeerDetails> list_peers();
sequence<ChannelDetails> list_channels();
NetworkGraph network_graph();
Expand Down Expand Up @@ -236,6 +238,7 @@ enum NodeError {
"InvalidDateTime",
"InvalidFeeRate",
"InvalidScriptPubKey",
"InvalidPageToken",
"DuplicatePayment",
"UnsupportedCurrency",
"InsufficientFunds",
Expand Down Expand Up @@ -279,6 +282,10 @@ enum PaymentFailureReason {

typedef dictionary PaymentDetails;

typedef dictionary PaymentDetailsPage;

typedef interface PageToken;

[Remote]
dictionary RouteParametersConfig {
u64? max_total_routing_fee_msat;
Expand Down
21 changes: 16 additions & 5 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,20 @@ use crate::config::{
default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole,
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT,
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT, PAYMENT_CACHE_CAPACITY,
PAYMENT_CACHE_WARMUP_COUNT,
};
use crate::connection::ConnectionManager;
use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed};
use crate::entropy::NodeEntropy;
use crate::event::EventQueue;
use crate::fee_estimator::OnchainFeeEstimator;
use crate::gossip::GossipSource;
use crate::io::sqlite_store::SqliteStore;
use crate::io::utils::{
open_or_migrate_fs_store, read_all_objects, read_event_queue,
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
read_output_sweeper, read_peer_info, read_scorer,
read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph,
read_node_metrics, read_output_sweeper, read_peer_info, read_scorer,
};
use crate::io::vss_store::VssStoreBuilder;
use crate::io::{
Expand Down Expand Up @@ -1458,10 +1460,11 @@ fn build_with_store_internal(
let (payment_store_res, node_metris_res, pending_payment_store_res, address_pool_res) = runtime
.block_on(async move {
tokio::join!(
read_all_objects(
read_n_objects(
&*kv_store_ref,
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
PAYMENT_CACHE_WARMUP_COUNT,
Arc::clone(&logger_ref),
),
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
Expand Down Expand Up @@ -1490,7 +1493,11 @@ fn build_with_store_internal(

let payment_store = match payment_store_res {
Ok(payments) => Arc::new(PaymentStore::new(
payments,
// The read hands us the newest payments first, while the cache treats the objects it
// is seeded with as increasingly recently used. Reverse them, so that the newest
// payment is the last one to be evicted rather than the first.
payments.into_iter().rev().collect(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LRU seeding order is guarded only by a commentsrc/builder.rs:1495 read_n_objects returns newest-first, but DataStore::new treats the last seed element as most-recently-used, so the builder must .rev(). Dropping that call would silently evict the newest payments first with no test to catch it.

KeepLeastRecentlyUsed::new(PAYMENT_CACHE_CAPACITY),
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
Arc::clone(&kv_store),
Expand Down Expand Up @@ -1745,8 +1752,12 @@ fn build_with_store_internal(
};

let pending_payment_store = match pending_payment_store_res {
// NOTE: This store must keep all its entries in memory: the wallet scans it in full on
// every chain tip change and to resolve replaced transactions. It stays bounded anyway,
// as entries are removed once a payment is no longer pending.
Ok(pending_payments) => Arc::new(PendingPaymentStore::new(
pending_payments,
KeepAllEntries,
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
Arc::clone(&kv_store),
Expand Down
17 changes: 17 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! Objects for configuring the node.

use std::fmt;
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::time::Duration;

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

// The number of payments we keep in memory.
//
// The payment history grows for the lifetime of a node, so we cache only the most recently used
// payments and read the rest back from the store as they are needed. At roughly 400 to 500 bytes
// per cached payment, this bounds the payment store's share of memory at well under a megabyte,
// while still covering the recent payments a node actually works with.
pub(crate) const PAYMENT_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(1000).unwrap();

// The number of payments we read into the cache when starting up.
//
// This matches the built-in storage backends' page size, so warming the cache costs a single page
// listing and one batch of reads. Immediately after startup, a first-page `Node::list_payments`
// call reads only its keys from storage; the payment bodies come from the cache. Later activity
// may displace those entries.
pub(crate) const PAYMENT_CACHE_WARMUP_COUNT: NonZeroUsize = NonZeroUsize::new(50).unwrap();

@benthecarman benthecarman Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have this 50 as a const somewhere, can we use that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we have some consts that happen to be 50, but they are somewhat orthogonal? Which one do you have in mind exactly?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the comment highlights that its equal ot the page size. We have separate consts for posgres and sqlite page size. Should unify those and use that const here

@tnull tnull Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure? It's basically a coincidence that it's a page size, though likely a multiple of page size makes sense? Not sure if we want to couple the concepts strongly here, would also be a layer violation somewhat, and one additional thing we'd need to disentangle when upstreaming VSS/Postgres stores?


// The default {Esplora,Electrum} client timeout we're using.
const DEFAULT_PER_REQUEST_TIMEOUT_SECS: u8 = 10;

Expand Down
Loading
Loading