Skip to content

Commit d6d82d8

Browse files
committed
Add checks on inputs: sufficient, reserve, fees
1 parent 32ba447 commit d6d82d8

File tree

2 files changed

+250
-18
lines changed

2 files changed

+250
-18
lines changed

lightning/src/ln/chan_utils.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,27 @@ pub const HTLC_TIMEOUT_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 288;
8181
/// outputs.
8282
pub const HTLC_SUCCESS_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 327;
8383

84+
/// The size of the 2-of-2 multisig script
85+
const MULTISIG_SCRIPT_SIZE: u64 =
86+
1 + // OP_2
87+
1 + // data len
88+
33 + // pubkey1
89+
1 + // data len
90+
33 + // pubkey2
91+
1 + // OP_2
92+
1; // OP_CHECKMULTISIG
93+
/// The weight of a funding transaction input (2-of-2 P2WSH)
94+
/// See https://github.com/lightning/bolts/blob/master/03-transactions.md#expected-weight-of-the-commitment-transaction
95+
pub const FUNDING_TRANSACTION_WITNESS_WEIGHT: u64 =
96+
1 + // number_of_witness_elements
97+
1 + // nil_len
98+
1 + // sig len
99+
73 + // sig1
100+
1 + // sig len
101+
73 + // sig2
102+
1 + // witness_script_length
103+
MULTISIG_SCRIPT_SIZE;
104+
84105
/// Gets the weight for an HTLC-Success transaction.
85106
#[inline]
86107
pub fn htlc_success_tx_weight(channel_type_features: &ChannelTypeFeatures) -> u64 {

lightning/src/ln/channel.rs

Lines changed: 229 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ use crate::ln::chan_utils::{
4747
get_commitment_transaction_number_obscure_factor,
4848
ClosingTransaction, commit_tx_fee_sat,
4949
};
50+
#[cfg(splicing)]
51+
use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
5052
use crate::ln::chan_utils;
5153
use crate::ln::onion_utils::HTLCFailReason;
5254
use crate::chain::BestBlock;
@@ -4093,28 +4095,63 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
40934095
}
40944096
}
40954097

