Skip to content

Commit 98a46ac

Browse files
authored
Merge pull request #3452 from arik-so/stop-disbursement-underflow
Detect underflows in `build_closing_transaction`
2 parents 9728e51 + a652980 commit 98a46ac

File tree

1 file changed

+36
-20
lines changed

1 file changed

+36
-20
lines changed

lightning/src/ln/channel.rs

+36-20
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,7 @@ pub(super) struct ChannelContext<SP: Deref> where SP::Target: SignerProvider {
15011501
// The `next_funding_txid` field allows peers to finalize the signing steps of an interactive
15021502
// transaction construction, or safely abort that transaction if it was not signed by one of the
15031503
// peers, who has thus already removed it from its state.
1504-
//
1504+
//
15051505
// If we've sent `commtiment_signed` for an interactively constructed transaction
15061506
// during a signing session, but have not received `tx_signatures` we MUST set `next_funding_txid`
15071507
// to the txid of that interactive transaction, else we MUST NOT set it.
@@ -4351,7 +4351,7 @@ impl<SP: Deref> Channel<SP> where
43514351
}
43524352

43534353
#[inline]
4354-
fn build_closing_transaction(&self, proposed_total_fee_satoshis: u64, skip_remote_output: bool) -> (ClosingTransaction, u64) {
4354+
fn build_closing_transaction(&self, proposed_total_fee_satoshis: u64, skip_remote_output: bool) -> Result<(ClosingTransaction, u64), ChannelError> {
43554355
assert!(self.context.pending_inbound_htlcs.is_empty());
43564356
assert!(self.context.pending_outbound_htlcs.is_empty());
43574357
assert!(self.context.pending_update_fee.is_none());
@@ -4368,10 +4368,18 @@ impl<SP: Deref> Channel<SP> where
43684368
total_fee_satoshis += (-value_to_counterparty) as u64;
43694369
}
43704370

4371+
debug_assert!(value_to_counterparty >= 0);
4372+
if value_to_counterparty < 0 {
4373+
return Err(ChannelError::close(format!("Value to counterparty below 0: {}", value_to_counterparty)))
4374+
}
43714375
if skip_remote_output || value_to_counterparty as u64 <= self.context.holder_dust_limit_satoshis {
43724376
value_to_counterparty = 0;
43734377
}
43744378

4379+
debug_assert!(value_to_holder >= 0);
4380+
if value_to_holder < 0 {
4381+
return Err(ChannelError::close(format!("Value to holder below 0: {}", value_to_holder)))
4382+
}
43754383
if value_to_holder as u64 <= self.context.holder_dust_limit_satoshis {
43764384
value_to_holder = 0;
43774385
}
@@ -4382,7 +4390,7 @@ impl<SP: Deref> Channel<SP> where
43824390
let funding_outpoint = self.funding_outpoint().into_bitcoin_outpoint();
43834391

43844392
let closing_transaction = ClosingTransaction::new(value_to_holder as u64, value_to_counterparty as u64, holder_shutdown_script, counterparty_shutdown_script, funding_outpoint);
4385-
(closing_transaction, total_fee_satoshis)
4393+
Ok((closing_transaction, total_fee_satoshis))
43864394
}
43874395

