Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e4dbe31
Set restrictive file permissions for seed file
tnull Jan 7, 2026
042c5de
Run rate limiter garbage collection before inserting new user
joostjager Apr 2, 2026
618deed
Reject malformed (odd-length) hex strings
tnull May 19, 2026
73a255b
Refuse skimmed fees on payments without a record
tnull Oct 8, 2026
fbd3bd7
Reject oversized splice-out amounts
tnull Jun 10, 2026
de12588
Reject overflowing unified receive amounts
benthecarman Jun 12, 2026
09f9bbb
Avoid Bolt11 claim amount underflow
benthecarman Jun 12, 2026
c4f3c17
Keep insert-or-update cache unchanged on persist failure
benthecarman Jun 15, 2026
5af74e4
Keep update cache unchanged on persist failure
benthecarman Jun 15, 2026
3bcef49
Keep removed objects cached on persist failure
benthecarman Jun 15, 2026
36c7208
Fix anchor reserves when splicing in all funds
tankyleo Jun 27, 2026
1e48b30
Reserve onchain funds for anchor channels when peer sets them optional
tankyleo Jun 26, 2026
2ee491f
bitcoind: fix raw TX fetching when using REST
jharveyb Jul 24, 2026
47cc674
Follow reorgs with bitcoind REST
tnull Aug 5, 2026
b92fe5f
Stop logging payment preimages at INFO level
elnafateh Aug 9, 2026
2ad1237
Retain Electrum `Filter` registrations across `stop`/`start`
Jolah1 Aug 6, 2026
b5fa4c2
Fail closed on manager read errors
tnull Aug 7, 2026
82aa471
Collect the raw mempool txids into a HashSet
tankyleo Aug 25, 2026
6ba5f19
Fix BOLT11 DuplicatePayment triggering on-chain fallback in unified p…
elnafateh Aug 25, 2026
75b089f
Fix unified payment falling back to on-chain after PersistenceFailed
elnafateh Aug 25, 2026
833a715
Keep reserved change addresses marked during fee estimates
jkczyz Sep 11, 2026
78e875e
fix: BOLT12 arm of UnifiedPayment::send returns DuplicatePayment/Pers…
Bartok9 Sep 13, 2026
c43be1a
fix(payments): ignore late failures for successful Lightning payments
thesimplekid Sep 22, 2026
1cbcd61
Count unknown channel types in anchor reserve
tankyleo Oct 6, 2026
ad36b93
Drop spurious outbound payment status update on inb payment
TheBlueMatt Sep 13, 2026
9517adf
Do not mark a successful inbound payment as failed on a dup HTLC
TheBlueMatt Sep 13, 2026
68d2142
Reject payments with unknown required TLVs
TheBlueMatt Sep 13, 2026
81eb994
Reject underpaid BOLT11 JIT invoices
tnull Oct 7, 2026
11e3da9
Bump version to v0.7.1 and update CHANGELOG
tnull Oct 8, 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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
# 0.7.1 - Oct. 12, 2026
This patch release backports a number of bug fixes and security-relevant hardening.