4096-
/// Check that a balance value meets the channel reserve requirements or violates them (below reserve).
4097-
/// The channel value is an input as opposed to using from self, so that this can be used in case of splicing
4098-
/// to checks with new channel value (before being comitted to it).
4098+
/// Check a balance against a channel reserver requirement
40994099
#[cfg(splicing)]
4100-
pub fn check_balance_meets_reserve_requirements(&self, balance: u64, channel_value: u64) -> Result<(), ChannelError> {
4100+
pub fn check_balance_meets_reserve_requirement(balance: u64, channel_value: u64, dust_limit: u64) -> (bool, u64) {
4101+
let channel_reserve = get_v2_channel_reserve_satoshis(channel_value, dust_limit);
41014102
if balance == 0 {
4102-
return Ok(());
4103+
// 0 balance is fine
4104+
(true, channel_reserve)
4105+
} else {
4106+
((balance >= channel_reserve), channel_reserve)
41034107
}
4104-
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
4105-
channel_value, self.holder_dust_limit_satoshis);
4106-
if balance < holder_selected_channel_reserve_satoshis {
4108+
}
4109+
4110+
/// Check that post-splicing balance meets reserver requirements, but only if it met it pre-splice as well
4111+
#[cfg(splicing)]
4112+
pub fn check_splice_balance_meets_v2_reserve_requirement_noerr(pre_balance: u64, post_balance: u64, pre_channel_value: u64, post_channel_value: u64, dust_limit: u64) -> (bool, u64) {
4113+
match Self::check_balance_meets_reserve_requirement(
4114+
post_balance, post_channel_value, dust_limit
4115+
) {
4116+
(true, channel_reserve) => (true, channel_reserve),
4117+
(false, channel_reserve) =>
4118+
// post is not OK, check pre
4119+
match Self::check_balance_meets_reserve_requirement(
4120+
pre_balance, pre_channel_value, dust_limit
4121+
) {
4122+
(true, _) =>
4123+
// pre OK, post not -> not
4124+
(false, channel_reserve),
4125+
(false, _) =>
4126+
// post not OK, but so was pre -> OK
4127+
(true, channel_reserve),
4128+
}
4129+
}
4130+
}
4131+
4132+
/// Check that balances meet the channel reserve requirements or violates them (below reserve).
4133+
/// The channel value is an input as opposed to using from self, so that this can be used in case of splicing
4134+
/// to check with new channel value (before being comitted to it).
4135+
#[cfg(splicing)]
4136+
pub fn check_splice_balances_meet_v2_reserve_requirements(&self, self_balance_pre: u64, self_balance_post: u64, counterparty_balance_pre: u64, counterparty_balance_post: u64, channel_value_pre: u64, channel_value_post: u64) -> Result<(), ChannelError> {
4137+
let (is_ok, channel_reserve_self) = Self::check_splice_balance_meets_v2_reserve_requirement_noerr(
4138+
self_balance_pre, self_balance_post, channel_value_pre, channel_value_post,
4139+
self.holder_dust_limit_satoshis
4140+
);
4141+
if !is_ok {
41074142
return Err(ChannelError::Warn(format!(
4108-
"Balance below reserve mandated by holder, {} vs {}",
4109-
balance, holder_selected_channel_reserve_satoshis,
4143+
"Balance below reserve, mandated by holder, {} vs {}",
4144+
self_balance_post, channel_reserve_self,
41104145
)));
41114146
}
4112-
let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
4113-
channel_value, self.counterparty_dust_limit_satoshis);
4114-
if balance < counterparty_selected_channel_reserve_satoshis {
4147+
let (is_ok, channel_reserve_cp) = Self::check_splice_balance_meets_v2_reserve_requirement_noerr(
4148+
counterparty_balance_pre, counterparty_balance_post, channel_value_pre, channel_value_post,
4149+
self.counterparty_dust_limit_satoshis
4150+
);
4151+
if !is_ok {
41154152
return Err(ChannelError::Warn(format!(
41164153
"Balance below reserve mandated by counterparty, {} vs {}",
4117-
balance, counterparty_selected_channel_reserve_satoshis,
4154+
counterparty_balance_post, channel_reserve_cp,
41184155
)));
41194156
}
41204157
Ok(())
@@ -4676,6 +4713,58 @@ fn estimate_v2_funding_transaction_fee(
46764713
fee_for_weight(funding_feerate_sat_per_1000_weight, weight)
46774714
}
46784715

4716+
/// Verify that the provided inputs by a counterparty to the funding transaction are enough
4717+
/// to cover the intended contribution amount *plus* the proportional fees of the counterparty.
4718+
/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain
4719+
/// the fees of the inputs, fees of the inputs weight, and for the initiator,
4720+
/// the fees of the common fields as well as the output and extra input weights.
4721+
/// Returns estimated (partial) fees as additional information
4722+
#[cfg(splicing)]
4723+
pub(super) fn check_v2_funding_inputs_sufficient(
4724+
contribution_amount: i64, funding_inputs: &[(TxIn, Transaction, Weight)], is_initiator: bool,
4725+
is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
4726+
) -> Result<u64, ChannelError> {
4727+
let mut total_input_witness_weight = Weight::from_wu(funding_inputs.iter().map(|(_, _, w)| w.to_wu()).sum());
4728+
if is_initiator && is_splice {
4729+
// consider the weight of the witness needed for spending the old funding transaction
4730+
total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT);
4731+
}
4732+
let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs.len(), total_input_witness_weight, funding_feerate_sat_per_1000_weight);
4733+
4734+
let mut total_input_sats = 0u64;
4735+
for (idx, input) in funding_inputs.iter().enumerate() {
4736+
if let Some(output) = input.1.output.get(input.0.previous_output.vout as usize) {
4737+
total_input_sats = total_input_sats.saturating_add(output.value.to_sat());
4738+
} else {
4739+
return Err(ChannelError::Warn(format!(
4740+
"Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]",
4741+
input.1.compute_txid(), input.0.previous_output.vout, idx
4742+
)));
4743+
}
4744+
}
4745+
4746+
// If the inputs are enough to cover intended contribution amount, with fees even when
4747+
// there is a change output, we are fine.
4748+
// If the inputs are less, but enough to cover intended contribution amount, with
4749+
// (lower) fees with no change, we are also fine (change will not be generated).
4750+
// So it's enough to check considering the lower, no-change fees.
4751+
//
4752+
// Note: dust limit is not relevant in this check.
4753+
//
4754+
// TODO(splicing): refine check including the fact wether a change will be added or not.
4755+
// Can be done once dual funding preparation is included.
4756+
4757+
let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64);
4758+
if (total_input_sats as i64) < minimal_input_amount_needed {
4759+
Err(ChannelError::Warn(format!(
4760+
"Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.",
4761+
total_input_sats, contribution_amount, estimated_fee,
4762+
)))
4763+
} else {
4764+
Ok(estimated_fee)
4765+
}
4766+
}
4767+
46794768
/// Context for dual-funded channels.
46804769
pub(super) struct DualFundingChannelContext {
46814770
/// The amount in satoshis we will be contributing to the channel.
@@ -8337,7 +8426,7 @@ impl<SP: Deref> FundedChannel<SP> where
83378426
/// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs).
83388427
#[cfg(splicing)]
83398428
pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64,
8340-
_our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
8429+
our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
83418430
funding_feerate_per_kw: u32, locktime: u32,
83428431
) -> Result<msgs::SpliceInit, APIError> {
83438432
// Check if a splice has been initiated already.
@@ -8371,6 +8460,13 @@ impl<SP: Deref> FundedChannel<SP> where
83718460
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
83728461
// (Cannot test for miminum required post-splice channel value)
83738462

8463+
// Check that inputs are sufficient to cover our contribution.
8464+
// Extra common weight is the weight for spending the old funding
8465+
let _fee = check_v2_funding_inputs_sufficient(our_funding_contribution_satoshis, &our_funding_inputs, true, true, funding_feerate_per_kw)
8466+
.map_err(|err| APIError::APIMisuseError { err: format!(
8467+
"Insufficient inputs for splicing; channel ID {}, err {}",
8468+
self.context.channel_id(), err,
8469+
)})?;
83748470

83758471
self.pending_splice_pre = Some(PendingSplice {
83768472
our_funding_contribution: our_funding_contribution_satoshis,
@@ -8472,10 +8568,13 @@ impl<SP: Deref> FundedChannel<SP> where
84728568

84738569
let pre_channel_value = self.funding.get_value_satoshis();
84748570
let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis);
8475-
let post_balance = PendingSplice::add_checked(self.funding.value_to_self_msat, our_funding_contribution);
8476-
// Early check for reserve requirement, assuming maximum balance of full channel value
8571+
let pre_balance_self = self.funding.value_to_self_msat;
8572+
let post_balance_self = PendingSplice::add_checked(pre_balance_self, our_funding_contribution);
8573+
let pre_balance_counterparty = pre_channel_value.saturating_sub(pre_balance_self);
8574+
let post_balance_counterparty = post_channel_value.saturating_sub(post_balance_self);
8575+
// Pre-check for reserve requirement
84778576
// This will also be checked later at tx_complete
8478-
let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?;
8577+
let _res = self.context.check_splice_balances_meet_v2_reserve_requirements(pre_balance_self, post_balance_self, pre_balance_counterparty, post_balance_counterparty, pre_channel_value, post_channel_value)?;
84798578
Ok(())
84808579
}
84818580

@@ -11067,8 +11166,12 @@ mod tests {
1106711166
use bitcoin::constants::ChainHash;
1106811167
use bitcoin::script::{ScriptBuf, Builder};
1106911168
use bitcoin::transaction::{Transaction, TxOut, Version};
11169+
#[cfg(splicing)]
11170+
use bitcoin::transaction::TxIn;
1107011171
use bitcoin::opcodes;
1107111172
use bitcoin::network::Network;
11173+
#[cfg(splicing)]
11174+
use bitcoin::Weight;
1107211175
use crate::ln::onion_utils::INVALID_ONION_BLINDING;
1107311176
use crate::types::payment::{PaymentHash, PaymentPreimage};
1107411177
use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint};
@@ -12875,6 +12978,114 @@ mod tests {
1287512978
);
1287612979
}
1287712980

