Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a6ffb98
data_store: Share mutation implementation
tnull Aug 13, 2026
d34be06
wallet: Deduplicate payment lookup on TxReplaced
tnull Aug 6, 2026
d695378
data_store: Make readers async and fallible
tnull Aug 6, 2026
a5f1bcb
f Adapt upstream asynchronous reads
tnull Aug 12, 2026
bc1c4c7
f Do not replay sent payments for logging
tnull Aug 12, 2026
3282393
f Adapt later asynchronous reads
tnull Aug 13, 2026
c679b82
data_store: Let readers wait for in-flight writes
tnull Aug 6, 2026
ba74ad7
data_store: Add a per-store cache policy
tnull Aug 6, 2026
add7c26
f Adapt upstream store construction
tnull Aug 12, 2026
dd515bb
f Avoid reads before inserts
tnull Aug 12, 2026
b4e982f
f Avoid reads before removals
tnull Aug 12, 2026
19e9a98
f Simplify cache policy internals
tnull Aug 13, 2026
a04645a
Add an `InvalidPageToken` error variant
tnull Aug 6, 2026
fbd520e
tests: Introduce a payment listing helper
tnull Aug 6, 2026
4d9bbb2
f Adapt later payment listing tests
tnull Aug 13, 2026
03a5322
io: Share bounded store reads
tnull Aug 12, 2026
3591b77
Paginate `Node::list_payments`
tnull Aug 6, 2026
ce6b8b0
f Document pagination consistency
tnull Aug 11, 2026
01090c8
f Qualify payment page token lifetime
tnull Aug 11, 2026
997ccf1
f Preallocate missing payment reads
tnull Aug 13, 2026
345c1d2
Bound the payment store's in-memory cache
tnull Aug 6, 2026
9abea2b
f Adapt upstream payment store tests
tnull Aug 12, 2026
23f92a0
f Adapt later bounded-store tests
tnull Aug 13, 2026
329f616
io: Read only as many objects as a store needs
tnull Aug 6, 2026
f9edeff
f Qualify payment cache warmup
tnull Aug 13, 2026
280f7c1
f Bail out on a page token that does not advance
tnull Aug 13, 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?


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

Expand Down
Loading
Loading