43884396
fn funding_outpoint(&self) -> OutPoint {
@@ -6136,19 +6144,27 @@ impl<SP: Deref> Channel<SP> where
61366144
if let Some((fee, skip_remote_output, fee_range, holder_sig)) = self.context.last_sent_closing_fee.clone() {
61376145
debug_assert!(holder_sig.is_none());
61386146
log_trace!(logger, "Attempting to generate pending closing_signed...");
6139-
let (closing_tx, fee) = self.build_closing_transaction(fee, skip_remote_output);
6140-
let closing_signed = self.get_closing_signed_msg(&closing_tx, skip_remote_output,
6141-
fee, fee_range.min_fee_satoshis, fee_range.max_fee_satoshis, logger);
6142-
let signed_tx = if let (Some(ClosingSigned { signature, .. }), Some(counterparty_sig)) =
6143-
(closing_signed.as_ref(), self.context.last_received_closing_sig) {
6144-
let funding_redeemscript = self.context.get_funding_redeemscript();
6145-
let sighash = closing_tx.trust().get_sighash_all(&funding_redeemscript, self.context.channel_value_satoshis);
6146-
debug_assert!(self.context.secp_ctx.verify_ecdsa(&sighash, &counterparty_sig,
6147-
&self.context.get_counterparty_pubkeys().funding_pubkey).is_ok());
6148-
Some(self.build_signed_closing_transaction(&closing_tx, &counterparty_sig, signature))
6149-
} else { None };
6150-
let shutdown_result = signed_tx.as_ref().map(|_| self.shutdown_result_coop_close());
6151-
(closing_signed, signed_tx, shutdown_result)
6147+
let closing_transaction_result = self.build_closing_transaction(fee, skip_remote_output);
6148+
match closing_transaction_result {
6149+
Ok((closing_tx, fee)) => {
6150+
let closing_signed = self.get_closing_signed_msg(&closing_tx, skip_remote_output,
6151+
fee, fee_range.min_fee_satoshis, fee_range.max_fee_satoshis, logger);
6152+
let signed_tx = if let (Some(ClosingSigned { signature, .. }), Some(counterparty_sig)) =
6153+
(closing_signed.as_ref(), self.context.last_received_closing_sig) {
6154+
let funding_redeemscript = self.context.get_funding_redeemscript();
6155+
let sighash = closing_tx.trust().get_sighash_all(&funding_redeemscript, self.context.channel_value_satoshis);
6156+
debug_assert!(self.context.secp_ctx.verify_ecdsa(&sighash, &counterparty_sig,
6157+
&self.context.get_counterparty_pubkeys().funding_pubkey).is_ok());
6158+
Some(self.build_signed_closing_transaction(&closing_tx, &counterparty_sig, signature))
6159+
} else { None };
6160+
let shutdown_result = signed_tx.as_ref().map(|_| self.shutdown_result_coop_close());
6161+
(closing_signed, signed_tx, shutdown_result)
6162+
}
6163+
Err(err) => {
6164+
let shutdown = self.context.force_shutdown(true, ClosureReason::ProcessingError {err: err.to_string()});
6165+
(None, None, Some(shutdown))
6166+
}
6167+
}
61526168
} else { (None, None, None) }
61536169
} else { (None, None, None) };
61546170

@@ -6616,7 +6632,7 @@ impl<SP: Deref> Channel<SP> where
66166632
let (our_min_fee, our_max_fee) = self.calculate_closing_fee_limits(fee_estimator);
66176633

66186634
assert!(self.context.shutdown_scriptpubkey.is_some());
6619-
let (closing_tx, total_fee_satoshis) = self.build_closing_transaction(our_min_fee, false);
6635+
let (closing_tx, total_fee_satoshis) = self.build_closing_transaction(our_min_fee, false)?;
66206636
log_trace!(logger, "Proposing initial closing_signed for our counterparty with a fee range of {}-{} sat (with initial proposal {} sats)",
66216637
our_min_fee, our_max_fee, total_fee_satoshis);
66226638

@@ -6850,7 +6866,7 @@ impl<SP: Deref> Channel<SP> where
68506866

68516867
let funding_redeemscript = self.context.get_funding_redeemscript();
68526868
let mut skip_remote_output = false;
6853-
let (mut closing_tx, used_total_fee) = self.build_closing_transaction(msg.fee_satoshis, skip_remote_output);
6869+
let (mut closing_tx, used_total_fee) = self.build_closing_transaction(msg.fee_satoshis, skip_remote_output)?;
68546870
if used_total_fee != msg.fee_satoshis {
68556871
return Err(ChannelError::close(format!("Remote sent us a closing_signed with a fee other than the value they can claim. Fee in message: {}. Actual closing tx fee: {}", msg.fee_satoshis, used_total_fee)));
68566872
}
@@ -6862,7 +6878,7 @@ impl<SP: Deref> Channel<SP> where
68626878
// The remote end may have decided to revoke their output due to inconsistent dust
68636879
// limits, so check for that case by re-checking the signature here.
68646880
skip_remote_output = true;
6865-
closing_tx = self.build_closing_transaction(msg.fee_satoshis, skip_remote_output).0;
6881+
closing_tx = self.build_closing_transaction(msg.fee_satoshis, skip_remote_output)?.0;
68666882
let sighash = closing_tx.trust().get_sighash_all(&funding_redeemscript, self.context.channel_value_satoshis);
68676883
secp_check!(self.context.secp_ctx.verify_ecdsa(&sighash, &msg.signature, self.context.counterparty_funding_pubkey()), "Invalid closing tx signature from peer".to_owned());
68686884
},
@@ -6893,7 +6909,7 @@ impl<SP: Deref> Channel<SP> where
68936909
(closing_tx, $new_fee)
68946910
} else {
68956911
skip_remote_output = false;
6896-
self.build_closing_transaction($new_fee, skip_remote_output)
6912+
self.build_closing_transaction($new_fee, skip_remote_output)?
68976913
};
68986914

68996915
let closing_signed = self.get_closing_signed_msg(&closing_tx, skip_remote_output, used_fee, our_min_fee, our_max_fee, logger);

0 commit comments

Comments
 (0)