12981+
#[cfg(splicing)]
12982+
fn funding_input_sats(input_value_sats: u64) -> (TxIn, Transaction, Weight) {
12983+
use crate::sign::P2WPKH_WITNESS_WEIGHT;
12984+
12985+
let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() };
12986+
let input_1_prev_tx = Transaction {
12987+
input: vec![], output: vec![input_1_prev_out],
12988+
version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO,
12989+
};
12990+
let input_1_txin = TxIn {
12991+
previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 },
12992+
..Default::default()
12993+
};
12994+
(input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT))
12995+
}
12996+
12997+
#[cfg(splicing)]
12998+
#[test]
12999+
fn test_check_v2_funding_inputs_sufficient() {
13000+
use crate::ln::channel::check_v2_funding_inputs_sufficient;
13001+
13002+
// positive case, inputs well over intended contribution
13003+
assert_eq!(
13004+
check_v2_funding_inputs_sufficient(
13005+
220_000,
13006+
&[
13007+
funding_input_sats(200_000),
13008+
funding_input_sats(100_000),
13009+
],
13010+
true,
13011+
true,
13012+
2000,
13013+
).unwrap(),
13014+
1948,
13015+
);
13016+
13017+
// negative case, inputs clearly insufficient
13018+
{
13019+
let res = check_v2_funding_inputs_sufficient(
13020+
220_000,
13021+
&[
13022+
funding_input_sats(100_000),
13023+
],
13024+
true,
13025+
true,
13026+
2000,
13027+
);
13028+
assert_eq!(
13029+
format!("{:?}", res.err().unwrap()),
13030+
"Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1410. Need more inputs.",
13031+
);
13032+
}
13033+
13034+
// barely covers
13035+
{
13036+
let expected_fee: u64 = 1948;
13037+
assert_eq!(
13038+
check_v2_funding_inputs_sufficient(
13039+
(300_000 - expected_fee - 20) as i64,
13040+
&[
13041+
funding_input_sats(200_000),
13042+
funding_input_sats(100_000),
13043+
],
13044+
true,
13045+
true,
13046+
2000,
13047+
).unwrap(),
13048+
expected_fee,
13049+
);
13050+
}
13051+
13052+
// higher fee rate, does not cover
13053+
{
13054+
let res = check_v2_funding_inputs_sufficient(
13055+
298032,
13056+
&[
13057+
funding_input_sats(200_000),
13058+
funding_input_sats(100_000),
13059+
],
13060+
true,
13061+
true,
13062+
2200,
13063+
);
13064+
assert_eq!(
13065+
format!("{:?}", res.err().unwrap()),
13066+
"Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2143. Need more inputs.",
13067+
);
13068+
}
13069+
13070+
// barely covers, less fees (no extra weight, no init)
13071+
{
13072+
let expected_fee: u64 = 1076;
13073+
assert_eq!(
13074+
check_v2_funding_inputs_sufficient(
13075+
(300_000 - expected_fee - 20) as i64,
13076+
&[
13077+
funding_input_sats(200_000),
13078+
funding_input_sats(100_000),
13079+
],
13080+
false,
13081+
false,
13082+
2000,
13083+
).unwrap(),
13084+
expected_fee,
13085+
);
13086+
}
13087+
}
13088+
1287813089
#[cfg(splicing)]
1287913090
fn get_pre_and_post(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> (u64, u64) {
1288013091
use crate::ln::channel::PendingSplice;

0 commit comments

Comments
 (0)