Skip to content

Commit dc02b76

Browse files
authored
Merge pull request #278 from benthecarman/implement-fee-bumping
Add fee bumping for on-chain payments and pending splices
2 parents aa2d60f + 5b1177a commit dc02b76

21 files changed

Lines changed: 1046 additions & 95 deletions

File tree

‎docs/api-guide.md‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,13 @@ RPCs with no permission mapping return `UNIMPLEMENTED`, even for admin tokens.
9797
| ---------- | ------ |
9898
| `node:read` | Node information, balances, and pathfinding scores |
9999
| `onchain:receive` | Create on-chain receive addresses |
100-
| `onchain:send` | Send on-chain funds |
100+
| `onchain:send` | Send on-chain funds or bump an on-chain payment fee |
101101
| `invoices:create` | Create BOLT11/BOLT12 invoices and incoming refund requests |
102102
| `payments:read` | Read payments, forwarded payments, and forwarding statistics |
103103
| `payments:claim` | Claim or fail held BOLT11 payments |
104104
| `payments:send` | Send BOLT11, BOLT12, spontaneous, unified, and refund payments; splice out |
105105
| `channels:read` | List channels |
106-
| `channels:manage` | Open, configure, cooperatively close, or splice funds into channels |
106+
| `channels:manage` | Open, configure, cooperatively close, splice funds into channels, or bump pending splice fees |
107107
| `channels:force_close` | Force-close channels |
108108
| `peers:read` | List peers |
109109
| `peers:manage` | Connect or disconnect peers |
@@ -174,6 +174,12 @@ All RPCs are unary (single request, single response) unless noted otherwise.
174174
|------------------|----------------------------------------------------------------------|
175175
| `OnchainReceive` | Generate a new on-chain funding address |
176176
| `OnchainSend` | Send to a Bitcoin address (with optional fee rate and send-all mode) |
177+
| `OnchainBumpFee` | Raise the fee of an unconfirmed outbound on-chain payment using RBF |
178+
179+
`OnchainBumpFee` replaces a payment's transaction while preserving its payment ID and recipient
180+
amount. Use the `payment_id` from `ListPayments`. The optional `fee_rate_sat_per_vb` sets the new
181+
total fee rate in sat/vB; omit it to use an automatic rate. The response contains the replacement
182+
`txid`. Confirmed, inbound, Lightning, and channel funding payments are not eligible.
177183

178184
### BOLT11 Payments
179185

@@ -229,15 +235,28 @@ a channel just-in-time when the invoice is paid.
229235

230236
### Channel Management
231237

