Skip to content

Commit 3b3c2a5

Browse files
committed
Add checks on inputs: sufficient, fees, minor fixes
1 parent 1687505 commit 3b3c2a5

File tree

3 files changed

+198
-8
lines changed

3 files changed

+198
-8
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: 176 additions & 2 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;
@@ -4691,6 +4693,58 @@ fn estimate_v2_funding_transaction_fee(
46914693
fee_for_weight(funding_feerate_sat_per_1000_weight, weight)
46924694
}
46934695

4696+
/// Verify that the provided inputs by a counterparty to the funding transaction are enough
4697+
/// to cover the intended contribution amount *plus* the proportional fees of the counterparty.
4698+
/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain
4699+
/// the fees of the inputs, fees of the inputs weight, and for the initiator,
4700+
/// the fees of the common fields as well as the output and extra input weights.
4701+
/// Returns estimated (partial) fees as additional information
4702+
#[cfg(splicing)]
4703+
pub(super) fn check_v2_funding_inputs_sufficient(
4704+
contribution_amount: i64, funding_inputs: &[(TxIn, Transaction, Weight)], is_initiator: bool,
4705+
is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
4706+
) -> Result<u64, ChannelError> {
4707+
let mut total_input_witness_weight = Weight::from_wu(funding_inputs.iter().map(|(_, _, w)| w.to_wu()).sum());
4708+
if is_initiator && is_splice {
4709+
// consider the weight of the witness needed for spending the old funding transaction
4710+
total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT);
4711+
}
4712+
let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs.len(), total_input_witness_weight, funding_feerate_sat_per_1000_weight);
4713+
4714+
let mut total_input_sats = 0u64;
4715+
for (idx, input) in funding_inputs.iter().enumerate() {
4716+
if let Some(output) = input.1.output.get(input.0.previous_output.vout as usize) {
4717+
total_input_sats = total_input_sats.saturating_add(output.value.to_sat());
4718+
} else {
4719+
return Err(ChannelError::Warn(format!(
4720+
"Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]",
4721+
input.1.compute_txid(), input.0.previous_output.vout, idx
4722+
)));
4723+
}
4724+
}
4725+
4726+
// If the inputs are enough to cover intended contribution amount, with fees even when
4727+
// there is a change output, we are fine.
4728+
// If the inputs are less, but enough to cover intended contribution amount, with
4729+
// (lower) fees with no change, we are also fine (change will not be generated).
4730+
// So it's enough to check considering the lower, no-change fees.
4731+
//
4732+
// Note: dust limit is not relevant in this check.
4733+
//
4734+
// TODO(splicing): refine check including the fact wether a change will be added or not.
4735+
// Can be done once dual funding preparation is included.
4736+
4737+
let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64);
4738+
if (total_input_sats as i64) < minimal_input_amount_needed {
4739+
Err(ChannelError::Warn(format!(
4740+
"Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.",
4741+
total_input_sats, contribution_amount, estimated_fee,
4742+
)))
4743+
} else {
4744+
Ok(estimated_fee)
4745+
}
4746+
}
4747+
46944748
/// Context for dual-funded channels.
46954749
pub(super) struct DualFundingChannelContext {
46964750
/// The amount in satoshis we will be contributing to the channel.
@@ -8355,7 +8409,7 @@ impl<SP: Deref> FundedChannel<SP> where
83558409
/// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs).
83568410
#[cfg(splicing)]
83578411
pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64,
8358-
_our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
8412+
our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
83598413
funding_feerate_per_kw: u32, locktime: u32,
83608414
) -> Result<msgs::SpliceInit, APIError> {
83618415
// Check if a splice has been initiated already.
@@ -8389,6 +8443,13 @@ impl<SP: Deref> FundedChannel<SP> where
83898443
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
83908444
// (Cannot test for miminum required post-splice channel value)
83918445

8446+
// Check that inputs are sufficient to cover our contribution.
8447+
// Extra common weight is the weight for spending the old funding
8448+
let _fee = check_v2_funding_inputs_sufficient(our_funding_contribution_satoshis, &our_funding_inputs, true, true, funding_feerate_per_kw)
8449+
.map_err(|err| APIError::APIMisuseError { err: format!(
8450+
"Insufficient inputs for splicing; channel ID {}, err {}",
8451+
self.context.channel_id(), err,
8452+
)})?;
83928453

83938454
self.pending_splice_pre = Some(PendingSplice {
83948455
our_funding_contribution: our_funding_contribution_satoshis,
@@ -8440,7 +8501,8 @@ impl<SP: Deref> FundedChannel<SP> where
84408501

84418502
if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 {
84428503
return Err(ChannelError::Warn(format!(
8443-
"Splice-out not supported, only splice in, relative {} + {}",
8504+
"Splice-out not supported, only splice in, contribution is {} ({} + {})",
8505+
their_funding_contribution_satoshis + our_funding_contribution_satoshis,
84448506
their_funding_contribution_satoshis, our_funding_contribution_satoshis,
84458507
)));
84468508
}
@@ -11050,8 +11112,12 @@ mod tests {
1105011112
use bitcoin::constants::ChainHash;
1105111113
use bitcoin::script::{ScriptBuf, Builder};
1105211114
use bitcoin::transaction::{Transaction, TxOut, Version};
11115+
#[cfg(splicing)]
11116+
use bitcoin::transaction::TxIn;
1105311117
use bitcoin::opcodes;
1105411118
use bitcoin::network::Network;
11119+
#[cfg(splicing)]
11120+
use bitcoin::Weight;
1105511121
use crate::ln::onion_utils::INVALID_ONION_BLINDING;
1105611122
use crate::types::payment::{PaymentHash, PaymentPreimage};
1105711123
use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint};
@@ -12854,6 +12920,114 @@ mod tests {
1285412920
);
1285512921
}
1285612922

12923+
#[cfg(splicing)]
12924+
fn funding_input_sats(input_value_sats: u64) -> (TxIn, Transaction, Weight) {
12925+
use crate::sign::P2WPKH_WITNESS_WEIGHT;
12926+
12927+
let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() };
12928+
let input_1_prev_tx = Transaction {
12929+
input: vec![], output: vec![input_1_prev_out],
12930+
version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO,
12931+
};
12932+
let input_1_txin = TxIn {
12933+
previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 },
12934+
..Default::default()
12935+
};
12936+
(input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT))
12937+
}
12938+
12939+
#[cfg(splicing)]
12940+
#[test]
12941+
fn test_check_v2_funding_inputs_sufficient() {
12942+
use crate::ln::channel::check_v2_funding_inputs_sufficient;
12943+
12944+
// positive case, inputs well over intended contribution
12945+
assert_eq!(
12946+
check_v2_funding_inputs_sufficient(
12947+
220_000,
12948+
&[
12949+
funding_input_sats(200_000),
12950+
funding_input_sats(100_000),
12951+
],
12952+
true,
12953+
true,
12954+
2000,
12955+
).unwrap(),
12956+
1948,
12957+
);
12958+
12959+
// negative case, inputs clearly insufficient
12960+
{
12961+
let res = check_v2_funding_inputs_sufficient(
12962+
220_000,
12963+
&[
12964+
funding_input_sats(100_000),
12965+
],
12966+
true,
12967+
true,
12968+
2000,
12969+
);
12970+
assert_eq!(
12971+
format!("{:?}", res.err().unwrap()),
12972+
"Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1410. Need more inputs.",
12973+
);
12974+
}
12975+
12976+
// barely covers
12977+
{
12978+
let expected_fee: u64 = 1948;
12979+
assert_eq!(
12980+
check_v2_funding_inputs_sufficient(
12981+
(300_000 - expected_fee - 20) as i64,
12982+
&[
12983+
funding_input_sats(200_000),
12984+
funding_input_sats(100_000),
12985+
],
12986+
true,
12987+
true,
12988+
2000,
12989+
).unwrap(),
12990+
expected_fee,
12991+
);
12992+
}
12993+
12994+
// higher fee rate, does not cover
12995+
{
12996+
let res = check_v2_funding_inputs_sufficient(
12997+
298032,
12998+
&[
12999+
funding_input_sats(200_000),
13000+
funding_input_sats(100_000),
13001+
],
13002+
true,
13003+
true,
13004+
2200,
13005+
);
13006+
assert_eq!(
13007+
format!("{:?}", res.err().unwrap()),
13008+
"Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2143. Need more inputs.",
13009+
);
13010+
}
13011+
13012+
// barely covers, less fees (no extra weight, no init)
13013+
{
13014+
let expected_fee: u64 = 1076;
13015+
assert_eq!(
13016+
check_v2_funding_inputs_sufficient(
13017+
(300_000 - expected_fee - 20) as i64,
13018+
&[
13019+
funding_input_sats(200_000),
13020+
funding_input_sats(100_000),
13021+
],
13022+
false,
13023+
false,
13024+
2000,
13025+
).unwrap(),
13026+
expected_fee,
13027+
);
13028+
}
13029+
}
13030+
1285713031
#[cfg(splicing)]
1285813032
fn get_pre_and_post(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> (u64, u64) {
1285913033
use crate::ln::channel::PendingSplice;

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4303,16 +4303,11 @@ where
43034303
hash_map::Entry::Occupied(mut chan_phase_entry) => {
43044304
let locktime = locktime.unwrap_or(self.current_best_block().height);
43054305
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
4306-
let msg = match chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_per_kw, locktime) {
4307-
Ok(m) => m,
4308-
Err(e) => return Err(e),
4309-
};
4310-
4306+
let msg = chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_per_kw, locktime)?;
43114307
peer_state.pending_msg_events.push(events::MessageSendEvent::SendSpliceInit {
43124308
node_id: *counterparty_node_id,
43134309
msg,
43144310
});
4315-
43164311
Ok(())
43174312
} else {
43184313
Err(APIError::ChannelUnavailable {

0 commit comments

Comments
 (0)