Skip to content

Commit da75376

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 008b937 commit da75376

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
@@ -1259,7 +1259,7 @@ impl Wallet {
12591259

12601260
#[allow(deprecated)]
12611261
pub(crate) fn bump_fee_rbf(
1262-
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
1262+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>, cur_anchor_reserve_sats: u64,
12631263
) -> Result<Txid, Error> {
12641264
let payment = self.payment_store.get(&payment_id).ok_or_else(|| {
12651265
log_error!(self.logger, "Payment {} not found in payment store", payment_id);
@@ -1407,6 +1407,41 @@ impl Wallet {
14071407
}?
14081408
};
14091409

1410+
let old_fee_sats = locked_wallet
1411+
.calculate_fee(&old_tx)
1412+
.map_err(|e| {
1413+
log_error!(self.logger, "Failed to calculate fee of transaction {}: {}", txid, e);
1414+
Error::WalletOperationFailed
1415+
})?
1416+
.to_sat();
1417+
let replacement_fee_sats = locked_wallet
1418+
.calculate_fee(&psbt.unsigned_tx)
1419+
.map_err(|e| {
1420+
log_error!(
1421+
self.logger,
1422+
"Failed to calculate fee of replacement transaction for {}: {}",
1423+
txid,
1424+
e
1425+
);
1426+
Error::WalletOperationFailed
1427+
})?
1428+
.to_sat();
1429+
let additional_fee_sats = replacement_fee_sats.saturating_sub(old_fee_sats);
1430+
let balance = locked_wallet.balance();
1431+
let spendable_amount_sats =
1432+
self.get_balances_inner(balance, cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
1433+
if spendable_amount_sats < additional_fee_sats {
1434+
log_error!(
1435+
self.logger,
1436+
"Unable to bump fee due to insufficient reserve-preserving funds. \
1437+
Available: {}sats, required additional fee: {}sats, reserve: {}sats",
1438+
spendable_amount_sats,
1439+
additional_fee_sats,
1440+
cur_anchor_reserve_sats,
1441+
);
1442+
return Err(Error::InsufficientFunds);
1443+
}
1444+
14101445
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
14111446
Ok(finalized) => {
14121447
if !finalized {

tests/integration_tests_rust.rs

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

2923+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2924+
async fn onchain_fee_bump_rbf_respects_anchor_reserve() {
2925+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2926+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2927+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2928+
2929+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2930+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2931+
2932+
let premine_amount_sat = 1_000_000;
2933+
premine_and_distribute_funds(
2934+
&bitcoind.client,
2935+
&electrsd.client,
2936+
vec![addr_a.clone(), addr_b],
2937+
Amount::from_sat(premine_amount_sat),
2938+
)
2939+
.await;
2940+
2941+
node_a.sync_wallets().unwrap();
2942+
node_b.sync_wallets().unwrap();
2943+
2944+
open_channel(&node_b, &node_a, 200_000, false, &electrsd).await;
2945+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2946+
node_a.sync_wallets().unwrap();
2947+
node_b.sync_wallets().unwrap();
2948+
expect_channel_ready_event!(node_b, node_a.node_id());
2949+
2950+
let balances_before = node_b.list_balances();
2951+
let reserve = balances_before.total_anchor_channels_reserve_sats;
2952+
assert!(reserve > 0, "Anchor reserve should be non-zero after channel open");
2953+
let spendable_before = balances_before.spendable_onchain_balance_sats;
2954+
2955+
let buffer_sats = 5_000;
2956+
assert!(spendable_before > buffer_sats);
2957+
let amount_to_send_sats = spendable_before - buffer_sats;
2958+
let txid =
2959+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
2960+
wait_for_tx(&electrsd.client, txid).await;
2961+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
2962+
node_b.sync_wallets().unwrap();
2963+
2964+
let payment_id = PaymentId(txid.to_byte_array());
2965+
let high_fee_rate = bitcoin::FeeRate::from_sat_per_kwu(20_000);
2966+
assert_eq!(
2967+
Err(NodeError::InsufficientFunds),
2968+
node_b.onchain_payment().bump_fee_rbf(payment_id, Some(high_fee_rate))
2969+
);
2970+
}
2971+
29232972
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
29242973
async fn open_channel_with_all_with_anchors() {
29252974
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)