Split splice initiation into two phases
What changed, and why it matters
This commit refactors how Lightning channel 'splicing' is started. Instead of requiring the user to manually pick coins and hand them to the API, it splits the process into two steps: first the library returns a 'funding template', then the user's wallet automatically selects coins through a standard trait and the result is handed back. The change is mostly an API usability improvement and moves fee-estimation/validation code into a dedicated module. There is no direct evidence in the commit that it fixes a security vulnerability; it is a design/API change.
Treat as a normal API refactor. Reviewers should verify that the new two-phase flow does not introduce state-machine bugs (e.g., splice_channel without a following funding_contributed leaving a channel in a stale state), that the moved fee/validation logic preserves all prior checks, and that serialization backward compatibility with LegacySplice works correctly across upgrades. No urgent security action is indicated by the commit itself.
Security signals we found
Large refactor of funding/splice API surface
Moves fee estimation and input validation to co-located module
Adds new serialization variant for in-flight quiescent splice state
Changes error handling path for funding contribution failures (SpliceFundingFailed event)
No mention of vulnerability, CVE, bug fix, or security issue in commit message
Evidence from the diff
The patch renames SpliceContribution to FundingContribution, introduces FundingTemplate returned by ChannelManager::splice_channel, and adds ChannelManager::funding_contributed to begin quiescence/negotiation. Coin selection is now performed via the existing CoinSelectionSource trait (sync and async variants) rather than by the caller. Fee estimation and input-validation logic are relocated from ln/channel.rs to ln/funding.rs. Serialization of QuiescentAction is updated to support a new Splice variant while keeping a LegacySplice variant for backward compatibility. Tests and fuzz harnesses are updated to the new two-phase API. No security bug or exploit primitive is described in the commit message or diff.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/funding.rslightning/src/ln/splicing_tests.rslightning/src/ln/async_signer_tests.rslightning/src/ln/functional_test_utils.rslightning/src/util/ser.rsfuzz/src/chanmon_consistency.rsfuzz/src/full_stack.rslightning-tests/src/upgrade_downgrade_tests.rsInspect captured patch +1660 / −1090
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index bb80004..70dda13 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -26,6 +26,7 @@ use bitcoin::opcodes;
use bitcoin::script::{Builder, ScriptBuf};
use bitcoin::transaction::Version;
use bitcoin::transaction::{Transaction, TxOut};
+use bitcoin::FeeRate;
use bitcoin::hash_types::BlockHash;
use bitcoin::hashes::sha256::Hash as Sha256;
@@ -45,6 +46,7 @@ use lightning::chain::{
chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch,
};
use lightning::events;
+use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
use lightning::ln::channel::{
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS,
};
@@ -53,7 +55,6 @@ use lightning::ln::channelmanager::{
ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails,
};
use lightning::ln::functional_test_utils::*;
-use lightning::ln::funding::{FundingTxInput, SpliceContribution};
use lightning::ln::inbound_payment::ExpandedKey;
use lightning::ln::msgs::{
BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, Init, MessageSendEvent,
@@ -72,12 +73,14 @@ use lightning::sign::{
SignerProvider,
};
use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
+use lightning::util::async_poll::{MaybeSend, MaybeSync};
use lightning::util::config::UserConfig;
use lightning::util::errors::APIError;
use lightning::util::hash_tables::*;
use lightning::util::logger::Logger;
use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer};
use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner};
+use lightning::util::test_utils::TestWalletSource;
use lightning_invoice::RawBolt11Invoice;
@@ -176,63 +179,6 @@ impl Writer for VecWriter {
}
}
-pub struct TestWallet {
- secret_key: SecretKey,
- utxos: Mutex<Vec<lightning::events::bump_transaction::Utxo>>,
- secp: Secp256k1<bitcoin::secp256k1::All>,
-}
-
-impl TestWallet {
- pub fn new(secret_key: SecretKey) -> Self {
- Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() }
- }
-
- fn get_change_script(&self) -> Result<ScriptBuf, ()> {
- let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp));
- Ok(ScriptBuf::new_p2wpkh(&public_key.wpubkey_hash().unwrap()))
- }
-
- pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, value: Amount) -> TxOut {
- let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp));
- let utxo = lightning::events::bump_transaction::Utxo::new_v0_p2wpkh(
- outpoint,
- value,
- &public_key.wpubkey_hash().unwrap(),
- );
- self.utxos.lock().unwrap().push(utxo.clone());
- utxo.output
- }
-
- pub fn sign_tx(
- &self, mut tx: Transaction,
- ) -> Result<Transaction, bitcoin::sighash::P2wpkhError> {
- let utxos = self.utxos.lock().unwrap();
- for i in 0..tx.input.len() {
- if let Some(utxo) =
- utxos.iter().find(|utxo| utxo.outpoint == tx.input[i].previous_output)
- {
- let sighash = bitcoin::sighash::SighashCache::new(&tx).p2wpkh_signature_hash(
- i,
- &utxo.output.script_pubkey,
- utxo.output.value,
- bitcoin::EcdsaSighashType::All,
- )?;
- let signature = self.secp.sign_ecdsa(
- &secp256k1::Message::from_digest(sighash.to_byte_array()),
- &self.secret_key,
- );
- let bitcoin_sig = bitcoin::ecdsa::Signature {
- signature,
- sighash_type: bitcoin::EcdsaSighashType::All,
- };
- tx.input[i].witness =
- bitcoin::Witness::p2wpkh(&bitcoin_sig, &self.secret_key.public_key(&self.secp));
- }
- }
- Ok(tx)
- }
-}
-
/// The LDK API requires that any time we tell it we're done persisting a `ChannelMonitor[Update]`
/// we never pass it in as the "latest" `ChannelMonitor` on startup. However, we can pass
/// out-of-date monitors as long as we never told LDK we finished persisting them, which we do by
@@ -542,7 +488,7 @@ type ChanMan<'a> = ChannelManager<
Arc<FuzzEstimator>,
&'a FuzzRouter,
&'a FuzzRouter,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
>;
#[inline]
@@ -778,7 +724,9 @@ fn send_mpp_hop_payment(
}
#[inline]
-pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
+pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
+ data: &[u8], underlying_out: Out, anchors: bool,
+) {
let out = SearchingOutput::new(underlying_out);
let broadcast = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) });
let router = FuzzRouter {};
@@ -805,7 +753,7 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
macro_rules! make_node {
($node_id: expr, $fee_estimator: expr) => {{
- let logger: Arc<dyn Logger> =
+ let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
Arc::new(test_logger::TestLogger::new($node_id.to_string(), out.clone()));
let node_secret = SecretKey::from_slice(&[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -854,6 +802,7 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
),
monitor,
keys_manager,
+ logger,
)
}};
}
@@ -865,7 +814,7 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
keys,
fee_estimator| {
let keys_manager = Arc::clone(keys);
- let logger: Arc<dyn Logger> =
+ let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
Arc::new(test_logger::TestLogger::new(node_id.to_string(), out.clone()));
let chain_monitor = Arc::new(TestChainMonitor::new(
broadcast.clone(),
@@ -1159,9 +1108,9 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
}};
}
- let wallet_a = TestWallet::new(SecretKey::from_slice(&[1; 32]).unwrap());
- let wallet_b = TestWallet::new(SecretKey::from_slice(&[2; 32]).unwrap());
- let wallet_c = TestWallet::new(SecretKey::from_slice(&[3; 32]).unwrap());
+ let wallet_a = TestWalletSource::new(SecretKey::from_slice(&[1; 32]).unwrap());
+ let wallet_b = TestWalletSource::new(SecretKey::from_slice(&[2; 32]).unwrap());
+ let wallet_c = TestWalletSource::new(SecretKey::from_slice(&[3; 32]).unwrap());
let wallets = vec![wallet_a, wallet_b, wallet_c];
let coinbase_tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
@@ -1175,12 +1124,8 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
})
.collect(),
};
- let coinbase_txid = coinbase_tx.compute_txid();
wallets.iter().enumerate().for_each(|(i, w)| {
- w.add_utxo(
- bitcoin::OutPoint { txid: coinbase_txid, vout: i as u32 },
- Amount::from_sat(100_000),
- );
+ w.add_utxo(coinbase_tx.clone(), i as u32);
});
let fee_est_a = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) });
@@ -1192,11 +1137,13 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
// 3 nodes is enough to hit all the possible cases, notably unknown-source-unknown-dest
// forwarding.
- let (node_a, mut monitor_a, keys_manager_a) = make_node!(0, fee_est_a);
- let (node_b, mut monitor_b, keys_manager_b) = make_node!(1, fee_est_b);
- let (node_c, mut monitor_c, keys_manager_c) = make_node!(2, fee_est_c);
+ let (node_a, mut monitor_a, keys_manager_a, logger_a) = make_node!(0, fee_est_a);
+ let (node_b, mut monitor_b, keys_manager_b, logger_b) = make_node!(1, fee_est_b);
+ let (node_c, mut monitor_c, keys_manager_c, logger_c) = make_node!(2, fee_est_c);
let mut nodes = [node_a, node_b, node_c];
+ let loggers = [logger_a, logger_b, logger_c];
+ let fee_estimators = [Arc::clone(&fee_est_a), Arc::clone(&fee_est_b), Arc::clone(&fee_est_c)];
// Connect peers first, then create channels
connect_peers!(nodes[0], nodes[1]);
@@ -2130,79 +2077,107 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
},
0xa0 => {
- let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap();
- let contribution =
- SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None);
- let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[0].splice_channel(
- &chan_a_id,
- &nodes[1].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw = fee_estimators[0].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[0].splice_channel(&chan_a_id, &nodes[1].get_our_node_id(), feerate) {
+ Ok(funding_template) => {
+ let wallet = WalletSync::new(&wallets[0], Arc::clone(&loggers[0]));
+ if let Ok(contribution) =
+ funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ {
+ let _ = nodes[0].funding_contributed(
+ &chan_a_id,
+ &nodes[1].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
},
0xa1 => {
- let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap();
- let contribution =
- SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None);
- let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[1].splice_channel(
- &chan_a_id,
- &nodes[0].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw = fee_estimators[1].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[1].splice_channel(&chan_a_id, &nodes[0].get_our_node_id(), feerate) {
+ Ok(funding_template) => {
+ let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
+ if let Ok(contribution) =
+ funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ {
+ let _ = nodes[1].funding_contributed(
+ &chan_a_id,
+ &nodes[0].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
},
0xa2 => {
- let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap();
- let contribution =
- SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None);
- let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[1].splice_channel(
- &chan_b_id,
- &nodes[2].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw = fee_estimators[1].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[1].splice_channel(&chan_b_id, &nodes[2].get_our_node_id(), feerate) {
+ Ok(funding_template) => {
+ let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
+ if let Ok(contribution) =
+ funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ {
+ let _ = nodes[1].funding_contributed(
+ &chan_b_id,
+ &nodes[2].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
},
0xa3 => {
- let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap();
- let contribution =
- SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None);
- let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[2].splice_channel(
- &chan_b_id,
- &nodes[1].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw = fee_estimators[2].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[2].splice_channel(&chan_b_id, &nodes[1].get_our_node_id(), feerate) {
+ Ok(funding_template) => {
+ let wallet = WalletSync::new(&wallets[2], Arc::clone(&loggers[2]));
+ if let Ok(contribution) =
+ funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ {
+ let _ = nodes[2].funding_contributed(
+ &chan_b_id,
+ &nodes[1].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
},
@@ -2217,24 +2192,35 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
.map(|chan| chan.outbound_capacity_msat)
.unwrap();
if outbound_capacity_msat >= 20_000_000 {
- let contribution = SpliceContribution::splice_out(vec![TxOut {
- value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
- script_pubkey: coinbase_tx.output[0].script_pubkey.clone(),
- }]);
- let funding_feerate_sat_per_kw =
- fee_est_a.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[0].splice_channel(
- &chan_a_id,
- &nodes[1].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw =
+ fee_estimators[0].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[0].splice_channel(&chan_a_id, &nodes[1].get_our_node_id(), feerate)
+ {
+ Ok(funding_template) => {
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
+ script_pubkey: coinbase_tx.output[0].script_pubkey.clone(),
+ }];
+ let wallet = WalletSync::new(&wallets[0], Arc::clone(&loggers[0]));
+ if let Ok(contribution) =
+ funding_template.splice_out_sync(outputs, &wallet)
+ {
+ let _ = nodes[0].funding_contributed(
+ &chan_a_id,
+ &nodes[1].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
}
},
@@ -2246,24 +2232,35 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
.map(|chan| chan.outbound_capacity_msat)
.unwrap();
if outbound_capacity_msat >= 20_000_000 {
- let contribution = SpliceContribution::splice_out(vec![TxOut {
- value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
- script_pubkey: coinbase_tx.output[1].script_pubkey.clone(),
- }]);
- let funding_feerate_sat_per_kw =
- fee_est_b.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[1].splice_channel(
- &chan_a_id,
- &nodes[0].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw =
+ fee_estimators[1].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[1].splice_channel(&chan_a_id, &nodes[0].get_our_node_id(), feerate)
+ {
+ Ok(funding_template) => {
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
+ script_pubkey: coinbase_tx.output[1].script_pubkey.clone(),
+ }];
+ let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
+ if let Ok(contribution) =
+ funding_template.splice_out_sync(outputs, &wallet)
+ {
+ let _ = nodes[1].funding_contributed(
+ &chan_a_id,
+ &nodes[0].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
}
},
@@ -2275,24 +2272,35 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
.map(|chan| chan.outbound_capacity_msat)
.unwrap();
if outbound_capacity_msat >= 20_000_000 {
- let contribution = SpliceContribution::splice_out(vec![TxOut {
- value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
- script_pubkey: coinbase_tx.output[1].script_pubkey.clone(),
- }]);
- let funding_feerate_sat_per_kw =
- fee_est_b.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[1].splice_channel(
- &chan_b_id,
- &nodes[2].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw =
+ fee_estimators[1].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[1].splice_channel(&chan_b_id, &nodes[2].get_our_node_id(), feerate)
+ {
+ Ok(funding_template) => {
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
+ script_pubkey: coinbase_tx.output[1].script_pubkey.clone(),
+ }];
+ let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
+ if let Ok(contribution) =
+ funding_template.splice_out_sync(outputs, &wallet)
+ {
+ let _ = nodes[1].funding_contributed(
+ &chan_b_id,
+ &nodes[2].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
}
},
@@ -2304,24 +2312,35 @@ pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) {
.map(|chan| chan.outbound_capacity_msat)
.unwrap();
if outbound_capacity_msat >= 20_000_000 {
- let contribution = SpliceContribution::splice_out(vec![TxOut {
- value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
- script_pubkey: coinbase_tx.output[2].script_pubkey.clone(),
- }]);
- let funding_feerate_sat_per_kw =
- fee_est_c.ret_val.load(atomic::Ordering::Acquire);
- if let Err(e) = nodes[2].splice_channel(
- &chan_b_id,
- &nodes[1].get_our_node_id(),
- contribution,
- funding_feerate_sat_per_kw,
- None,
- ) {
- assert!(
- matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")),
- "{:?}",
- e
- );
+ let feerate_sat_per_kw =
+ fee_estimators[2].ret_val.load(atomic::Ordering::Acquire);
+ let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64);
+ match nodes[2].splice_channel(&chan_b_id, &nodes[1].get_our_node_id(), feerate)
+ {
+ Ok(funding_template) => {
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS),
+ script_pubkey: coinbase_tx.output[2].script_pubkey.clone(),
+ }];
+ let wallet = WalletSync::new(&wallets[2], Arc::clone(&loggers[2]));
+ if let Ok(contribution) =
+ funding_template.splice_out_sync(outputs, &wallet)
+ {
+ let _ = nodes[2].funding_contributed(
+ &chan_b_id,
+ &nodes[1].get_our_node_id(),
+ contribution,
+ None,
+ );
+ }
+ },
+ Err(e) => {
+ assert!(
+ matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")),
+ "{:?}",
+ e
+ );
+ },
}
}
},
@@ -2609,7 +2628,7 @@ impl<O: Output> SearchingOutput<O> {
}
}
-pub fn chanmon_consistency_test<Out: Output>(data: &[u8], out: Out) {
+pub fn chanmon_consistency_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
do_test(data, out.clone(), false);
do_test(data, out, true);
}
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index c55da4f..2163ca0 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -22,6 +22,7 @@ use bitcoin::opcodes;
use bitcoin::script::{Builder, ScriptBuf};
use bitcoin::transaction::Version;
use bitcoin::transaction::{Transaction, TxIn, TxOut};
+use bitcoin::FeeRate;
use bitcoin::hash_types::{BlockHash, Txid};
use bitcoin::hashes::sha256::Hash as Sha256;
@@ -30,8 +31,6 @@ use bitcoin::hashes::Hash as _;
use bitcoin::hex::FromHex;
use bitcoin::WPubkeyHash;
-use lightning::ln::funding::{FundingTxInput, SpliceContribution};
-
use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode};
use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs};
use lightning::chain;
@@ -41,7 +40,7 @@ use lightning::chain::chaininterface::{
use lightning::chain::chainmonitor;
use lightning::chain::transaction::OutPoint;
use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen};
-use lightning::events::bump_transaction::sync::WalletSourceSync;
+use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
use lightning::events::Event;
use lightning::ln::channel_state::ChannelDetails;
use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId, PaymentId};
@@ -65,6 +64,7 @@ use lightning::sign::{
SignerProvider,
};
use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
+use lightning::util::async_poll::{MaybeSend, MaybeSync};
use lightning::util::config::{ChannelConfig, UserConfig};
use lightning::util::hash_tables::*;
use lightning::util::logger::Logger;
@@ -227,7 +227,7 @@ type ChannelMan<'a> = ChannelManager<
Arc<dyn chain::Filter>,
Arc<TestBroadcaster>,
Arc<FuzzEstimator>,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
Arc<TestPersister>,
Arc<KeyProvider>,
>,
@@ -239,14 +239,20 @@ type ChannelMan<'a> = ChannelManager<
Arc<FuzzEstimator>,
&'a FuzzRouter,
&'a FuzzRouter,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
>;
type PeerMan<'a> = PeerManager<
Peer<'a>,
Arc<ChannelMan<'a>>,
- Arc<P2PGossipSync<Arc<NetworkGraph<Arc<dyn Logger>>>, Arc<dyn UtxoLookup>, Arc<dyn Logger>>>,
+ Arc<
+ P2PGossipSync<
+ Arc<NetworkGraph<Arc<dyn Logger + MaybeSend + MaybeSync>>>,
+ Arc<dyn UtxoLookup>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
+ >,
+ >,
IgnoringMessageHandler,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
IgnoringMessageHandler,
Arc<KeyProvider>,
IgnoringMessageHandler,
@@ -260,7 +266,7 @@ struct MoneyLossDetector<'a> {
Arc<dyn chain::Filter>,
Arc<TestBroadcaster>,
Arc<FuzzEstimator>,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
Arc<TestPersister>,
Arc<KeyProvider>,
>,
@@ -285,7 +291,7 @@ impl<'a> MoneyLossDetector<'a> {
Arc<dyn chain::Filter>,
Arc<TestBroadcaster>,
Arc<FuzzEstimator>,
- Arc<dyn Logger>,
+ Arc<dyn Logger + MaybeSend + MaybeSync>,
Arc<TestPersister>,
Arc<KeyProvider>,
>,
@@ -520,7 +526,7 @@ impl SignerProvider for KeyProvider {
}
#[inline]
-pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
+pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>) {
if data.len() < 32 {
return;
}
@@ -1024,20 +1030,26 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
if splice_in_sats == 0 {
continue;
}
- // Create a funding input from the coinbase transaction
- if let Ok(input) = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0) {
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_sats.min(900_000)), // Cap at available funds minus fees
- vec![input],
- Some(wallet.get_change_script().unwrap()),
- );
- let _ = channelmanager.splice_channel(
- &chan.channel_id,
- &chan.counterparty.node_id,
- contribution,
- 253, // funding_feerate_per_kw
+ let chan_id = chan.channel_id;
+ let counterparty = chan.counterparty.node_id;
+ if let Ok(funding_template) = channelmanager.splice_channel(
+ &chan_id,
+ &counterparty,
+ FeeRate::from_sat_per_kwu(253),
+ ) {
+ let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
+ if let Ok(contribution) = funding_template.splice_in_sync(
None,
- );
+ Amount::from_sat(splice_in_sats.min(900_000)),
+ &wallet_sync,
+ ) {
+ let _ = channelmanager.funding_contributed(
+ &chan_id,
+ &counterparty,
+ contribution,
+ None,
+ );
+ }
}
},
// Splice-out: remove funds from a channel
@@ -1060,17 +1072,29 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
// Cap splice-out at a reasonable portion of channel capacity
let max_splice_out = chan.channel_value_satoshis / 4;
let splice_out_sats = splice_out_sats.min(max_splice_out).max(546); // At least dust limit
- let contribution = SpliceContribution::splice_out(vec![TxOut {
- value: Amount::from_sat(splice_out_sats),
- script_pubkey: wallet.get_change_script().unwrap(),
- }]);
- let _ = channelmanager.splice_channel(
- &chan.channel_id,
- &chan.counterparty.node_id,
- contribution,
- 253, // funding_feerate_per_kw
- None,
- );
+ let chan_id = chan.channel_id;
+ let counterparty = chan.counterparty.node_id;
+ if let Ok(funding_template) = channelmanager.splice_channel(
+ &chan_id,
+ &counterparty,
+ FeeRate::from_sat_per_kwu(253),
+ ) {
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(splice_out_sats),
+ script_pubkey: wallet.get_change_script().unwrap(),
+ }];
+ let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
+ if let Ok(contribution) =
+ funding_template.splice_out_sync(outputs, &wallet_sync)
+ {
+ let _ = channelmanager.funding_contributed(
+ &chan_id,
+ &counterparty,
+ contribution,
+ None,
+ );
+ }
+ }
},
_ => return,
}
@@ -1137,14 +1161,15 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) {
}
}
-pub fn full_stack_test<Out: test_logger::Output>(data: &[u8], out: Out) {
- let logger: Arc<dyn Logger> = Arc::new(test_logger::TestLogger::new("".to_owned(), out));
+pub fn full_stack_test<Out: test_logger::Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
+ let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
+ Arc::new(test_logger::TestLogger::new("".to_owned(), out));
do_test(data, &logger);
}
#[no_mangle]
pub extern "C" fn full_stack_run(data: *const u8, datalen: usize) {
- let logger: Arc<dyn Logger> =
+ let logger: Arc<dyn Logger + MaybeSend + MaybeSync> =
Arc::new(test_logger::TestLogger::new("".to_owned(), test_logger::DevNull {}));
do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, &logger);
}
@@ -1930,6 +1955,7 @@ pub fn write_fst_seeds(path: &str) {
#[cfg(test)]
mod tests {
+ use lightning::util::async_poll::{MaybeSend, MaybeSync};
use lightning::util::logger::{Logger, Record};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
@@ -1961,7 +1987,7 @@ mod tests {
let test = super::two_peer_forwarding_seed();
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
- super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
+ super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
let log_entries = logger.lines.lock().unwrap();
// 1
@@ -1996,7 +2022,7 @@ mod tests {
let test = super::gossip_exchange_seed();
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
- super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
+ super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
let log_entries = logger.lines.lock().unwrap();
assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)) or the announced channel's counterparties: ChannelAnnouncement { node_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, node_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, bitcoin_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, bitcoin_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, contents: UnsignedChannelAnnouncement { features: [], chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, node_id_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), node_id_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), bitcoin_key_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), bitcoin_key_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), excess_data: [] } }".to_string())), Some(&1));
@@ -2009,7 +2035,7 @@ mod tests {
let test = super::splice_seed();
let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) });
- super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>));
+ super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>));
let log_entries = logger.lines.lock().unwrap();
diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs
index 14b0a5c..dde1941 100644
--- a/lightning-tests/src/upgrade_downgrade_tests.rs
+++ b/lightning-tests/src/upgrade_downgrade_tests.rs
@@ -49,7 +49,6 @@ use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER};
use lightning::events::bump_transaction::sync::WalletSourceSync;
use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType};
use lightning::ln::functional_test_utils::*;
-use lightning::ln::funding::SpliceContribution;
use lightning::ln::msgs::BaseMessageHandler as _;
use lightning::ln::msgs::ChannelMessageHandler as _;
use lightning::ln::msgs::MessageSendEvent;
@@ -453,11 +452,13 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) {
reconnect_b_c_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_b_c_args);
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], ChannelId(chan_id_bytes_a), contribution);
+ }];
+ let channel_id = ChannelId(chan_id_bytes_a);
+ let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
for node in nodes.iter() {
mine_transaction(node, &splice_tx);
connect_blocks(node, ANTI_REORG_DELAY - 1);
diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs
index b81279c..f34a2b3 100644
--- a/lightning/src/ln/async_signer_tests.rs
+++ b/lightning/src/ln/async_signer_tests.rs
@@ -11,8 +11,7 @@
//! properly with a signer implementation that asynchronously derives signatures.
use crate::events::bump_transaction::sync::WalletSourceSync;
-use crate::ln::funding::SpliceContribution;
-use crate::ln::splicing_tests::negotiate_splice_tx;
+use crate::ln::splicing_tests::{initiate_splice_out, negotiate_splice_tx};
use crate::prelude::*;
use crate::util::ser::Writeable;
use bitcoin::secp256k1::Secp256k1;
@@ -1573,10 +1572,11 @@ fn test_async_splice_initial_commit_sig() {
);
// Negotiate a splice up until the signature exchange.
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs);
negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 48b5299..e000ebc 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -11,7 +11,7 @@ use bitcoin::absolute::LockTime;
use bitcoin::amount::{Amount, SignedAmount};
use bitcoin::consensus::encode;
use bitcoin::constants::ChainHash;
-use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash};
+use bitcoin::script::{Builder, Script, ScriptBuf};
use bitcoin::sighash::EcdsaSighashType;
use bitcoin::transaction::{Transaction, TxOut};
use bitcoin::Witness;
@@ -36,6 +36,7 @@ use crate::chain::channelmonitor::{
};
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::chain::BestBlock;
+use crate::events::bump_transaction::Input;
use crate::events::{ClosureReason, FundingInfo};
use crate::ln::chan_utils;
use crate::ln::chan_utils::{
@@ -43,7 +44,7 @@ use crate::ln::chan_utils::{
selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters,
ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters,
CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction,
- BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT,
+ EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT,
};
use crate::ln::channel_state::{
ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails,
@@ -55,12 +56,11 @@ use crate::ln::channelmanager::{
PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT,
MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA,
};
-use crate::ln::funding::{FundingTxInput, SpliceContribution};
+use crate::ln::funding::{FundingContribution, FundingTemplate, FundingTxInput};
use crate::ln::interactivetxs::{
calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue,
InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend,
InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput,
- TX_COMMON_FIELDS_WEIGHT,
};
use crate::ln::msgs;
use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket};
@@ -69,7 +69,6 @@ use crate::ln::onion_utils::{
};
use crate::ln::script::{self, ShutdownScript};
use crate::ln::types::ChannelId;
-use crate::ln::LN_MAX_MSG_LEN;
use crate::offers::static_invoice::StaticInvoice;
use crate::routing::gossip::NodeId;
use crate::sign::ecdsa::EcdsaChannelSigner;
@@ -2686,6 +2685,20 @@ impl FundingScope {
self.channel_transaction_parameters.funding_outpoint
}
+ /// Gets the funding output for this channel, if available.
+ ///
+ /// When a channel is spliced, this continues to refer to the original funding output (which
+ /// was spent by the splice transaction) until the splice transaction reaches sufficient
+ /// confirmations to be locked (and we exchange `splice_locked` messages with our peer).
+ pub fn get_funding_output(&self) -> Option<TxOut> {
+ self.channel_transaction_parameters.make_funding_redeemscript_opt().map(|redeem_script| {
+ TxOut {
+ value: Amount::from_sat(self.get_value_satoshis()),
+ script_pubkey: redeem_script.to_p2wsh(),
+ }
+ })
+ }
+
fn get_funding_txid(&self) -> Option<Txid> {
self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid)
}
@@ -3010,7 +3023,12 @@ impl_writeable_tlv_based!(SpliceInstructions, {
#[derive(Debug)]
pub(crate) enum QuiescentAction {
- Splice(SpliceInstructions),
+ // Deprecated in favor of the Splice variant and no longer produced as of LDK 0.3.
+ LegacySplice(SpliceInstructions),
+ Splice {
+ contribution: FundingContribution,
+ locktime: LockTime,
+ },
#[cfg(any(test, fuzzing))]
DoNothing,
}
@@ -3023,11 +3041,19 @@ pub(crate) enum StfuResponse {
#[cfg(any(test, fuzzing))]
impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,
(0, DoNothing) => {},
- {1, Splice} => (),
+ (2, Splice) => {
+ (0, contribution, required),
+ (1, locktime, required),
+ },
+ {1, LegacySplice} => (),
);
#[cfg(not(any(test, fuzzing)))]
-impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,,
- {1, Splice} => (),
+impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,
+ (2, Splice) => {
+ (0, contribution, required),
+ (1, locktime, required),
+ },
+ {1, LegacySplice} => (),
);
/// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`].
@@ -6632,130 +6658,6 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis))
}
-fn check_splice_contribution_sufficient(
- contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate,
-) -> Result<SignedAmount, String> {
- if contribution.inputs().is_empty() {
- let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
- contribution.inputs(),
- contribution.outputs(),
- is_initiator,
- true, // is_splice
- funding_feerate.to_sat_per_kwu() as u32,
- ));
-
- let contribution_amount = contribution.net_value();
- contribution_amount
- .checked_sub(
- estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"),
- )
- .ok_or(format!(
- "{estimated_fee} splice-out amount plus {} fee estimate exceeds the total bitcoin supply",
- contribution_amount.unsigned_abs(),
- ))
- } else {
- check_v2_funding_inputs_sufficient(
- contribution.value_added(),
- contribution.inputs(),
- contribution.outputs(),
- is_initiator,
- true,
- funding_feerate.to_sat_per_kwu() as u32,
- )
- .map(|_| contribution.net_value())
- }
-}
-
-/// Estimate our part of the fee of the new funding transaction.
-#[allow(dead_code)] // TODO(dual_funding): TODO(splicing): Remove allow once used.
-#[rustfmt::skip]
-fn estimate_v2_funding_transaction_fee(
- funding_inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool,
- funding_feerate_sat_per_1000_weight: u32,
-) -> u64 {
- let input_weight: u64 = funding_inputs
- .iter()
- .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight))
- .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight));
-
- let output_weight: u64 = outputs
- .iter()
- .map(|txout| txout.weight().to_wu())
- .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight));
-
- let mut weight = input_weight.saturating_add(output_weight);
-
- // The initiator pays for all common fields and the shared output in the funding transaction.
- if is_initiator {
- weight = weight
- .saturating_add(TX_COMMON_FIELDS_WEIGHT)
- // The weight of the funding output, a P2WSH output
- // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want
- // to calculate the contributed weight, so we use an all-zero hash.
- .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh(
- &WScriptHash::from_raw_hash(Hash::all_zeros())
- )).to_wu());
-
- // The splice initiator pays for the input spending the previous funding output.
- if is_splice {
- weight = weight
- .saturating_add(BASE_INPUT_WEIGHT)
- .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT)
- .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT);
- #[cfg(feature = "grind_signatures")]
- {
- // Guarantees a low R signature
- weight -= 1;
- }
- }
- }
-
- fee_for_weight(funding_feerate_sat_per_1000_weight, weight)
-}
-
-/// Verify that the provided inputs to the funding transaction are enough
-/// to cover the intended contribution amount *plus* the proportional fees.
-/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain
-/// the fees of the inputs, fees of the inputs weight, and for the initiator,
-/// the fees of the common fields as well as the output and extra input weights.
-/// Returns estimated (partial) fees as additional information
-#[rustfmt::skip]
-fn check_v2_funding_inputs_sufficient(
- contributed_input_value: Amount, funding_inputs: &[FundingTxInput], outputs: &[TxOut],
- is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
-) -> Result<Amount, String> {
- let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
- funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
- ));
-
- let mut total_input_value = Amount::ZERO;
- for FundingTxInput { utxo, .. } in funding_inputs.iter() {
- total_input_value = total_input_value.checked_add(utxo.output.value)
- .ok_or("Sum of input values is greater than the total bitcoin supply")?;
- }
-
- // If the inputs are enough to cover intended contribution amount, with fees even when
- // there is a change output, we are fine.
- // If the inputs are less, but enough to cover intended contribution amount, with
- // (lower) fees with no change, we are also fine (change will not be generated).
- // So it's enough to check considering the lower, no-change fees.
- //
- // Note: dust limit is not relevant in this check.
- //
- // TODO(splicing): refine check including the fact wether a change will be added or not.
- // Can be done once dual funding preparation is included.
-
- let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee)
- .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?;
- if total_input_value < minimal_input_amount_needed {
- Err(format!(
- "Total input amount {total_input_value} is lower than needed for splice-in contribution {contributed_input_value}, considering fees of {estimated_fee}. Need more inputs.",
- ))
- } else {
- Ok(estimated_fee)
- }
-}
-
/// Context for negotiating channels (dual-funded V2 open, splicing)
#[derive(Debug)]
pub(super) struct FundingNegotiationContext {
@@ -7121,7 +7023,7 @@ where
self.reset_pending_splice_state()
} else {
match self.quiescent_action.take() {
- Some(QuiescentAction::Splice(instructions)) => {
+ Some(QuiescentAction::LegacySplice(instructions)) => {
self.context.channel_state.clear_awaiting_quiescence();
let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs();
Some(SpliceFundingFailed {
@@ -7131,6 +7033,16 @@ where
contributed_outputs: outputs,
})
},
+ Some(QuiescentAction::Splice { contribution, .. }) => {
+ self.context.channel_state.clear_awaiting_quiescence();
+ let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs();
+ Some(SpliceFundingFailed {
+ funding_txo: None,
+ channel_type: None,
+ contributed_inputs: inputs,
+ contributed_outputs: outputs,
+ })
+ },
#[cfg(any(test, fuzzing))]
Some(quiescent_action) => {
self.quiescent_action = Some(quiescent_action);
@@ -11551,7 +11463,12 @@ where
self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger);
if let Some(quiescent_action) = self.quiescent_action.as_ref() {
- if matches!(quiescent_action, QuiescentAction::Splice(_)) {
+ // TODO(splicing): If we didn't win quiescence, then we can contribute as an acceptor
+ // instead of waiting for the splice to lock.
+ if matches!(
+ quiescent_action,
+ QuiescentAction::Splice { .. } | QuiescentAction::LegacySplice(_)
+ ) {
self.context.channel_state.set_awaiting_quiescence();
}
}
@@ -12196,14 +12113,7 @@ where
}
/// Initiate splicing.
- /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction.
- /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs).
- /// - `change_script`: an option change output script. If `None` and needed, one will be
- /// generated by `SignerProvider::get_destination_script`.
- pub fn splice_channel<L: Logger>(
- &mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32,
- logger: &L,
- ) -> Result<Option<msgs::Stfu>, APIError> {
+ pub fn splice_channel(&mut self, feerate: FeeRate) -> Result<FundingTemplate, APIError> {
if self.holder_commitment_point.current_point().is_none() {
return Err(APIError::APIMisuseError {
err: format!(
@@ -12213,17 +12123,29 @@ where
});
}
- // Check if a splice has been initiated already.
- // Note: only a single outstanding splice is supported (per spec)
- if self.pending_splice.is_some() || self.quiescent_action.is_some() {
+ if self.quiescent_action.is_some() {
return Err(APIError::APIMisuseError {
err: format!(
- "Channel {} cannot be spliced, as it has already a splice pending",
+ "Channel {} cannot be spliced as one is waiting to be negotiated",
self.context.channel_id(),
),
});
}
+ if let Some(pending_splice) = &self.pending_splice {
+ if let Some(funding_negotiation) = &pending_splice.funding_negotiation {
+ debug_assert!(self.context.channel_state.is_quiescent());
+ if funding_negotiation.is_initiator() {
+ return Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel {} cannot be spliced as one is currently being negotiated",
+ self.context.channel_id(),
+ ),
+ });
+ }
+ }
+ }
+
if !self.context.is_usable() {
return Err(APIError::APIMisuseError {
err: format!(
@@ -12233,81 +12155,68 @@ where
});
}
- let our_funding_contribution = contribution.net_value();
- if our_funding_contribution == SignedAmount::ZERO {
- return Err(APIError::APIMisuseError {
- err: format!(
- "Channel {} cannot be spliced; contribution cannot be zero",
- self.context.channel_id(),
- ),
- });
- }
+ let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set");
+ let previous_utxo =
+ self.funding.get_funding_output().expect("funding_output should be set");
+ let shared_input = Input {
+ outpoint: funding_txo.into_bitcoin_outpoint(),
+ previous_utxo,
+ satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
+ };
- // Fees for splice-out are paid from the channel balance whereas fees for splice-in
- // are paid by the funding inputs. Therefore, in the case of splice-out, we add the
- // fees on top of the user-specified contribution. We leave the user-specified
- // contribution as-is for splice-ins.
- let adjusted_funding_contribution = check_splice_contribution_sufficient(
- &contribution,
- true,
- FeeRate::from_sat_per_kwu(u64::from(funding_feerate_per_kw)),
- )
- .map_err(|e| APIError::APIMisuseError {
- err: format!(
- "Channel {} cannot be {}; {}",
- self.context.channel_id(),
- if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" },
- e
- ),
- })?;
+ Ok(FundingTemplate::new(Some(shared_input), feerate, true))
+ }
- // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
- // (Cannot test for miminum required post-splice channel value)
- let their_funding_contribution = SignedAmount::ZERO;
- self.validate_splice_contributions(
- adjusted_funding_contribution,
- their_funding_contribution,
- )
- .map_err(|err| APIError::APIMisuseError { err })?;
-
- for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() {
- const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
- channel_id: ChannelId([0; 32]),
- serial_id: 0,
- prevtx: None,
- prevtx_out: 0,
- sequence: 0,
- // Mutually exclusive with prevtx, which is accounted for below.
- shared_input_txid: None,
- };
- let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length();
- if message_len > LN_MAX_MSG_LEN {
- return Err(APIError::APIMisuseError {
- err: format!(
- "Funding input references a prevtx that is too large for tx_add_input: {}",
- utxo.outpoint,
- ),
- });
- }
- }
+ pub fn funding_contributed<L: Logger>(
+ &mut self, contribution: FundingContribution, locktime: LockTime, logger: &L,
+ ) -> Result<Option<msgs::Stfu>, SpliceFundingFailed> {
+ debug_assert!(contribution.is_splice());
- let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
+ if let Err(e) = contribution.net_value().and_then(|our_funding_contribution| {
+ // For splice-out, our_funding_contribution is adjusted to cover fees if there
+ // aren't any inputs.
+ self.validate_splice_contributions(our_funding_contribution, SignedAmount::ZERO)
+ }) {
+ log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e);
- let action = QuiescentAction::Splice(SpliceInstructions {
- adjusted_funding_contribution,
- our_funding_inputs,
- our_funding_outputs,
- change_script,
- funding_feerate_per_kw,
- locktime,
- });
- self.propose_quiescence(logger, action)
- .map_err(|e| APIError::APIMisuseError { err: e.to_owned() })
+ let (contributed_inputs, contributed_outputs) =
+ contribution.into_contributed_inputs_and_outputs();
+
+ return Err(SpliceFundingFailed {
+ funding_txo: None,
+ channel_type: None,
+ contributed_inputs,
+ contributed_outputs,
+ });
+ }
+
+ self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }).map_err(
+ |(e, action)| {
+ log_error!(logger, "{}", e);
+ // FIXME: Any better way to do this?
+ if let QuiescentAction::Splice { contribution, .. } = action {
+ let (contributed_inputs, contributed_outputs) =
+ contribution.into_contributed_inputs_and_outputs();
+ SpliceFundingFailed {
+ funding_txo: None,
+ channel_type: None,
+ contributed_inputs,
+ contributed_outputs,
+ }
+ } else {
+ debug_assert!(false);
+ SpliceFundingFailed {
+ funding_txo: None,
+ channel_type: None,
+ contributed_inputs: vec![],
+ contributed_outputs: vec![],
+ }
+ }
+ },
+ )
}
fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit {
- debug_assert!(self.pending_splice.is_none());
-
let SpliceInstructions {
adjusted_funding_contribution,
our_funding_inputs,
@@ -12329,6 +12238,13 @@ where
change_script,
};
+ self.send_splice_init_internal(context)
+ }
+
+ fn send_splice_init_internal(
+ &mut self, context: FundingNegotiationContext,
+ ) -> msgs::SpliceInit {
+ debug_assert!(self.pending_splice.is_none());
// Rotate the funding pubkey using the prev_funding_txid as a tweak
let prev_funding_txid = self.funding.get_funding_txid();
let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) {
@@ -12343,6 +12259,10 @@ where
_ => todo!(),
};
+ let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight;
+ let funding_contribution_satoshis = context.our_funding_contribution.to_sat();
+ let locktime = context.funding_tx_locktime.to_consensus_u32();
+
let funding_negotiation =
FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey };
self.pending_splice = Some(PendingFunding {
@@ -12354,7 +12274,7 @@ where
msgs::SpliceInit {
channel_id: self.context.channel_id,
- funding_contribution_satoshis: adjusted_funding_contribution.to_sat(),
+ funding_contribution_satoshis,
funding_feerate_per_kw,
locktime,
funding_pubkey,
@@ -12421,7 +12341,7 @@ where
}
// TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient,
- // similarly to the check in `splice_channel`.
+ // similarly to the check in `funding_contributed`.
debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO);
let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis);
@@ -13408,14 +13328,14 @@ where
#[rustfmt::skip]
pub fn propose_quiescence<L: Logger>(
&mut self, logger: &L, action: QuiescentAction,
- ) -> Result<Option<msgs::Stfu>, &'static str> {
+ ) -> Result<Option<msgs::Stfu>, (&'static str, QuiescentAction)> {
log_debug!(logger, "Attempting to initiate quiescence");
if !self.context.is_usable() {
- return Err("Channel is not in a usable state to propose quiescence");
+ return Err(("Channel is not in a usable state to propose quiescence", action));
}
if self.quiescent_action.is_some() {
- return Err("Channel already has a pending quiescent action and cannot start another");
+ return Err(("Channel already has a pending quiescent action and cannot start another", action));
}
self.quiescent_action = Some(action);
@@ -13556,9 +13476,10 @@ where
"Internal Error: Didn't have anything to do after reaching quiescence".to_owned()
));
},
- Some(QuiescentAction::Splice(instructions)) => {
+ Some(QuiescentAction::LegacySplice(instructions)) => {
if self.pending_splice.is_some() {
- self.quiescent_action = Some(QuiescentAction::Splice(instructions));
+ debug_assert!(false);
+ self.quiescent_action = Some(QuiescentAction::LegacySplice(instructions));
return Err(ChannelError::WarnAndDisconnect(
format!(
@@ -13571,6 +13492,53 @@ where
let splice_init = self.send_splice_init(instructions);
return Ok(Some(StfuResponse::SpliceInit(splice_init)));
},
+ Some(QuiescentAction::Splice { contribution, locktime }) => {
+ // TODO(splicing): If the splice has been negotiated but has not been locked, we
+ // can RBF here to add the contribution.
+ if self.pending_splice.is_some() {
+ debug_assert!(false);
+ self.quiescent_action =
+ Some(QuiescentAction::Splice { contribution, locktime });
+
+ return Err(ChannelError::WarnAndDisconnect(
+ format!(
+ "Channel {} cannot be spliced as it already has a splice pending",
+ self.context.channel_id(),
+ ),
+ ));
+ }
+
+ let prev_funding_input = self.funding.to_splice_funding_input();
+ let is_initiator = contribution.is_initiator();
+ let our_funding_contribution = match contribution.net_value() {
+ Ok(net_value) => net_value,
+ Err(e) => {
+ debug_assert!(false);
+ return Err(ChannelError::WarnAndDisconnect(
+ format!(
+ "Internal Error: Insufficient funding contribution: {}",
+ e,
+ )
+ ));
+ },
+ };
+ let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32;
+ let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
+
+ let context = FundingNegotiationContext {
+ is_initiator,
+ our_funding_contribution,
+ funding_tx_locktime: locktime,
+ funding_feerate_sat_per_1000_weight: funding_feerate_per_kw,
+ shared_funding_input: Some(prev_funding_input),
+ our_funding_inputs,
+ our_funding_outputs,
+ change_script,
+ };
+
+ let splice_init = self.send_splice_init_internal(context);
+ return Ok(Some(StfuResponse::SpliceInit(splice_init)));
+ },
#[cfg(any(test, fuzzing))]
Some(QuiescentAction::DoNothing) => {
// In quiescence test we want to just hang out here, letting the test manually
@@ -16130,7 +16098,6 @@ mod tests {
};
use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey};
use crate::ln::channelmanager::{self, HTLCSource, PaymentId};
- use crate::ln::funding::FundingTxInput;
use crate::ln::msgs;
use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT};
use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason};
@@ -16162,7 +16129,7 @@ mod tests {
use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1};
use bitcoin::secp256k1::{PublicKey, SecretKey};
use bitcoin::transaction::{Transaction, TxOut, Version};
- use bitcoin::{ScriptBuf, WPubkeyHash, WitnessProgram, WitnessVersion};
+ use bitcoin::{WitnessProgram, WitnessVersion};
use std::cmp;
fn dummy_inbound_update_add() -> InboundUpdateAdd {
@@ -18510,250 +18477,6 @@ mod tests {
assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some());
}
- #[test]
- #[rustfmt::skip]
- fn test_estimate_v2_funding_transaction_fee() {
- use crate::ln::channel::estimate_v2_funding_transaction_fee;
-
- let one_input = [funding_input_sats(1_000)];
- let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)];
-
- // 2 inputs, initiator, 2000 sat/kw feerate
- assert_eq!(
- estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 2000),
- if cfg!(feature = "grind_signatures") { 1512 } else { 1516 },
- );
-
- // higher feerate
- assert_eq!(
- estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 3000),
- if cfg!(feature = "grind_signatures") { 2268 } else { 2274 },
- );
-
- // only 1 input
- assert_eq!(
- estimate_v2_funding_transaction_fee(&one_input, &[], true, false, 2000),
- if cfg!(feature = "grind_signatures") { 970 } else { 972 },
- );
-
- // 0 inputs
- assert_eq!(
- estimate_v2_funding_transaction_fee(&[], &[], true, false, 2000),
- 428,
- );
-
- // not initiator
- assert_eq!(
- estimate_v2_funding_transaction_fee(&[], &[], false, false, 2000),
- 0,
- );
-
- // splice initiator
- assert_eq!(
- estimate_v2_funding_transaction_fee(&one_input, &[], true, true, 2000),
- if cfg!(feature = "grind_signatures") { 1736 } else { 1740 },
- );
-
- // splice acceptor
- assert_eq!(
- estimate_v2_funding_transaction_fee(&one_input, &[], false, true, 2000),
- if cfg!(feature = "grind_signatures") { 542 } else { 544 },
- );
- }
-
- #[rustfmt::skip]
- fn funding_input_sats(input_value_sats: u64) -> FundingTxInput {
- let prevout = TxOut {
- value: Amount::from_sat(input_value_sats),
- script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
- };
- let prevtx = Transaction {
- input: vec![], output: vec![prevout],
- version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO,
- };
-
- FundingTxInput::new_p2wpkh(prevtx, 0).unwrap()
- }
-
- fn funding_output_sats(output_value_sats: u64) -> TxOut {
- TxOut {
- value: Amount::from_sat(output_value_sats),
- script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
- }
- }
-
- #[test]
- #[rustfmt::skip]
- fn test_check_v2_funding_inputs_sufficient() {
- use crate::ln::channel::check_v2_funding_inputs_sufficient;
-
- // positive case, inputs well over intended contribution
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(220_000),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[],
- true,
- true,
- 2000,
- ).unwrap(),
- Amount::from_sat(expected_fee),
- );
- }
-
- // Net splice-in
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(220_000),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[
- funding_output_sats(200_000),
- ],
- true,
- true,
- 2000,
- ).unwrap(),
- Amount::from_sat(expected_fee),
- );
- }
-
- // Net splice-out
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(220_000),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[
- funding_output_sats(400_000),
- ],
- true,
- true,
- 2000,
- ).unwrap(),
- Amount::from_sat(expected_fee),
- );
- }
-
- // Net splice-out, inputs insufficient to cover fees
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(220_000),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[
- funding_output_sats(400_000),
- ],
- true,
- true,
- 90000,
- ),
- Err(format!(
- "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
- Amount::from_sat(expected_fee),
- )),
- );
- }
-
- // negative case, inputs clearly insufficient
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(220_000),
- &[
- funding_input_sats(100_000),
- ],
- &[],
- true,
- true,
- 2000,
- ),
- Err(format!(
- "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
- Amount::from_sat(expected_fee),
- )),
- );
- }
-
- // barely covers
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(300_000 - expected_fee - 20),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[],
- true,
- true,
- 2000,
- ).unwrap(),
- Amount::from_sat(expected_fee),
- );
- }
-
- // higher fee rate, does not cover
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(298032),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[],
- true,
- true,
- 2200,
- ),
- Err(format!(
- "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.",
- Amount::from_sat(expected_fee),
- )),
- );
- }
-
- // barely covers, less fees (no extra weight, not initiator)
- {
- let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 };
- assert_eq!(
- check_v2_funding_inputs_sufficient(
- Amount::from_sat(300_000 - expected_fee - 20),
- &[
- funding_input_sats(200_000),
- funding_input_sats(100_000),
- ],
- &[],
- false,
- false,
- 2000,
- ).unwrap(),
- Amount::from_sat(expected_fee),
- );
- }
- }
-
fn get_pre_and_post(
pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64,
) -> (u64, u64) {
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index eae26cc..f70f4b1 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -30,7 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::secp256k1::{PublicKey, SecretKey};
-use bitcoin::{secp256k1, Sequence, SignedAmount};
+use bitcoin::{secp256k1, FeeRate, Sequence, SignedAmount};
use crate::blinded_path::message::{
AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext,
@@ -64,7 +64,7 @@ use crate::ln::channel::{
UpdateFulfillCommitFetch, WithChannelContext,
};
use crate::ln::channel_state::ChannelDetails;
-use crate::ln::funding::SpliceContribution;
+use crate::ln::funding::{FundingContribution, FundingTemplate};
use crate::ln::inbound_payment;
use crate::ln::interactivetxs::InteractiveTxMessageSend;
use crate::ln::msgs;
@@ -4546,13 +4546,14 @@ impl<
///
/// # Arguments
///
- /// Provide a `contribution` to determine if value is spliced in or out. The splice initiator is
- /// responsible for paying fees for common fields, shared inputs, and shared outputs along with
- /// any contributed inputs and outputs. Fees are determined using `funding_feerate_per_kw` and
- /// must be covered by the supplied inputs for splice-in or the channel balance for splice-out.
+ /// The splice initiator is responsible for paying fees for common fields, shared inputs, and
+ /// shared outputs along with any contributed inputs and outputs. Fees are determined using
+ /// `feerate` and must be covered by the supplied inputs for splice-in or the channel balance
+ /// for splice-out.
///
- /// An optional `locktime` for the funding transaction may be specified. If not given, the
- /// current best block height is used.
+ /// Returns a [`FundingTemplate`] which should be used to build a [`FundingContribution`] via
+ /// one of its splice methods (e.g., [`FundingTemplate::splice_in_sync`]). The resulting
+ /// contribution must then be passed to [`ChannelManager::funding_contributed`].
///
/// # Events
///
@@ -4570,29 +4571,26 @@ impl<
/// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be
/// emitted with the new funding output. At this point, a new splice can be negotiated by
/// calling `splice_channel` again on this channel.
+ ///
+ /// [`FundingContribution`]: crate::ln::funding::FundingContribution
#[rustfmt::skip]
pub fn splice_channel(
- &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
- contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>,
- ) -> Result<(), APIError> {
- let mut res = Ok(());
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, feerate: FeeRate,
+ ) -> Result<FundingTemplate, APIError> {
+ let mut res = Err(APIError::APIMisuseError { err: String::new() });
PersistenceNotifierGuard::optionally_notify(self, || {
let result = self.internal_splice_channel(
- channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime
+ channel_id, counterparty_node_id, feerate,
);
res = result;
- match res {
- Ok(_) => NotifyOption::DoPersist,
- Err(_) => NotifyOption::SkipPersistNoEvents,
- }
+ NotifyOption::SkipPersistNoEvents
});
res
}
fn internal_splice_channel(
- &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
- contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>,
- ) -> Result<(), APIError> {
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, feerate: FeeRate,
+ ) -> Result<FundingTemplate, APIError> {
let per_peer_state = self.per_peer_state.read().unwrap();
let peer_state_mutex = match per_peer_state
@@ -4618,22 +4616,8 @@ impl<
// Look for the channel
match peer_state.channel_by_id.entry(*channel_id) {
hash_map::Entry::Occupied(mut chan_phase_entry) => {
- let locktime = locktime.unwrap_or_else(|| self.current_best_block().height);
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
- let logger = WithChannelContext::from(&self.logger, &chan.context, None);
- let msg_opt = chan.splice_channel(
- contribution,
- funding_feerate_per_kw,
- locktime,
- &&logger,
- )?;
- if let Some(msg) = msg_opt {
- peer_state.pending_msg_events.push(MessageSendEvent::SendStfu {
- node_id: *counterparty_node_id,
- msg,
- });
- }
- Ok(())
+ chan.splice_channel(feerate)
} else {
Err(APIError::ChannelUnavailable {
err: format!(
@@ -6342,6 +6326,108 @@ impl<
result
}
+ /// Adds or removes funds from the given channel as specified by a [`FundingContribution`].
+ ///
+ /// Used after [`ChannelManager::splice_channel`] by constructing a [`FundingContribution`]
+ /// from the returned [`FundingTemplate`] and passing it here.
+ ///
+ /// Calling this method will commence the process of creating a new funding transaction for the
+ /// channel. An [`Event::FundingTransactionReadyForSigning`] will be generated once the
+ /// transaction is successfully constructed interactively with the counterparty.
+ /// If unsuccessful, an [`Event::SpliceFailed`] will be surfaced instead.
+ ///
+ /// An optional `locktime` for the funding transaction may be specified. If not given, the
+ /// current best block height is used.
+ ///
+ /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
+ /// `counterparty_node_id` is provided.
+ ///
+ /// Returns [`APIMisuseError`] when a channel is not in a state where it is expecting funding
+ /// contribution.
+ ///
+ /// [`ChannelUnavailable`]: APIError::ChannelUnavailable
+ /// [`APIMisuseError`]: APIError::APIMisuseError
+ pub fn funding_contributed(
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
+ contribution: FundingContribution, locktime: Option<u32>,
+ ) -> Result<(), APIError> {
+ let mut result = Ok(());
+ PersistenceNotifierGuard::optionally_notify(self, || {
+ let per_peer_state = self.per_peer_state.read().unwrap();
+ let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id);
+ if peer_state_mutex_opt.is_none() {
+ result = Err(APIError::ChannelUnavailable {
+ err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}")
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ }
+
+ let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap();
+
+ match peer_state.channel_by_id.get_mut(channel_id) {
+ Some(channel) => match channel.as_funded_mut() {
+ Some(chan) => {
+ let locktime = bitcoin::absolute::LockTime::from_consensus(
+ locktime.unwrap_or_else(|| self.current_best_block().height),
+ );
+ let logger = WithChannelContext::from(&self.logger, chan.context(), None);
+ match chan.funding_contributed(contribution, locktime, &&logger) {
+ Ok(msg_opt) => {
+ if let Some(msg) = msg_opt {
+ peer_state.pending_msg_events.push(
+ MessageSendEvent::SendStfu {
+ node_id: *counterparty_node_id,
+ msg,
+ },
+ );
+ }
+ },
+ Err(splice_funding_failed) => {
+ let pending_events = &mut self.pending_events.lock().unwrap();
+ pending_events.push_back((
+ events::Event::SpliceFailed {
+ channel_id: *channel_id,
+ counterparty_node_id: *counterparty_node_id,
+ user_channel_id: channel.context().get_user_id(),
+ abandoned_funding_txo: splice_funding_failed.funding_txo,
+ channel_type: splice_funding_failed.channel_type.clone(),
+ contributed_inputs: splice_funding_failed
+ .contributed_inputs,
+ contributed_outputs: splice_funding_failed
+ .contributed_outputs,
+ },
+ None,
+ ));
+ },
+ }
+
+ return NotifyOption::DoPersist;
+ },
+ None => {
+ result = Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel with id {} not expecting funding contribution",
+ channel_id
+ ),
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ },
+ None => {
+ result = Err(APIError::ChannelUnavailable {
+ err: format!(
+ "Channel with id {} not found for the passed counterparty node_id {}",
+ channel_id, counterparty_node_id
+ ),
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ }
+ });
+
+ result
+ }
+
/// Handles a signed funding transaction generated by interactive transaction construction and
/// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`]
/// event.
@@ -13315,7 +13401,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
});
notify = NotifyOption::SkipPersistHandleEvents;
},
- Err(msg) => log_trace!(logger, "{}", msg),
+ Err((msg, _action)) => log_trace!(logger, "{}", msg),
}
} else {
result = Err(APIError::APIMisuseError {
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 33f78b1..66a0147 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -404,10 +404,10 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>(
}
pub fn provide_anchor_reserves<'a, 'b, 'c>(nodes: &[Node<'a, 'b, 'c>]) -> Transaction {
- provide_anchor_utxo_reserves(nodes, 1, Amount::ONE_BTC)
+ provide_utxo_reserves(nodes, 1, Amount::ONE_BTC)
}
-pub fn provide_anchor_utxo_reserves<'a, 'b, 'c>(
+pub fn provide_utxo_reserves<'a, 'b, 'c>(
nodes: &[Node<'a, 'b, 'c>], utxos: usize, amount: Amount,
) -> Transaction {
let mut output = Vec::with_capacity(nodes.len());
@@ -614,6 +614,10 @@ impl<'a, 'b, 'c> Node<'a, 'b, 'c> {
self.blocks.lock().unwrap()[height as usize].0.header
}
+ pub fn provide_funding_utxos(&self, utxos: usize, amount: Amount) -> Transaction {
+ provide_utxo_reserves(core::slice::from_ref(self), utxos, amount)
+ }
+
/// Executes `enable_channel_signer_op` for every single signer operation for this channel.
#[cfg(test)]
pub fn enable_all_channel_signer_ops(&self, peer_id: &PublicKey, chan_id: &ChannelId) {
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index 9981250..e369bd8 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -9,29 +9,329 @@
//! Types pertaining to funding channels.
-use alloc::vec::Vec;
+use bitcoin::hashes::Hash;
+use bitcoin::secp256k1::PublicKey;
+use bitcoin::{
+ Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut,
+ WScriptHash, Weight,
+};
+
+use core::ops::Deref;
+
+use crate::events::bump_transaction::sync::CoinSelectionSourceSync;
+use crate::events::bump_transaction::{CoinSelectionSource, Input, Utxo};
+use crate::ln::chan_utils::{
+ make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT,
+ FUNDING_TRANSACTION_WITNESS_WEIGHT,
+};
+use crate::ln::interactivetxs::{get_output_weight, TX_COMMON_FIELDS_WEIGHT};
+use crate::ln::msgs;
+use crate::ln::types::ChannelId;
+use crate::ln::LN_MAX_MSG_LEN;
+use crate::prelude::*;
+use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
+use crate::util::async_poll::MaybeSend;
+
+/// A template for contributing to a channel's splice funding transaction.
+///
+/// This is returned from [`ChannelManager::splice_channel`] when a channel is ready to be
+/// spliced. It must be converted to a [`FundingContribution`] using one of the splice methods
+/// and passed to [`ChannelManager::funding_contributed`] in order to resume the splicing
+/// process.
+///
+/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
+/// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FundingTemplate {
+ /// The shared input, which, if present indicates the funding template is for a splice funding
+ /// transaction.
+ shared_input: Option<Input>,
+
+ /// The fee rate to use for coin selection.
+ feerate: FeeRate,
+
+ /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for
+ /// common fields and shared inputs and outputs.
+ is_initiator: bool,
+}
-use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut};
-use bitcoin::{Script, Sequence, Transaction, Weight};
+impl FundingTemplate {
+ /// Constructs a [`FundingTemplate`] for a splice using the provided shared input.
+ pub(super) fn new(
+ shared_input: Option<Input>, feerate: FeeRate, is_initiator: bool,
+ ) -> Self {
+ Self { shared_input, feerate, is_initiator }
+ }
+}
-use crate::events::bump_transaction::Utxo;
-use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT;
-use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
+macro_rules! build_funding_contribution {
+ ($value_added:expr, $outputs:expr, $change_script:expr, $shared_input:expr, $feerate:expr, $is_initiator:expr, $wallet:ident, $($await:tt)*) => {{
+ let value_added: Amount = $value_added;
+ let outputs: Vec<TxOut> = $outputs;
+ let change_script: Option<ScriptBuf> = $change_script;
+ let shared_input: Option<Input> = $shared_input;
+ let feerate: FeeRate = $feerate;
+ let is_initiator: bool = $is_initiator;
+
+ let value_removed = outputs.iter().map(|txout| txout.value).sum();
+ let is_splice = shared_input.is_some();
+
+ let inputs = if value_added == Amount::ZERO {
+ vec![]
+ } else {
+ // Used for creating a redeem script for the new funding txo, since the funding pubkeys
+ // are unknown at this point. Only needed when selecting which UTXOs to include in the
+ // funding tx that would be sufficient to pay for fees. Hence, the value doesn't matter.
+ let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap();
+
+ let shared_output = bitcoin::TxOut {
+ value: shared_input
+ .as_ref()
+ .map(|shared_input| shared_input.previous_utxo.value)
+ .unwrap_or(Amount::ZERO)
+ .checked_add(value_added)
+ .ok_or(())?
+ .checked_sub(value_removed)
+ .ok_or(())?,
+ script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(),
+ };
+
+ let claim_id = None;
+ let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default();
+ let selection = if outputs.is_empty() {
+ let must_pay_to = &[shared_output];
+ $wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)$(.$await)*?
+ } else {
+ let must_pay_to: Vec<_> = outputs.iter().cloned().chain(core::iter::once(shared_output)).collect();
+ $wallet.select_confirmed_utxos(claim_id, must_spend, &must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)$(.$await)*?
+ };
+ selection.confirmed_utxos
+ };
+
+ // NOTE: Must NOT fail after UTXO selection
+
+ let estimated_fee = estimate_transaction_fee(&inputs, &outputs, is_initiator, is_splice, feerate);
+
+ let contribution = FundingContribution {
+ value_added,
+ estimated_fee,
+ inputs,
+ outputs,
+ change_script,
+ feerate,
+ is_initiator,
+ is_splice,
+ };
+
+ Ok(contribution)
+ }};
+}
+
+impl FundingTemplate {
+ /// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
+ /// coin selection.
+ ///
+ /// An optional `change_script` may be given to use as a change output. If `None` and change is
+ /// needed, one will be generated using [`SignerProvider::get_destination_script`].
+ ///
+ /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
+ pub async fn splice_in<W: Deref + MaybeSend>(
+ self, change_script: Option<ScriptBuf>, value_added: Amount, wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSource + MaybeSend,
+ {
+ if value_added == Amount::ZERO {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(value_added, vec![], change_script, shared_input, feerate, is_initiator, wallet, await)
+ }
+
+ /// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
+ /// coin selection.
+ ///
+ /// An optional `change_script` may be given to use as a change output. If `None` and change is
+ /// needed, one will be generated using [`SignerProvider::get_destination_script`].
+ ///
+ /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
+ pub fn splice_in_sync<W: Deref>(
+ self, change_script: Option<ScriptBuf>, value_added: Amount, wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSourceSync,
+ {
+ if value_added == Amount::ZERO {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(
+ value_added,
+ vec![],
+ change_script,
+ shared_input,
+ feerate,
+ is_initiator,
+ wallet,
+ )
+ }
+
+ /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
+ /// perform coin selection.
+ pub async fn splice_out<W: Deref + MaybeSend>(
+ self, outputs: Vec<TxOut>, wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSource + MaybeSend,
+ {
+ if outputs.is_empty() {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(Amount::ZERO, outputs, None, shared_input, feerate, is_initiator, wallet, await)
+ }
+
+ /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
+ /// perform coin selection.
+ pub fn splice_out_sync<W: Deref>(
+ self, outputs: Vec<TxOut>, wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSourceSync,
+ {
+ if outputs.is_empty() {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(
+ Amount::ZERO,
+ outputs,
+ None,
+ shared_input,
+ feerate,
+ is_initiator,
+ wallet,
+ )
+ }
+
+ /// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
+ /// `wallet` to perform coin selection.
+ ///
+ /// An optional `change_script` may be given to use as a change output. If `None` and change is
+ /// needed, one will be generated using [`SignerProvider::get_destination_script`].
+ ///
+ /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
+ pub async fn splice_in_and_out<W: Deref + MaybeSend>(
+ self, change_script: Option<ScriptBuf>, value_added: Amount, outputs: Vec<TxOut>,
+ wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSource + MaybeSend,
+ {
+ if value_added == Amount::ZERO && outputs.is_empty() {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(value_added, outputs, change_script, shared_input, feerate, is_initiator, wallet, await)
+ }
-/// The components of a splice's funding transaction that are contributed by one party.
+ /// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
+ /// `wallet` to perform coin selection.
+ ///
+ /// An optional `change_script` may be given to use as a change output. If `None` and change is
+ /// needed, one will be generated using [`SignerProvider::get_destination_script`].
+ ///
+ /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
+ pub fn splice_in_and_out_sync<W: Deref>(
+ self, change_script: Option<ScriptBuf>, value_added: Amount, outputs: Vec<TxOut>,
+ wallet: W,
+ ) -> Result<FundingContribution, ()>
+ where
+ W::Target: CoinSelectionSourceSync,
+ {
+ if value_added == Amount::ZERO && outputs.is_empty() {
+ return Err(());
+ }
+ let FundingTemplate { shared_input, feerate, is_initiator } = self;
+ build_funding_contribution!(
+ value_added,
+ outputs,
+ change_script,
+ shared_input,
+ feerate,
+ is_initiator,
+ wallet,
+ )
+ }
+}
+
+fn estimate_transaction_fee(
+ inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool,
+ feerate: FeeRate,
+) -> Amount {
+ let input_weight: u64 = inputs
+ .iter()
+ .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight))
+ .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight));
+
+ let output_weight: u64 = outputs
+ .iter()
+ .map(|txout| txout.weight().to_wu())
+ .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight));
+
+ let mut weight = input_weight.saturating_add(output_weight);
+
+ // The initiator pays for all common fields and the shared output in the funding transaction.
+ if is_initiator {
+ weight = weight
+ .saturating_add(TX_COMMON_FIELDS_WEIGHT)
+ // The weight of the funding output, a P2WSH output
+ // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want
+ // to calculate the contributed weight, so we use an all-zero hash.
+ //
+ // TODO(taproot): Needs to consider different weights based on channel type
+ .saturating_add(
+ get_output_weight(&ScriptBuf::new_p2wsh(&WScriptHash::from_raw_hash(
+ Hash::all_zeros(),
+ )))
+ .to_wu(),
+ );
+
+ // The splice initiator pays for the input spending the previous funding output.
+ if is_splice {
+ weight = weight
+ .saturating_add(BASE_INPUT_WEIGHT)
+ .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT)
+ .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT);
+ #[cfg(feature = "grind_signatures")]
+ {
+ // Guarantees a low R signature
+ weight -= 1;
+ }
+ }
+ }
+
+ Weight::from_wu(weight) * feerate
+}
+
+/// The components of a funding transaction contributed by one party.
#[derive(Debug, Clone)]
-pub struct SpliceContribution {
- /// The amount from [`inputs`] to contribute to the splice.
+pub struct FundingContribution {
+ /// The amount to contribute to the channel.
///
- /// [`inputs`]: Self::inputs
+ /// If `value_added` is [`Amount::ZERO`], then any fees will be deducted from the channel
+ /// balance instead of paid by `inputs`.
value_added: Amount,
- /// The inputs included in the splice's funding transaction to meet the contributed amount
- /// plus fees. Any excess amount will be sent to a change output.
+ /// The estimate fees responsible to be paid for the contribution.
+ estimated_fee: Amount,
+
+ /// The inputs included in the funding transaction to meet the contributed amount plus fees. Any
+ /// excess amount will be sent to a change output.
inputs: Vec<FundingTxInput>,
- /// The outputs to include in the splice's funding transaction. The total value of all
- /// outputs plus fees will be the amount that is removed.
+ /// The outputs to include in the funding transaction. The total value of all outputs plus fees
+ /// will be the amount that is removed.
outputs: Vec<TxOut>,
/// An optional change output script. This will be used if needed or, when not set,
@@ -39,63 +339,127 @@ pub struct SpliceContribution {
///
/// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
change_script: Option<ScriptBuf>,
+
+ /// The fee rate used to select `inputs`.
+ feerate: FeeRate,
+
+ /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for
+ /// common fields and shared inputs and outputs.
+ is_initiator: bool,
+
+ /// Whether the contribution is for funding a splice.
+ is_splice: bool,
}
-impl SpliceContribution {
- /// Creates a contribution for when funds are only added to a channel.
- pub fn splice_in(
- value_added: Amount, inputs: Vec<FundingTxInput>, change_script: Option<ScriptBuf>,
- ) -> Self {
- Self { value_added, inputs, outputs: vec![], change_script }
+impl_writeable_tlv_based!(FundingContribution, {
+ (1, value_added, required),
+ (3, estimated_fee, required),
+ (5, inputs, optional_vec),
+ (7, outputs, optional_vec),
+ (9, change_script, option),
+ (11, feerate, required),
+ (13, is_initiator, required),
+ (15, is_splice, required),
+});
+
+impl FundingContribution {
+ pub(super) fn feerate(&self) -> FeeRate {
+ self.feerate
}
- /// Creates a contribution for when funds are only removed from a channel.
- pub fn splice_out(outputs: Vec<TxOut>) -> Self {
- Self { value_added: Amount::ZERO, inputs: vec![], outputs, change_script: None }
+ pub(super) fn is_initiator(&self) -> bool {
+ self.is_initiator
}
- /// Creates a contribution for when funds are both added to and removed from a channel.
- ///
- /// Note that `value_added` represents the value added by `inputs` but should not account for
- /// value removed by `outputs`. The net value contributed can be obtained by calling
- /// [`SpliceContribution::net_value`].
- pub fn splice_in_and_out(
- value_added: Amount, inputs: Vec<FundingTxInput>, outputs: Vec<TxOut>,
- change_script: Option<ScriptBuf>,
- ) -> Self {
- Self { value_added, inputs, outputs, change_script }
+ pub(super) fn is_splice(&self) -> bool {
+ self.is_splice
+ }
+
+ pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
+ let FundingContribution { inputs, outputs, change_script, .. } = self;
+ (inputs, outputs, change_script)
+ }
+
+ pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<OutPoint>, Vec<TxOut>) {
+ (self.inputs.into_iter().map(|input| input.utxo.outpoint).collect(), self.outputs)
}
/// The net value contributed to a channel by the splice. If negative, more value will be
- /// spliced out than spliced in.
- pub fn net_value(&self) -> SignedAmount {
- let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX);
+ /// spliced out than spliced in. Fees will be deducted from the expected splice-out amount
+ /// if no inputs were included.
+ pub fn net_value(&self) -> Result<SignedAmount, String> {
+ for FundingTxInput { utxo, prevtx, .. } in self.inputs.iter() {
+ use crate::util::ser::Writeable;
+ const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
+ channel_id: ChannelId([0; 32]),
+ serial_id: 0,
+ prevtx: None,
+ prevtx_out: 0,
+ sequence: 0,
+ // Mutually exclusive with prevtx, which is accounted for below.
+ shared_input_txid: None,
+ };
+ let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length();
+ if message_len > LN_MAX_MSG_LEN {
+ return Err(format!(
+ "Funding input references a prevtx that is too large for tx_add_input: {}",
+ utxo.outpoint
+ ));
+ }
+ }
+
+ // Fees for splice-out are paid from the channel balance whereas fees for splice-in
+ // are paid by the funding inputs. Therefore, in the case of splice-out, we add the
+ // fees on top of the user-specified contribution. We leave the user-specified
+ // contribution as-is for splice-ins.
+ if !self.inputs.is_empty() {
+ let mut total_input_value = Amount::ZERO;
+ for FundingTxInput { utxo, .. } in self.inputs.iter() {
+ total_input_value = total_input_value
+ .checked_add(utxo.output.value)
+ .ok_or("Sum of input values is greater than the total bitcoin supply")?;
+ }
+
+ // If the inputs are enough to cover intended contribution amount, with fees even when
+ // there is a change output, we are fine.
+ // If the inputs are less, but enough to cover intended contribution amount, with
+ // (lower) fees with no change, we are also fine (change will not be generated).
+ // So it's enough to check considering the lower, no-change fees.
+ //
+ // Note: dust limit is not relevant in this check.
+
+ let contributed_input_value = self.value_added;
+ let estimated_fee = self.estimated_fee;
+ let minimal_input_amount_needed = contributed_input_value
+ .checked_add(estimated_fee)
+ .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?;
+ if total_input_value < minimal_input_amount_needed {
+ return Err(format!(
+ "Total input amount {total_input_value} is lower than needed for splice-in contribution {contributed_input_value}, considering fees of {estimated_fee}. Need more inputs.",
+ ));
+ }
+ }
+
+ let unpaid_fees = if self.inputs.is_empty() { self.estimated_fee } else { Amount::ZERO }
+ .to_signed()
+ .expect("fees should never exceed Amount::MAX_MONEY");
+ let value_added = self.value_added.to_signed().map_err(|_| "Value added too large")?;
let value_removed = self
.outputs
.iter()
.map(|txout| txout.value)
.sum::<Amount>()
.to_signed()
- .unwrap_or(SignedAmount::MAX);
+ .map_err(|_| "Value removed too large")?;
- value_added - value_removed
- }
+ let contribution_amount = value_added - value_removed;
+ let adjusted_contribution = contribution_amount.checked_sub(unpaid_fees).ok_or(format!(
+ "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply",
+ contribution_amount.unsigned_abs(),
+ self.estimated_fee,
+ ))?;
- pub(super) fn value_added(&self) -> Amount {
- self.value_added
- }
-
- pub(super) fn inputs(&self) -> &[FundingTxInput] {
- &self.inputs[..]
- }
-
- pub(super) fn outputs(&self) -> &[TxOut] {
- &self.outputs[..]
- }
-
- pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
- let SpliceContribution { value_added: _, inputs, outputs, change_script } = self;
- (inputs, outputs, change_script)
+ Ok(adjusted_contribution)
}
}
@@ -267,3 +631,260 @@ impl FundingTxInput {
self.utxo.output
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::{estimate_transaction_fee, FundingContribution, FundingTxInput};
+ use bitcoin::hashes::Hash;
+ use bitcoin::transaction::{Transaction, TxOut, Version};
+ use bitcoin::{Amount, FeeRate, ScriptBuf, SignedAmount, WPubkeyHash};
+
+ #[test]
+ #[rustfmt::skip]
+ fn test_estimate_transaction_fee() {
+ let one_input = [funding_input_sats(1_000)];
+ let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)];
+
+ // 2 inputs, initiator, 2000 sat/kw feerate
+ assert_eq!(
+ estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }),
+ );
+
+ // higher feerate
+ assert_eq!(
+ estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(3000)),
+ Amount::from_sat(if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }),
+ );
+
+ // only 1 input
+ assert_eq!(
+ estimate_transaction_fee(&one_input, &[], true, false, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(if cfg!(feature = "grind_signatures") { 970 } else { 972 }),
+ );
+
+ // 0 inputs
+ assert_eq!(
+ estimate_transaction_fee(&[], &[], true, false, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(428),
+ );
+
+ // not initiator
+ assert_eq!(
+ estimate_transaction_fee(&[], &[], false, false, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(0),
+ );
+
+ // splice initiator
+ assert_eq!(
+ estimate_transaction_fee(&one_input, &[], true, true, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }),
+ );
+
+ // splice acceptor
+ assert_eq!(
+ estimate_transaction_fee(&one_input, &[], false, true, FeeRate::from_sat_per_kwu(2000)),
+ Amount::from_sat(if cfg!(feature = "grind_signatures") { 542 } else { 544 }),
+ );
+ }
+
+ #[rustfmt::skip]
+ fn funding_input_sats(input_value_sats: u64) -> FundingTxInput {
+ let prevout = TxOut {
+ value: Amount::from_sat(input_value_sats),
+ script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
+ };
+ let prevtx = Transaction {
+ input: vec![], output: vec![prevout],
+ version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO,
+ };
+
+ FundingTxInput::new_p2wpkh(prevtx, 0).unwrap()
+ }
+
+ fn funding_output_sats(output_value_sats: u64) -> TxOut {
+ TxOut {
+ value: Amount::from_sat(output_value_sats),
+ script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
+ }
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn test_check_v2_funding_inputs_sufficient() {
+ // positive case, inputs well over intended contribution
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(220_000),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap()));
+ }
+
+ // Net splice-in
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(220_000),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![
+ funding_output_sats(200_000),
+ ],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(contribution.net_value(), Ok(SignedAmount::from_sat(220_000 - 200_000)));
+ }
+
+ // Net splice-out
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(220_000),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![
+ funding_output_sats(400_000),
+ ],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(contribution.net_value(), Ok(SignedAmount::from_sat(220_000 - 400_000)));
+ }
+
+ // Net splice-out, inputs insufficient to cover fees
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(220_000),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![
+ funding_output_sats(400_000),
+ ],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(90000),
+ };
+ assert_eq!(
+ contribution.net_value(),
+ Err(format!(
+ "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
+ Amount::from_sat(expected_fee),
+ )),
+ );
+ }
+
+ // negative case, inputs clearly insufficient
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(220_000),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(
+ contribution.net_value(),
+ Err(format!(
+ "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
+ Amount::from_sat(expected_fee),
+ )),
+ );
+ }
+
+ // barely covers
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(300_000 - expected_fee - 20),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap()));
+ }
+
+ // higher fee rate, does not cover
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(298032),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![],
+ change_script: None,
+ is_initiator: true,
+ is_splice: true,
+ feerate: FeeRate::from_sat_per_kwu(2200),
+ };
+ assert_eq!(
+ contribution.net_value(),
+ Err(format!(
+ "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.",
+ Amount::from_sat(expected_fee),
+ )),
+ );
+ }
+
+ // barely covers, less fees (no extra weight, not initiator)
+ {
+ let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 };
+ let contribution = FundingContribution {
+ value_added: Amount::from_sat(300_000 - expected_fee - 20),
+ estimated_fee: Amount::from_sat(expected_fee),
+ inputs: vec![
+ funding_input_sats(200_000),
+ funding_input_sats(100_000),
+ ],
+ outputs: vec![],
+ change_script: None,
+ is_initiator: false,
+ is_splice: false,
+ feerate: FeeRate::from_sat_per_kwu(2000),
+ };
+ assert_eq!(contribution.net_value(), Ok(contribution.value_added.to_signed().unwrap()));
+ }
+ }
+}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 4846f71..31c13e1 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -13,13 +13,13 @@ use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW};
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::ChannelMonitorUpdateStatus;
-use crate::events::bump_transaction::sync::WalletSourceSync;
+use crate::events::bump_transaction::sync::{WalletSourceSync, WalletSync};
use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType};
use crate::ln::chan_utils;
use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY;
use crate::ln::channelmanager::{provided_init_features, PaymentId, BREAKDOWN_TIMEOUT};
use crate::ln::functional_test_utils::*;
-use crate::ln::funding::{FundingTxInput, SpliceContribution};
+use crate::ln::funding::FundingContribution;
use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
use crate::ln::outbound_payment::RecipientOnionFields;
use crate::ln::types::ChannelId;
@@ -27,10 +27,14 @@ use crate::routing::router::{PaymentParameters, RouteParameters};
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
+use crate::sync::Arc;
+
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::PublicKey;
-use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash};
+use bitcoin::{
+ Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash,
+};
#[test]
fn test_splicing_not_supported_api_error() {
@@ -47,15 +51,8 @@ fn test_splicing_not_supported_api_error() {
let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1);
- let bs_contribution = SpliceContribution::splice_in(Amount::ZERO, Vec::new(), None);
-
- let res = nodes[1].node.splice_channel(
- &channel_id,
- &node_id_0,
- bs_contribution.clone(),
- 0, // funding_feerate_per_kw,
- None, // locktime
- );
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support splicing"))
@@ -76,13 +73,7 @@ fn test_splicing_not_supported_api_error() {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
- let res = nodes[1].node.splice_channel(
- &channel_id,
- &node_id_0,
- bs_contribution,
- 0, // funding_feerate_per_kw,
- None, // locktime
- );
+ let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate);
match res {
Err(APIError::ChannelUnavailable { err }) => {
assert!(err.contains("Peer does not support quiescence, a splicing prerequisite"))
@@ -102,64 +93,122 @@ fn test_v1_splice_in_negative_insufficient_inputs() {
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
// Amount being added to the channel through the splice-in
- let splice_in_sats = 20_000;
+ let splice_in_value = Amount::from_sat(20_000);
// Create additional inputs, but insufficient
- let extra_splice_funding_input_sats = splice_in_sats - 1;
- let funding_inputs =
- create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]);
+ let extra_splice_funding_input = splice_in_value - Amount::ONE_SAT;
- let contribution =
- SpliceContribution::splice_in(Amount::from_sat(splice_in_sats), funding_inputs, None);
+ provide_utxo_reserves(&nodes, 1, extra_splice_funding_input);
+
+ let feerate = FeeRate::from_sat_per_kwu(1024);
// Initiate splice-in, with insufficient input contribution
- let res = nodes[0].node.splice_channel(
- &channel_id,
- &nodes[1].node.get_our_node_id(),
- contribution,
- 1024, // funding_feerate_per_kw,
- None, // locktime
- );
- match res {
- Err(APIError::APIMisuseError { err }) => {
- assert!(err.contains("Need more inputs"))
- },
- _ => panic!("Wrong error {:?}", res.err().unwrap()),
- }
+ let funding_template = nodes[0]
+ .node
+ .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), feerate)
+ .unwrap();
+
+ let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
+ assert!(funding_template.splice_in_sync(None, splice_in_value, &wallet).is_err());
}
pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- initiator_contribution: SpliceContribution,
+ funding_contribution: FundingContribution,
) {
- let new_funding_script =
- complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone());
+ let new_funding_script = complete_splice_handshake(initiator, acceptor);
+
complete_interactive_funding_negotiation(
initiator,
acceptor,
channel_id,
- initiator_contribution,
+ funding_contribution,
new_funding_script,
);
}
-pub fn complete_splice_handshake<'a, 'b, 'c, 'd>(
+pub fn initiate_splice_in<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- initiator_contribution: SpliceContribution,
-) -> ScriptBuf {
- let node_id_initiator = initiator.node.get_our_node_id();
+ value_added: Amount,
+) -> FundingContribution {
+ let change_script = Some(initiator.wallet_source.get_change_script().unwrap());
+ do_initiate_splice_in(initiator, acceptor, channel_id, value_added, change_script)
+}
+
+pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ value_added: Amount, change_script: Option<ScriptBuf>,
+) -> FundingContribution {
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+ let funding_template =
+ initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
+ let funding_contribution =
+ funding_template.splice_in_sync(change_script, value_added, &wallet).unwrap();
+ initiator
+ .node
+ .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
+ .unwrap();
+ funding_contribution
+}
+
+pub fn initiate_splice_out<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ outputs: Vec<TxOut>,
+) -> FundingContribution {
let node_id_acceptor = acceptor.node.get_our_node_id();
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+ let funding_template =
+ initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
+ let funding_contribution = funding_template.splice_out_sync(outputs, &wallet).unwrap();
+ initiator
+ .node
+ .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
+ .unwrap();
+ funding_contribution
+}
+
+pub fn initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ value_added: Amount, outputs: Vec<TxOut>,
+) -> FundingContribution {
+ let change_script = Some(initiator.wallet_source.get_change_script().unwrap());
+ do_initiate_splice_in_and_out(
+ initiator,
+ acceptor,
+ channel_id,
+ value_added,
+ outputs,
+ change_script,
+ )
+}
+pub fn do_initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ value_added: Amount, outputs: Vec<TxOut>, change_script: Option<ScriptBuf>,
+) -> FundingContribution {
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+ let funding_template =
+ initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
+ let funding_contribution = funding_template
+ .splice_in_and_out_sync(change_script, value_added, outputs, &wallet)
+ .unwrap();
initiator
.node
- .splice_channel(
- &channel_id,
- &node_id_acceptor,
- initiator_contribution,
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
+ .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
.unwrap();
+ funding_contribution
+}
+
+pub fn complete_splice_handshake<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>,
+) -> ScriptBuf {
+ let node_id_initiator = initiator.node.get_our_node_id();
+ let node_id_acceptor = acceptor.node.get_our_node_id();
let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor);
acceptor.node.handle_stfu(node_id_initiator, &stfu_init);
@@ -182,7 +231,7 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>(
pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- initiator_contribution: SpliceContribution, new_funding_script: ScriptBuf,
+ initiator_contribution: FundingContribution, new_funding_script: ScriptBuf,
) {
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
@@ -358,19 +407,18 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>(
pub fn splice_channel<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- initiator_contribution: SpliceContribution,
+ funding_contribution: FundingContribution,
) -> Transaction {
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
- let new_funding_script =
- complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone());
+ let new_funding_script = complete_splice_handshake(initiator, acceptor);
complete_interactive_funding_negotiation(
initiator,
acceptor,
channel_id,
- initiator_contribution,
+ funding_contribution,
new_funding_script,
);
let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, false);
@@ -384,20 +432,20 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>(
node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, num_blocks: u32,
-) {
+) -> Option<MessageSendEvent> {
connect_blocks(node_a, num_blocks);
connect_blocks(node_b, num_blocks);
let node_id_b = node_b.node.get_our_node_id();
let splice_locked_for_node_b =
get_event_msg!(node_a, MessageSendEvent::SendSpliceLocked, node_id_b);
- lock_splice(node_a, node_b, &splice_locked_for_node_b, false);
+ lock_splice(node_a, node_b, &splice_locked_for_node_b, false)
}
pub fn lock_splice<'a, 'b, 'c, 'd>(
node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>,
splice_locked_for_node_b: &msgs::SpliceLocked, is_0conf: bool,
-) {
+) -> Option<MessageSendEvent> {
let (prev_funding_outpoint, prev_funding_script) = node_a
.chain_monitor
.chain_monitor
@@ -411,6 +459,15 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
node_b.node.handle_splice_locked(node_id_a, splice_locked_for_node_b);
let mut msg_events = node_b.node.get_and_clear_pending_msg_events();
+
+ // If the acceptor had a pending QuiescentAction, return the stfu message so that it can be used
+ // for the next splice attempt.
+ let node_b_stfu = msg_events
+ .last()
+ .filter(|event| matches!(event, MessageSendEvent::SendStfu { .. }))
+ .is_some()
+ .then(|| msg_events.pop().unwrap());
+
assert_eq!(msg_events.len(), if is_0conf { 1 } else { 2 }, "{msg_events:?}");
if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) {
node_a.node.handle_splice_locked(node_id_b, &msg);
@@ -457,6 +514,8 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
.chain_source
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone());
node_b.chain_source.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
+
+ node_b_stfu
}
#[test]
@@ -501,20 +560,11 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000);
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
- nodes[0]
- .node
- .splice_channel(
- &channel_id,
- &node_id_1,
- contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
- .unwrap();
+ }];
+ let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone());
// Attempt a splice negotiation that only goes up to receiving `splice_init`. Reconnecting
// should implicitly abort the negotiation and reset the splice state such that we're able to
@@ -559,16 +609,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
- nodes[0]
- .node
- .splice_channel(
- &channel_id,
- &node_id_1,
- contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
- .unwrap();
+ let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone());
// Attempt a splice negotiation that ends mid-construction of the funding transaction.
// Reconnecting should implicitly abort the negotiation and reset the splice state such that
@@ -618,16 +659,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
- nodes[0]
- .node
- .splice_channel(
- &channel_id,
- &node_id_1,
- contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
- .unwrap();
+ let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone());
// Attempt a splice negotiation that ends before the initial `commitment_signed` messages are
// exchanged. The node missing the other's `commitment_signed` upon reconnecting should
@@ -705,7 +737,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
// Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting
// should not abort the negotiation or reset the splice state.
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, contribution);
+ let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
if reload {
let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode();
@@ -757,20 +790,11 @@ fn test_config_reject_inbound_splices() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000);
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
- nodes[0]
- .node
- .splice_channel(
- &channel_id,
- &node_id_1,
- contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
- .unwrap();
+ }];
+ let _ = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone());
let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu);
@@ -798,7 +822,8 @@ fn test_config_reject_inbound_splices() {
reconnect_args.send_announcement_sigs = (true, true);
reconnect_nodes(reconnect_args);
- let _ = splice_channel(&nodes[1], &nodes[0], channel_id, contribution);
+ let funding_contribution = initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs);
+ let _ = splice_channel(&nodes[1], &nodes[0], channel_id, funding_contribution);
}
#[test]
@@ -816,24 +841,23 @@ fn test_splice_in() {
let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000);
- let coinbase_tx1 = provide_anchor_reserves(&nodes);
- let coinbase_tx2 = provide_anchor_reserves(&nodes);
-
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
+ let utxo_value = added_value * 3 / 4;
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
let fees = Amount::from_sat(321);
- let initiator_contribution = SpliceContribution::splice_in(
+ provide_utxo_reserves(&nodes, 2, utxo_value);
+
+ let funding_contribution = do_initiate_splice_in(
+ &nodes[0],
+ &nodes[1],
+ channel_id,
added_value,
- vec![
- FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
- FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
- ],
Some(change_script.clone()),
);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
- let expected_change = Amount::ONE_BTC * 2 - added_value - fees;
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
expected_change,
@@ -868,7 +892,7 @@ fn test_splice_out() {
let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000);
- let initiator_contribution = SpliceContribution::splice_out(vec![
+ let outputs = vec![
TxOut {
value: Amount::from_sat(initial_channel_value_sat / 4),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
@@ -877,9 +901,10 @@ fn test_splice_out() {
value: Amount::from_sat(initial_channel_value_sat / 4),
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
},
- ]);
+ ];
+ let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
@@ -909,14 +934,12 @@ fn test_splice_in_and_out() {
let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000);
- let coinbase_tx1 = provide_anchor_reserves(&nodes);
- let coinbase_tx2 = provide_anchor_reserves(&nodes);
-
// Contribute a net negative value, with fees taken from the contributed inputs and the
// remaining value sent to change
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
let added_value = Amount::from_sat(htlc_limit_msat / 1000);
let removed_value = added_value * 2;
+ let utxo_value = added_value * 3 / 4;
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
let fees = if cfg!(feature = "grind_signatures") {
Amount::from_sat(383)
@@ -926,27 +949,29 @@ fn test_splice_in_and_out() {
assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000);
- let initiator_contribution = SpliceContribution::splice_in_and_out(
+ provide_utxo_reserves(&nodes, 2, utxo_value);
+
+ let outputs = vec![
+ TxOut {
+ value: removed_value / 2,
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ },
+ TxOut {
+ value: removed_value / 2,
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ },
+ ];
+ let funding_contribution = do_initiate_splice_in_and_out(
+ &nodes[0],
+ &nodes[1],
+ channel_id,
added_value,
- vec![
- FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
- FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
- ],
- vec![
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- },
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
- },
- ],
+ outputs,
Some(change_script.clone()),
);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
- let expected_change = Amount::ONE_BTC * 2 - added_value - fees;
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
expected_change,
@@ -965,13 +990,11 @@ fn test_splice_in_and_out() {
assert!(htlc_limit_msat < added_value.to_sat() * 1000);
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
- let coinbase_tx1 = provide_anchor_reserves(&nodes);
- let coinbase_tx2 = provide_anchor_reserves(&nodes);
-
// Contribute a net positive value, with fees taken from the contributed inputs and the
// remaining value sent to change
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
let removed_value = added_value / 2;
+ let utxo_value = added_value * 3 / 4;
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
let fees = if cfg!(feature = "grind_signatures") {
Amount::from_sat(383)
@@ -979,27 +1002,32 @@ fn test_splice_in_and_out() {
Amount::from_sat(384)
};
- let initiator_contribution = SpliceContribution::splice_in_and_out(
+ // Clear UTXOs so that the change output from the previous splice isn't considered
+ nodes[0].wallet_source.clear_utxos();
+
+ provide_utxo_reserves(&nodes, 2, utxo_value);
+
+ let outputs = vec![
+ TxOut {
+ value: removed_value / 2,
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ },
+ TxOut {
+ value: removed_value / 2,
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ },
+ ];
+ let funding_contribution = do_initiate_splice_in_and_out(
+ &nodes[0],
+ &nodes[1],
+ channel_id,
added_value,
- vec![
- FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
- FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
- ],
- vec![
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- },
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
- },
- ],
+ outputs,
Some(change_script.clone()),
);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
- let expected_change = Amount::ONE_BTC * 2 - added_value - fees;
+ let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
expected_change,
@@ -1016,46 +1044,108 @@ fn test_splice_in_and_out() {
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000);
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
+}
- let coinbase_tx1 = provide_anchor_reserves(&nodes);
- let coinbase_tx2 = provide_anchor_reserves(&nodes);
+#[test]
+fn test_fails_initiating_concurrent_splices() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let config = test_default_channel_config();
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
- // Fail adding a net contribution value of zero
- let added_value = Amount::from_sat(initial_channel_value_sat * 2);
- let removed_value = added_value;
- let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+ let node_0_id = nodes[0].node.get_our_node_id();
+ let node_1_id = nodes[1].node.get_our_node_id();
- let initiator_contribution = SpliceContribution::splice_in_and_out(
- added_value,
- vec![
- FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
- FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
- ],
- vec![
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- },
- TxOut {
- value: removed_value / 2,
- script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
- },
- ],
- Some(change_script),
+ provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC);
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(initial_channel_value_sat / 4),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }];
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
+ let funding_contribution = funding_template.splice_out_sync(outputs.clone(), &wallet).unwrap();
+ nodes[0]
+ .node
+ .funding_contributed(&channel_id, &node_1_id, funding_contribution.clone(), None)
+ .unwrap();
+
+ assert_eq!(
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
+ Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel {} cannot be spliced as one is waiting to be negotiated",
+ channel_id
+ ),
+ }),
);
+ let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]);
+
assert_eq!(
- nodes[0].node.splice_channel(
- &channel_id,
- &nodes[1].node.get_our_node_id(),
- initiator_contribution,
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- ),
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
Err(APIError::APIMisuseError {
- err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id),
+ err: format!(
+ "Channel {} cannot be spliced as one is currently being negotiated",
+ channel_id
+ ),
}),
);
+
+ // The acceptor can enqueue a quiescent action while the current splice is pending.
+ let added_value = Amount::from_sat(initial_channel_value_sat);
+ let acceptor_template = nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate).unwrap();
+ let acceptor_wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
+ let change_script = Some(nodes[1].wallet_source.get_change_script().unwrap());
+ let acceptor_contribution =
+ acceptor_template.splice_in_sync(change_script, added_value, &acceptor_wallet).unwrap();
+ nodes[1]
+ .node
+ .funding_contributed(&channel_id, &node_0_id, acceptor_contribution, None)
+ .unwrap();
+
+ complete_interactive_funding_negotiation(
+ &nodes[0],
+ &nodes[1],
+ channel_id,
+ funding_contribution,
+ new_funding_script,
+ );
+
+ assert_eq!(
+ nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate),
+ Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel {} cannot be spliced as one is currently being negotiated",
+ channel_id
+ ),
+ }),
+ );
+
+ let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ assert!(splice_locked.is_none());
+
+ expect_splice_pending_event(&nodes[0], &node_1_id);
+ expect_splice_pending_event(&nodes[1], &node_0_id);
+
+ // Now that the splice is pending, another splice may be initiated.
+ assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id, feerate).is_ok());
+
+ mine_transaction(&nodes[0], &splice_tx);
+ mine_transaction(&nodes[1], &splice_tx);
+ let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
+
+ // However, the acceptor had enqueued a quiescent action while the splice was pending, so it
+ // will now attempt to initiate quiescence.
+ assert!(
+ matches!(stfu, Some(MessageSendEvent::SendStfu { node_id, .. }) if node_id == node_0_id)
+ );
}
#[cfg(test)]
@@ -1091,16 +1181,19 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs:
let (_, _, channel_id, initial_funding_tx) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
+ let coinbase_tx = provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
// We want to have two HTLCs pending to make sure we can claim those sent before and after a
// splice negotiation.
let payment_amount = 1_000_000;
let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount);
+
let splice_in_amount = initial_channel_capacity / 2;
- let initiator_contribution = SpliceContribution::splice_in(
+ let initiator_contribution = do_initiate_splice_in(
+ &nodes[0],
+ &nodes[1],
+ channel_id,
Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()],
Some(nodes[0].wallet_source.get_change_script().unwrap()),
);
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
@@ -1296,7 +1389,7 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
// Negotiate the splice up until the nodes exchange `tx_complete`.
- let initiator_contribution = SpliceContribution::splice_out(vec![
+ let outputs = vec![
TxOut {
value: Amount::from_sat(initial_channel_value_sat / 4),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
@@ -1305,7 +1398,8 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
value: Amount::from_sat(initial_channel_value_sat / 4),
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
},
- ]);
+ ];
+ let initiator_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution);
// Node 0 should have a signing event to handle since they had a contribution in the splice.
@@ -1582,36 +1676,35 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) {
nodes[1].node.peer_disconnected(node_id_0);
let splice_out_sat = initial_channel_value_sat / 4;
- let node_0_contribution = SpliceContribution::splice_out(vec![TxOut {
+ let node_0_outputs = vec![TxOut {
value: Amount::from_sat(splice_out_sat),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
+ let node_0_funding_contribution =
+ funding_template.splice_out_sync(node_0_outputs, &wallet).unwrap();
nodes[0]
.node
- .splice_channel(
- &channel_id,
- &node_id_1,
- node_0_contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
+ .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
.unwrap();
+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
- let node_1_contribution = SpliceContribution::splice_out(vec![TxOut {
+ let node_1_outputs = vec![TxOut {
value: Amount::from_sat(splice_out_sat),
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let funding_template = nodes[1].node.splice_channel(&channel_id, &node_id_0, feerate).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
+ let node_1_funding_contribution =
+ funding_template.splice_out_sync(node_1_outputs, &wallet).unwrap();
nodes[1]
.node
- .splice_channel(
- &channel_id,
- &node_id_0,
- node_1_contribution.clone(),
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
+ .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
.unwrap();
+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
if reload {
@@ -1644,6 +1737,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) {
}
reconnect_args.send_stfu = (true, true);
reconnect_nodes(reconnect_args);
+
let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1);
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
@@ -1667,7 +1761,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) {
&nodes[0],
&nodes[1],
channel_id,
- node_0_contribution,
+ node_0_funding_contribution,
new_funding_script,
);
let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], use_0conf);
@@ -1806,7 +1900,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) {
&nodes[1],
&nodes[0],
channel_id,
- node_1_contribution,
+ node_1_funding_contribution,
new_funding_script,
);
let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[1], &nodes[0], use_0conf);
@@ -1845,17 +1939,15 @@ fn disconnect_on_unexpected_interactive_tx_message() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
+ provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
+
let splice_in_amount = initial_channel_capacity / 2;
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
+ let contribution =
+ initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount));
// Complete interactive-tx construction, but fail by having the acceptor send a duplicate
// tx_complete instead of commitment_signed.
- negotiate_splice_tx(initiator, acceptor, channel_id, contribution.clone());
+ negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
let _ = get_event!(initiator, Event::FundingTransactionReadyForSigning);
let _ = get_htlc_update_msgs(acceptor, &node_id_initiator);
@@ -1883,17 +1975,15 @@ fn fail_splice_on_interactive_tx_error() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
+ provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
+
let splice_in_amount = initial_channel_capacity / 2;
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
// Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead
// of sending tx_complete. The failure occurs because the serial id will have the wrong parity.
- let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
+ let funding_contribution =
+ initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount));
+ let _ = complete_splice_handshake(initiator, acceptor);
let tx_add_input =
get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
@@ -1907,7 +1997,7 @@ fn fail_splice_on_interactive_tx_error() {
match event {
Event::SpliceFailed { contributed_inputs, .. } => {
assert_eq!(contributed_inputs.len(), 1);
- assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint());
+ assert_eq!(contributed_inputs[0], funding_contribution.into_tx_parts().0[0].outpoint());
},
_ => panic!("Expected Event::SpliceFailed"),
}
@@ -1936,17 +2026,15 @@ fn fail_splice_on_tx_abort() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
+ provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
+
let splice_in_amount = initial_channel_capacity / 2;
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
// Fail during interactive-tx construction by having the acceptor send tx_abort instead of
// tx_complete.
- let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
+ let funding_contribution =
+ initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount));
+ let _ = complete_splice_handshake(initiator, acceptor);
let tx_add_input =
get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
@@ -1963,7 +2051,7 @@ fn fail_splice_on_tx_abort() {
match event {
Event::SpliceFailed { contributed_inputs, .. } => {
assert_eq!(contributed_inputs.len(), 1);
- assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint());
+ assert_eq!(contributed_inputs[0], funding_contribution.into_tx_parts().0[0].outpoint());
},
_ => panic!("Expected Event::SpliceFailed"),
}
@@ -1989,16 +2077,13 @@ fn fail_splice_on_channel_close() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
+ provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
+
let splice_in_amount = initial_channel_capacity / 2;
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
// Close the channel before completion of interactive-tx construction.
- let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
+ let _ = initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount));
+ let _ = complete_splice_handshake(initiator, acceptor);
let _tx_add_input =
get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
@@ -2039,25 +2124,12 @@ fn fail_quiescent_action_on_channel_close() {
let (_, _, channel_id, _) =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
- let coinbase_tx = provide_anchor_reserves(&nodes);
let splice_in_amount = initial_channel_capacity / 2;
- let contribution = SpliceContribution::splice_in(
- Amount::from_sat(splice_in_amount),
- vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
+
+ provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC);
// Close the channel before completion of STFU handshake.
- initiator
- .node
- .splice_channel(
- &channel_id,
- &node_id_acceptor,
- contribution,
- FEERATE_FLOOR_SATS_PER_KW,
- None,
- )
- .unwrap();
+ let _ = initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount));
let _stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor);
@@ -2134,19 +2206,21 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw
// Splice both channels, lock them, and connect enough blocks to trigger the legacy SCID pruning
// logic while the HTLC is still pending.
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs_0_1 = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id_0_1, outputs_0_1);
let splice_tx_0_1 = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution);
for node in &nodes {
mine_transaction(node, &splice_tx_0_1);
}
- let contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs_1_2 = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let contribution = initiate_splice_out(&nodes[1], &nodes[2], channel_id_1_2, outputs_1_2);
let splice_tx_1_2 = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution);
for node in &nodes {
mine_transaction(node, &splice_tx_1_2);
@@ -2250,10 +2324,11 @@ fn test_splice_buffer_commitment_signed_until_funding_tx_signed() {
// Negotiate a splice-out where only the initiator (node 0) has a contribution.
// This means node 1 will send their commitment_signed immediately after tx_complete.
- let initiator_contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let initiator_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution);
// Node 0 (initiator with contribution) should have a signing event to handle.
@@ -2370,10 +2445,11 @@ fn test_splice_buffer_invalid_commitment_signed_closes_channel() {
// Negotiate a splice-out where only the initiator (node 0) has a contribution.
// This means node 1 will send their commitment_signed immediately after tx_complete.
- let initiator_contribution = SpliceContribution::splice_out(vec![TxOut {
+ let outputs = vec![TxOut {
value: Amount::from_sat(1_000),
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
- }]);
+ }];
+ let initiator_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution);
// Node 0 (initiator with contribution) should have a signing event to handle.
diff --git a/lightning/src/ln/zero_fee_commitment_tests.rs b/lightning/src/ln/zero_fee_commitment_tests.rs
index d287b6e..b722155 100644
--- a/lightning/src/ln/zero_fee_commitment_tests.rs
+++ b/lightning/src/ln/zero_fee_commitment_tests.rs
@@ -129,7 +129,7 @@ fn test_htlc_claim_chunking() {
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &configs);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
- let coinbase_tx = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500));
+ let coinbase_tx = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500));
const CHAN_CAPACITY: u64 = 10_000_000;
let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value(
@@ -319,7 +319,7 @@ fn test_anchor_tx_too_big() {
let node_a_id = nodes[0].node.get_our_node_id();
- let _coinbase_tx_a = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500));
+ let _coinbase_tx_a = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500));
const CHAN_CAPACITY: u64 = 10_000_000;
let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value(
diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs
index 6579c03..2eace55 100644
--- a/lightning/src/util/ser.rs
+++ b/lightning/src/util/ser.rs
@@ -41,6 +41,7 @@ use bitcoin::secp256k1::ecdsa;
use bitcoin::secp256k1::schnorr;
use bitcoin::secp256k1::{PublicKey, SecretKey};
use bitcoin::transaction::{OutPoint, Transaction, TxOut};
+use bitcoin::FeeRate;
use bitcoin::{consensus, Sequence, TxIn, Weight, Witness};
use dnssec_prover::rr::Name;
@@ -1426,6 +1427,19 @@ impl Readable for Weight {
}
}
+impl Writeable for FeeRate {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.to_sat_per_kwu().write(w)
+ }
+}
+
+impl Readable for FeeRate {
+ fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
+ let sat_kwu: u64 = Readable::read(r)?;
+ Ok(FeeRate::from_sat_per_kwu(sat_kwu))
+ }
+}
+
impl Writeable for Txid {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
w.write_all(&self[..])
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.