Skip to content

Commit c82c2e5

Browse files
committed
Preserve anchor reserve during RBF
RBF can spend fee increases from the original transaction's change output. Check the replacement fee increase against the current anchor-channel reserve before signing. This prevents high manual fee rates from consuming funds reserved for anchor spends. This finding was discovered by Project Loupe. Co-Authored-By: HAL 9000
1 parent 65ee795 commit c82c2e5

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/payment/onchain.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,18 @@ impl OnchainPayment {
134134
/// The new transaction will have the same outputs as the original but with a
135135
/// higher fee, resulting in faster confirmation potential.
136136
///
137+
/// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into
138+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`].
139+
///
137140
/// Returns the [`Txid`] of the new replacement transaction if successful.
141+
///
142+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats
138143
pub fn bump_fee_rbf(
139144
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
140145
) -> Result<Txid, Error> {
146+
let cur_anchor_reserve_sats =
147+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
141148
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
142-
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt)
149+
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt, cur_anchor_reserve_sats)
143150
}
144151
}

src/wallet/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ impl Wallet {
12321232

12331233
#[allow(deprecated)]
12341234
pub(crate) fn bump_fee_rbf(
1235-
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
1235+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>, cur_anchor_reserve_sats: u64,
12361236
) -> Result<Txid, Error> {
12371237
let payment = self.payment_store.get(&payment_id).ok_or_else(|| {
12381238
log_error!(self.logger, "Payment {} not found in payment store", payment_id);
@@ -1380,6 +1380,41 @@ impl Wallet {
13801380
}?
13811381
};
13821382

1383+
let old_fee_sats = locked_wallet
1384+
.calculate_fee(&old_tx)
1385+
.map_err(|e| {
1386+
log_error!(self.logger, "Failed to calculate fee of transaction {}: {}", txid, e);
1387+
Error::WalletOperationFailed
1388+
})?
1389+
.to_sat();
1390+
let replacement_fee_sats = locked_wallet
1391+
.calculate_fee(&psbt.unsigned_tx)
1392+
.map_err(|e| {
1393+
log_error!(
1394+
self.logger,
1395+
"Failed to calculate fee of replacement transaction for {}: {}",
1396+
txid,
1397+
e
1398+
);
1399+
Error::WalletOperationFailed
1400+
})?
1401+
.to_sat();
1402+
let additional_fee_sats = replacement_fee_sats.saturating_sub(old_fee_sats);
1403+
let balance = locked_wallet.balance();
1404+
let spendable_amount_sats =
1405+
self.get_balances_inner(balance, cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
1406+
if spendable_amount_sats < additional_fee_sats {
1407+
log_error!(
1408+
self.logger,
1409+
"Unable to bump fee due to insufficient reserve-preserving funds. \
1410+
Available: {}sats, required additional fee: {}sats, reserve: {}sats",
1411+
spendable_amount_sats,
1412+
additional_fee_sats,
1413+
cur_anchor_reserve_sats,
1414+
);
1415+
return Err(Error::InsufficientFunds);
1416+
}
1417+
13831418
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
13841419
Ok(finalized) => {
13851420
if !finalized {

tests/integration_tests_rust.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,55 @@ async fn onchain_fee_bump_rbf() {
30313031
assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded);
30323032
}
30333033

3034+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3035+
async fn onchain_fee_bump_rbf_respects_anchor_reserve() {
3036+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
3037+
let chain_source = random_chain_source(&bitcoind, &electrsd);
3038+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
3039+
3040+
let addr_a = node_a.onchain_payment().new_address().unwrap();
3041+
let addr_b = node_b.onchain_payment().new_address().unwrap();
3042+
3043+
let premine_amount_sat = 1_000_000;
3044+
premine_and_distribute_funds(
3045+
&bitcoind.client,
3046+
&electrsd.client,
3047+
vec![addr_a.clone(), addr_b],
3048+
Amount::from_sat(premine_amount_sat),
3049+
)
3050+
.await;
3051+
3052+
node_a.sync_wallets().unwrap();
3053+
node_b.sync_wallets().unwrap();
3054+
3055+
open_channel(&node_b, &node_a, 200_000, false, &electrsd).await;
3056+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
3057+
node_a.sync_wallets().unwrap();
3058+
node_b.sync_wallets().unwrap();
3059+
expect_channel_ready_event!(node_b, node_a.node_id());
3060+
3061+
let balances_before = node_b.list_balances();
3062+
let reserve = balances_before.total_anchor_channels_reserve_sats;
3063+
assert!(reserve > 0, "Anchor reserve should be non-zero after channel open");
3064+
let spendable_before = balances_before.spendable_onchain_balance_sats;
3065+
3066+
let buffer_sats = 5_000;
3067+
assert!(spendable_before > buffer_sats);
3068+
let amount_to_send_sats = spendable_before - buffer_sats;
3069+
let txid =
3070+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
3071+
wait_for_tx(&electrsd.client, txid).await;
3072+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
3073+
node_b.sync_wallets().unwrap();
3074+
3075+
let payment_id = PaymentId(txid.to_byte_array());
3076+
let high_fee_rate = bitcoin::FeeRate::from_sat_per_kwu(20_000);
3077+
assert_eq!(
3078+
Err(NodeError::InsufficientFunds),
3079+
node_b.onchain_payment().bump_fee_rbf(payment_id, Some(high_fee_rate.into()))
3080+
);
3081+
}
3082+
30343083
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
30353084
async fn open_channel_with_all_with_anchors() {
30363085
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)