232-
| RPC | Description |
233-
|-----------------------|------------------------------------------------------------------------|
234-
| `OpenChannel` | Open a new outbound channel (with optional push amount and fee config) |
235-
| `CloseChannel` | Cooperatively close a channel |
236-
| `ForceCloseChannel` | Force-close a channel unilaterally |
237-
| `SpliceIn` | Add on-chain funds to an existing channel |
238-
| `SpliceOut` | Remove funds from a channel back on-chain |
239-
| `UpdateChannelConfig` | Update forwarding fees and CLTV expiry delta |
240-
| `ListChannels` | List all channels with balances and configuration |
238+
| RPC | Description |
239+
| ----------------------- | ---------------------------------------------------------------------- |
240+
| `OpenChannel` | Open a new outbound channel (with optional push amount and fee config) |
241+
| `CloseChannel` | Cooperatively close a channel |
242+
| `ForceCloseChannel` | Force-close a channel unilaterally |
243+
| `SpliceIn` | Add on-chain funds to an existing channel |
244+
| `SpliceOut` | Remove funds from a channel back on-chain |
245+
| `BumpChannelFundingFee` | Raise the fee of a pending splice transaction |
246+
| `UpdateChannelConfig` | Update forwarding fees and CLTV expiry delta |
247+
| `ListChannels` | List all channels with balances and configuration |
248+
249+
> [!NOTE]
250+
> `BumpChannelFundingFee` supports pending splices only. It preserves the splice amount and
251+
> destination, and LDK Node selects the fee rate. General channel-opening fee bumps and
252+
> caller-selected splice fee rates are not supported.
253+
254+
Call it on the node that contributed to the splice, using the channel's `user_channel_id` and
255+
`counterparty_node_id`. A channel with no pending splice returns an error. An empty response means
256+
the fee bump has started; use [channel events](#event-streaming) to follow its progress.
257+
258+
The automatic increase can be below Bitcoin Core 29's minimum relay fee increase. Check that the
259+
replacement transaction reaches the mempool; a successful RPC does not guarantee relay or confirmation.
241260

242261
### Payment History
243262

‎e2e-tests/src/lib.rs‎

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ use std::io::{BufRead, BufReader, Write};
1111
use std::net::TcpListener;
1212
use std::path::{Path, PathBuf};
1313
use std::process::{Child, Command, Stdio};
14-
use std::time::{Duration, Instant};
14+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
1515

1616
use corepc_node::Node;
17+
use ldk_node::bitcoin::Amount;
1718
use ldk_server_client::client::{EventStream, LdkServerClient};
1819
use ldk_server_client::error::LdkServerErrorCode;
1920
use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
@@ -30,7 +31,7 @@ use ldk_server_grpc::types::{
3031
ClaimableAwaitingConfirmations, ForwardedPayment, LightningBalance, Payment, PaymentDirection,
3132
PaymentStatus,
3233
};
33-
use serde_json::Value;
34+
use serde_json::{json, Value};
3435

3536
const EVENT_TIMEOUT: Duration = Duration::from_secs(15);
3637

@@ -58,6 +59,8 @@ impl TestBitcoind {
5859

5960
fn with_extra_args(extra_args: &[&str]) -> Self {
6061
let mut conf = corepc_node::Conf::default();
62+
// Match the pinned LDK Node splice fixtures' 0.1 sat/vB relay fee increase.
63+
conf.args.push("-incrementalrelayfee=0.00000100");
6164
conf.args.extend_from_slice(extra_args);
6265

6366
let bitcoind = match std::env::var("BITCOIND_EXE") {
@@ -585,6 +588,17 @@ pub async fn wait_for_event(
585588
.expect("Timed out waiting for event")
586589
}
587590

591+
/// Wait for a negotiated splice and return its funding transaction ID.
592+
pub async fn splice_txid(events: &mut EventStream) -> String {
593+
let event = wait_for_event(events, |e| matches!(e, Event::SpliceNegotiated(_))).await;
594+
match event.event.unwrap() {
595+
Event::SpliceNegotiated(splice) => {
596+
splice.new_funding_txo.split(':').next().unwrap().to_string()
597+
},
598+
_ => unreachable!(),
599+
}
600+
}
601+
588602
/// Poll get_node_info until the server responds successfully.
589603
async fn wait_for_server_ready(handle: &LdkServerHandle, timeout: Duration) -> GetNodeInfoResponse {
590604
let start = std::time::Instant::now();
@@ -772,6 +786,81 @@ pub async fn mine_and_sync(
772786
}
773787
}
774788

789+
/// Wait for a transaction to enter the mempool and return its decoded details.
790+
pub async fn wait_for_transaction(bitcoind: &TestBitcoind, txid: &str) -> Value {
791+
tokio::time::timeout(Duration::from_secs(30), async {
792+
loop {
793+
let mempool: Vec<String> = bitcoind.bitcoind.client.call("getrawmempool", &[]).unwrap();
794+
if mempool.iter().any(|id| id == txid) {
795+
return bitcoind
796+
.bitcoind
797+
.client
798+
.call("getrawtransaction", &[json!(txid), json!(true)])
799+
.unwrap();
800+
}
801+
tokio::time::sleep(Duration::from_millis(100)).await;
802+
}
803+
})
804+
.await
805+
.expect("transaction did not enter the mempool")
806+
}
807+
808+
/// Wait for the on-chain wallet to complete another sync.
809+
///
810+
/// The pinned wallet records a replacement before its background sync sees the transaction.
811+
pub async fn wait_for_wallet_sync(server: &LdkServerHandle) {
812+
let after = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
813+
tokio::time::timeout(Duration::from_secs(30), async {
814+
loop {
815+
let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap();
816+
if info.latest_onchain_wallet_sync_timestamp.is_some_and(|timestamp| timestamp > after)
817+
{
818+
break;
819+
}
820+
tokio::time::sleep(Duration::from_millis(100)).await;
821+
}
822+
})
823+
.await
824+
.expect("wallet did not sync after the replacement");
825+
}
826+
827+
/// Wait for a transaction to appear in the node's payment history.
828+
pub async fn payment_for_tx(server: &LdkServerHandle, txid: &str) -> Payment {
829+
tokio::time::timeout(Duration::from_secs(30), async {
830+
loop {
831+
for payment in list_payments(server).await {
832+
if let Some(payment_kind::Kind::Onchain(onchain)) =
833+
payment.kind.as_ref().and_then(|kind| kind.kind.as_ref())
834+
{
835+
if onchain.txid == txid {
836+
return payment;
837+
}
838+
}
839+
}
840+
tokio::time::sleep(Duration::from_millis(100)).await;
841+
}
842+
})
843+
.await
844+
.expect("payment was not recorded")
845+
}
846+
847+
/// Check that a replacement preserves the recipient amount.
848+
pub async fn assert_replacement(
849+
bitcoind: &TestBitcoind, old: &str, new: &str, address: &str, amount: Amount,
850+
) {
851+
assert_ne!(old, new);
852+
let tx = wait_for_transaction(bitcoind, new).await;
853+
let recipient = tx["vout"]
854+
.as_array()
855+
.unwrap()
856+
.iter()
857+
.find(|output| output["scriptPubKey"]["address"] == address)
858+
.unwrap();
859+
assert_eq!(Amount::from_btc(recipient["value"].as_f64().unwrap()).unwrap(), amount);
860+
let mempool: Vec<String> = bitcoind.bitcoind.client.call("getrawmempool", &[]).unwrap();
861+
assert!(!mempool.iter().any(|id| id == old));
862+
}
863+
775864
/// Wait until the given client has at least one usable channel,
776865
/// periodically mining blocks to trigger chain sync.
777866
pub async fn wait_for_usable_channel(

0 commit comments

Comments
 (0)