## Bug Fixes and Improvements
- Inbound payments with a counterparty-withheld fee are now refused unless they are JIT-channel
payments for which we authorized the fee, closing a gap for BOLT12 and spontaneous payments. (#899)
- Unified (BIP21) payments no longer fall back to the next payment method when the BOLT12 or BOLT11
leg fails with `DuplicatePayment` or `PersistenceFailed`, avoiding potential double payments.
(#1038, #1098)
- A late `PaymentFailed` event no longer marks an already-succeeded outbound payment as failed.
(#1065)
- An inbound HTLC reusing the payment hash of an outbound payment no longer marks that outbound
payment as failed. (#1157)
- A duplicate HTLC for an already-received payment no longer marks that payment as failed. (#1157)
- Inbound payments carrying unknown even-typed custom TLVs are now failed back instead of being
surfaced as claimable, as LDK would refuse to claim them. (#1157)
- BOLT11 JIT invoices now enforce their amount at the recipient, so a payer can no longer bypass
the LSP and have an underpayment accepted over an existing channel. (#1156)
- `Builder::build` now fails instead of starting a fresh node when the `ChannelManager` can't be
read from the store for reasons other than it being absent. (#1032)
- In-memory payment data is no longer updated when the corresponding persistence operation fails.
(#933)
- An anchor reserve is now kept for peers that merely support (rather than require) anchor
channels, while splice-ins no longer require an additional reserve. (#660)
- Channels whose type is not yet negotiated are now counted toward the anchor reserve, so
concurrent channel opens can no longer leave too little reserved. (#1152)
- Electrum `Filter` registrations are now retained across `Node::stop`/`Node::start` cycles.
(#1025)
- Raw transaction fetching and reorg handling via Bitcoin Core REST have been fixed. (#1006, #1022)
- Fee estimation for send-all operations no longer clears a reserved change address, avoiding
address reuse. (#1096)
- Seed files are now created with owner-only permissions. (#741)
- Payment preimages are no longer logged on successful sends. (#1035)
- Several unchecked arithmetic operations have been fixed. (#927, #940)
- The async payments rate limiter now caps the number of tracked users. (#866)
- `bitcoind` mempool reconciliation now runs in linear rather than quadratic time. (#1068)
- Odd-length hex strings are now rejected when parsing hex in the bindings. (#909)

# 0.7.0 - Dec. 3, 2025
This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend.

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ldk-node"
version = "0.7.0"
version = "0.7.1"
authors = ["Elias Rohrer <dev@tnull.de>"]
homepage = "https://lightningdevkit.org/"
license = "MIT OR Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import PackageDescription

let tag = "v0.7.0"
let tag = "v0.7.1"
let checksum = "37e909987c285ddaaabf7caede58f0695491398acd4561987914996f4623a3c3"
let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip"

Expand Down
2 changes: 1 addition & 1 deletion bindings/kotlin/ldk-node-android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
libraryVersion=0.7.0
libraryVersion=0.7.1
2 changes: 1 addition & 1 deletion bindings/kotlin/ldk-node-jvm/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
libraryVersion=0.7.0
libraryVersion=0.7.1
2 changes: 1 addition & 1 deletion bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ldk_node"
version = "0.7.0"
version = "0.7.1"
authors = [
{ name="Elias Rohrer", email="dev@tnull.de" },
]
Expand Down
107 changes: 105 additions & 2 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,12 +1494,21 @@ fn build_with_store_internal(

// Initialize the ChannelManager
let channel_manager = {
if let Ok(res) = KVStoreSync::read(
let channel_manager_bytes = match KVStoreSync::read(
&*kv_store,
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
CHANNEL_MANAGER_PERSISTENCE_KEY,
) {
Ok(res) => Some(res),
Err(e) if e.kind() == lightning::io::ErrorKind::NotFound => None,
Err(e) => {
log_error!(logger, "Failed to read channel manager from store: {}", e);
return Err(BuildError::ReadFailed);
},
};

if let Some(res) = channel_manager_bytes {
let mut reader = Cursor::new(res);
let channel_monitor_references =
channel_monitors.iter().map(|(_, chanmon)| chanmon).collect();
Expand Down Expand Up @@ -1926,7 +1935,101 @@ pub(crate) fn sanitize_alias(alias_str: &str) -> Result<NodeAlias, BuildError> {

#[cfg(test)]
mod tests {
use super::{sanitize_alias, BuildError, NodeAlias};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use lightning::io;
use lightning::util::persist::{
KVStore, KVStoreSync, CHANNEL_MANAGER_PERSISTENCE_KEY,
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
};

use super::{sanitize_alias, BuildError, NodeAlias, NodeBuilder};
use crate::io::test_utils::InMemoryStore;
use crate::types::DynStore;

struct ChannelManagerReadFailingStore(InMemoryStore);

impl ChannelManagerReadFailingStore {
fn fails_read(primary_namespace: &str, secondary_namespace: &str, key: &str) -> bool {
primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
&& secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE
&& key == CHANNEL_MANAGER_PERSISTENCE_KEY
}
}

impl KVStoreSync for ChannelManagerReadFailingStore {
fn read(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
) -> io::Result<Vec<u8>> {
if Self::fails_read(primary_namespace, secondary_namespace, key) {
return Err(io::Error::new(io::ErrorKind::Other, "channel manager read failed"));
}
KVStoreSync::read(&self.0, primary_namespace, secondary_namespace, key)
}

fn write(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
) -> io::Result<()> {
KVStoreSync::write(&self.0, primary_namespace, secondary_namespace, key, buf)
}

fn remove(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
) -> io::Result<()> {
KVStoreSync::remove(&self.0, primary_namespace, secondary_namespace, key, lazy)
}

fn list(
&self, primary_namespace: &str, secondary_namespace: &str,
) -> io::Result<Vec<String>> {
KVStoreSync::list(&self.0, primary_namespace, secondary_namespace)
}
}

impl KVStore for ChannelManagerReadFailingStore {
fn read(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, io::Error>> + 'static + Send>> {
let res = KVStoreSync::read(self, primary_namespace, secondary_namespace, key);
Box::pin(async move { res })
}

fn write(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<(), io::Error>> + 'static + Send>> {
let res = KVStoreSync::write(self, primary_namespace, secondary_namespace, key, buf);
Box::pin(async move { res })
}

fn remove(
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
) -> Pin<Box<dyn Future<Output = Result<(), io::Error>> + 'static + Send>> {
let res = KVStoreSync::remove(self, primary_namespace, secondary_namespace, key, lazy);
Box::pin(async move { res })
}

fn list(
&self, primary_namespace: &str, secondary_namespace: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, io::Error>> + 'static + Send>> {
let res = KVStoreSync::list(self, primary_namespace, secondary_namespace);
Box::pin(async move { res })
}
}

#[test]
fn channel_manager_read_failure_fails_build() {
let mut builder = NodeBuilder::new();
builder.set_entropy_seed_bytes([42; 64]);
builder.set_log_facade_logger();
let store: Arc<DynStore> = Arc::new(ChannelManagerReadFailingStore(InMemoryStore::new()));

let result = builder.build_with_store(store);

assert!(matches!(result, Err(BuildError::ReadFailed)));
}

#[test]
fn sanitize_empty_node_alias() {
Expand Down
55 changes: 42 additions & 13 deletions src/chain/bitcoind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

use std::collections::{HashMap, VecDeque};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
Expand Down Expand Up @@ -910,7 +910,7 @@ impl BitcoindClient {
}

/// Retrieves the raw mempool.
pub(crate) async fn get_raw_mempool(&self) -> std::io::Result<Vec<Txid>> {
pub(crate) async fn get_raw_mempool(&self) -> std::io::Result<HashSet<Txid>> {
match self {
BitcoindClient::Rpc { rpc_client, .. } => {
Self::get_raw_mempool_rpc(Arc::clone(rpc_client)).await
Expand All @@ -922,7 +922,7 @@ impl BitcoindClient {
}

/// Retrieves the raw mempool via the RPC interface.
async fn get_raw_mempool_rpc(rpc_client: Arc<RpcClient>) -> std::io::Result<Vec<Txid>> {
async fn get_raw_mempool_rpc(rpc_client: Arc<RpcClient>) -> std::io::Result<HashSet<Txid>> {
let verbose_flag_json = serde_json::json!(false);
rpc_client
.call_method::<GetRawMempoolResponse>("getrawmempool", &[verbose_flag_json])
Expand All @@ -931,7 +931,7 @@ impl BitcoindClient {
}

/// Retrieves the raw mempool via the REST interface.
async fn get_raw_mempool_rest(rest_client: Arc<RestClient>) -> std::io::Result<Vec<Txid>> {
async fn get_raw_mempool_rest(rest_client: Arc<RestClient>) -> std::io::Result<HashSet<Txid>> {
rest_client
.request_resource::<JsonResponse, GetRawMempoolResponse>(
"mempool/contents.json?verbose=false",
Expand Down Expand Up @@ -1194,9 +1194,14 @@ impl BlockSource for BitcoindClient {
BitcoindClient::Rpc { rpc_client, .. } => {
Box::pin(async move { rpc_client.get_header(header_hash, height_hint).await })
},
BitcoindClient::Rest { rest_client, .. } => {
Box::pin(async move { rest_client.get_header(header_hash, height_hint).await })
},
BitcoindClient::Rest { rest_client, rpc_client, .. } => Box::pin(async move {
match rest_client.get_header(header_hash, height_hint).await {
Err(e) if e.kind() == BlockSourceErrorKind::Persistent => {
rpc_client.get_header(header_hash, height_hint).await
},
result => result,
}
}),
}
}

Expand Down Expand Up @@ -1272,9 +1277,12 @@ pub(crate) struct GetRawTransactionResponse(pub Transaction);
impl TryInto<GetRawTransactionResponse> for JsonResponse {
type Error = std::io::Error;
fn try_into(self) -> std::io::Result<GetRawTransactionResponse> {
// Accept both the bare hex-encoded TX from RPC, and the REST response
// of a JSON object with the hex-encoded TX in the 'hex' field.
let tx = self
.0
.as_str()
.or_else(|| self.0.get("hex").and_then(|v| v.as_str()))
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to parse getrawtransaction response",
Expand All @@ -1292,7 +1300,7 @@ impl TryInto<GetRawTransactionResponse> for JsonResponse {
}
}

pub struct GetRawMempoolResponse(Vec<Txid>);
pub struct GetRawMempoolResponse(HashSet<Txid>);

impl TryInto<GetRawMempoolResponse> for JsonResponse {
type Error = std::io::Error;
Expand All @@ -1302,7 +1310,7 @@ impl TryInto<GetRawMempoolResponse> for JsonResponse {
"Failed to parse getrawmempool response",
))?;

let mut mempool_transactions = Vec::with_capacity(res.len());
let mut mempool_transactions = HashSet::with_capacity(res.len());

for hex in res {
let txid = if let Some(hex_str) = hex.as_str() {
Expand All @@ -1322,7 +1330,7 @@ impl TryInto<GetRawMempoolResponse> for JsonResponse {
));
};

mempool_transactions.push(txid);
mempool_transactions.insert(txid);
}

Ok(GetRawMempoolResponse(mempool_transactions))
Expand Down Expand Up @@ -1480,6 +1488,8 @@ impl std::fmt::Display for HttpError {

#[cfg(test)]
mod tests {
use std::collections::HashSet;

use bitcoin::hashes::Hash;
use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};
use lightning_block_sync::http::JsonResponse;
Expand Down Expand Up @@ -1558,10 +1568,10 @@ mod tests {

#[test]
fn prop_get_raw_mempool_response_roundtrip(txids in vec(any::<[u8;32]>(), 0..10)) {
let txid_vec: Vec<Txid> = txids.into_iter().map(Txid::from_byte_array).collect();
let original = GetRawMempoolResponse(txid_vec.clone());
let txid_set: HashSet<Txid> = txids.into_iter().map(Txid::from_byte_array).collect();
let original = GetRawMempoolResponse(txid_set.clone());

let json_vec: Vec<String> = txid_vec.iter().map(|t| t.to_string()).collect();
let json_vec: Vec<String> = txid_set.iter().map(|t| t.to_string()).collect();
let json_val = serde_json::Value::Array(json_vec.iter().map(|s| json!(s)).collect());

let resp = JsonResponse(json_val);
Expand Down Expand Up @@ -1603,6 +1613,25 @@ mod tests {
prop_assert_eq!(decoded.0, tx);
}

#[test]
fn prop_get_raw_transaction_response_json_object_roundtrip(tx in arbitrary_transaction()) {
let hex = bitcoin::consensus::encode::serialize_hex(&tx);
let vsize = tx.vsize();
let json_val = json!({
"version": tx.version,
"vsize": vsize,
"hex": hex,
});

let resp = JsonResponse(json_val);
let decoded: GetRawTransactionResponse = resp.try_into().unwrap();

prop_assert_eq!(decoded.0.compute_txid(), tx.compute_txid());
prop_assert_eq!(decoded.0.compute_wtxid(), tx.compute_wtxid());

prop_assert_eq!(decoded.0, tx);
}

#[test]
fn prop_fee_response_roundtrip(fee_rate in any::<f64>()) {
let fee_rate = fee_rate.abs();
Expand Down
Loading
Loading