Skip to content

Commit 25df31a

Browse files
committed
WIP: Check all funding transactions
1 parent 7ccb545 commit 25df31a

File tree

2 files changed

+304
-62
lines changed

2 files changed

+304
-62
lines changed

lightning/src/ln/channel.rs

+282-61
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,32 @@ impl FundingScope {
18571857
#[cfg(splicing)]
18581858
struct PendingSplice {
18591859
pub our_funding_contribution: i64,
1860+
sent_funding_txid: Option<Txid>,
1861+
received_funding_txid: Option<Txid>,
1862+
}
1863+
1864+
/// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`].
1865+
struct ConfirmedTransaction<'a> {
1866+
tx: &'a Transaction,
1867+
txid: Option<Txid>,
1868+
}
1869+
1870+
impl<'a> ConfirmedTransaction<'a> {
1871+
/// Returns the underlying [`Transaction`].
1872+
pub fn tx(&self) -> &'a Transaction {
1873+
self.tx
1874+
}
1875+
1876+
/// Returns the [`Txid`], computing and caching it if necessary.
1877+
pub fn txid(&mut self) -> Txid {
1878+
*self.txid.get_or_insert_with(|| self.tx.compute_txid())
1879+
}
1880+
}
1881+
1882+
impl<'a> From<&'a Transaction> for ConfirmedTransaction<'a> {
1883+
fn from(tx: &'a Transaction) -> Self {
1884+
ConfirmedTransaction { tx, txid: None }
1885+
}
18601886
}
18611887

18621888
/// Contains everything about the channel including state, and various flags.
@@ -4859,6 +4885,40 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
48594885
self.get_initial_counterparty_commitment_signature(funding, logger)
48604886
}
48614887

4888+
#[cfg(splicing)]
4889+
fn check_get_splice_locked<L: Deref>(
4890+
&mut self, pending_splice: &PendingSplice, funding: &mut FundingScope, height: u32,
4891+
logger: &L,
4892+
) -> Option<msgs::SpliceLocked>
4893+
where
4894+
L::Target: Logger,
4895+
{
4896+
if !self.check_funding_confirmations(funding, height) {
4897+
return None;
4898+
}
4899+
4900+
let confirmed_funding_txid = match funding.get_funding_txid() {
4901+
Some(funding_txid) => funding_txid,
4902+
None => {
4903+
debug_assert!(false);
4904+
return None;
4905+
},
4906+
};
4907+
4908+
match pending_splice.sent_funding_txid {
4909+
Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => {
4910+
debug_assert!(false);
4911+
None
4912+
},
4913+
_ => {
4914+
Some(msgs::SpliceLocked {
4915+
channel_id: self.channel_id(),
4916+
splice_txid: confirmed_funding_txid,
4917+
})
4918+
},
4919+
}
4920+
}
4921+
48624922
fn check_funding_confirmations(&self, funding: &mut FundingScope, height: u32) -> bool {
48634923
let is_coinbase = funding
48644924
.funding_transaction
@@ -4892,6 +4952,107 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
48924952

48934953
return true;
48944954
}
4955+
4956+
fn check_for_funding_tx_confirmed<L: Deref>(
4957+
&mut self, funding: &mut FundingScope, block_hash: &BlockHash, height: u32,
4958+
index_in_block: usize, tx: &mut ConfirmedTransaction, logger: &L,
4959+
) -> Result<bool, ClosureReason>
4960+
where
4961+
L::Target: Logger
4962+
{
4963+
let funding_txo = match funding.get_funding_txo() {
4964+
Some(funding_txo) => funding_txo,
4965+
None => {
4966+
debug_assert!(false);
4967+
return Ok(false);
4968+
},
4969+
};
4970+
4971+
let mut is_funding_tx_confirmed = false;
4972+
4973+
// Check if the transaction is the expected funding transaction, and if it is,
4974+
// check that it pays the right amount to the right script.
4975+
if funding.funding_tx_confirmation_height == 0 {
4976+
if tx.txid() == funding_txo.txid {
4977+
let tx = tx.tx();
4978+
let txo_idx = funding_txo.index as usize;
4979+
if txo_idx >= tx.output.len() || tx.output[txo_idx].script_pubkey != funding.get_funding_redeemscript().to_p2wsh() ||
4980+
tx.output[txo_idx].value.to_sat() != funding.get_value_satoshis() {
4981+
if funding.is_outbound() {
4982+
// If we generated the funding transaction and it doesn't match what it
4983+
// should, the client is really broken and we should just panic and
4984+
// tell them off. That said, because hash collisions happen with high
4985+
// probability in fuzzing mode, if we're fuzzing we just close the
4986+
// channel and move on.
4987+
#[cfg(not(fuzzing))]
4988+
panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!");
4989+
}
4990+
self.update_time_counter += 1;
4991+
let err_reason = "funding tx had wrong script/value or output index";
4992+
return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() });
4993+
} else {
4994+
if funding.is_outbound() {
4995+
if !tx.is_coinbase() {
4996+
for input in tx.input.iter() {
4997+
if input.witness.is_empty() {
4998+
// We generated a malleable funding transaction, implying we've
4999+
// just exposed ourselves to funds loss to our counterparty.
5000+
#[cfg(not(fuzzing))]
5001+
panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!");
5002+
}
5003+
}
5004+
}
5005+
}
5006+
5007+
// The acceptor of v1-established channels doesn't have the funding
5008+
// transaction until it is seen on chain. Set it so that minimum_depth
5009+
// checks can tell if the coinbase transaction was used.
5010+
if funding.funding_transaction.is_none() {
5011+
funding.funding_transaction = Some(tx.clone());
5012+
}
5013+
5014+
funding.funding_tx_confirmation_height = height;
5015+
funding.funding_tx_confirmed_in = Some(*block_hash);
5016+
funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) {
5017+
Ok(scid) => Some(scid),
5018+
Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"),
5019+
};
5020+
}
5021+
5022+
is_funding_tx_confirmed = true;
5023+
}
5024+
}
5025+
5026+
Ok(is_funding_tx_confirmed)
5027+
}
5028+
5029+
fn check_for_funding_tx_spent<L: Deref>(
5030+
&mut self, funding: &FundingScope, tx: &Transaction, logger: &L,
5031+
) -> Result<(), ClosureReason>
5032+
where
5033+
L::Target: Logger
5034+
{
5035+
let funding_txo = match funding.get_funding_txo() {
5036+
Some(funding_txo) => funding_txo,
5037+
None => {
5038+
debug_assert!(false);
5039+
return Ok(());
5040+
},
5041+
};
5042+
5043+
for input in tx.input.iter() {
5044+
if input.previous_output == funding_txo.into_bitcoin_outpoint() {
5045+
log_info!(
5046+
logger, "Detected channel-closing tx {} spending {}:{}, closing channel {}",
5047+
tx.compute_txid(), input.previous_output.txid, input.previous_output.vout,
5048+
&self.channel_id(),
5049+
);
5050+
return Err(ClosureReason::CommitmentTxConfirmed);
5051+
}
5052+
}
5053+
5054+
Ok(())
5055+
}
48955056
}
48965057

48975058
// Internal utility functions for channels
@@ -5072,6 +5233,16 @@ pub(super) struct FundedChannel<SP: Deref> where SP::Target: SignerProvider {
50725233
pending_splice: Option<PendingSplice>,
50735234
}
50745235

5236+
#[cfg(splicing)]
5237+
macro_rules! promote_splice_funding {
5238+
($self: expr, $funding: expr) => {
5239+
core::mem::swap(&mut $self.funding, $funding);
5240+
$self.pending_splice = None;
5241+
$self.pending_funding.clear();
5242+
$self.context.announcement_sigs_state = AnnouncementSigsState::NotSent;
5243+
}
5244+
}
5245+
50755246
#[cfg(any(test, fuzzing))]
50765247
struct CommitmentTxInfoCached {
50775248
fee: u64,
@@ -8202,75 +8373,80 @@ impl<SP: Deref> FundedChannel<SP> where
82028373
NS::Target: NodeSigner,
82038374
L::Target: Logger
82048375
{
8205-
let mut msgs = (None, None);
8206-
if let Some(funding_txo) = self.funding.get_funding_txo() {
8207-
for &(index_in_block, tx) in txdata.iter() {
8208-
// Check if the transaction is the expected funding transaction, and if it is,
8209-
// check that it pays the right amount to the right script.
8210-
if self.funding.funding_tx_confirmation_height == 0 {
8211-
if tx.compute_txid() == funding_txo.txid {
8212-
let txo_idx = funding_txo.index as usize;
8213-
if txo_idx >= tx.output.len() || tx.output[txo_idx].script_pubkey != self.funding.get_funding_redeemscript().to_p2wsh() ||
8214-
tx.output[txo_idx].value.to_sat() != self.funding.get_value_satoshis() {
8215-
if self.funding.is_outbound() {
8216-
// If we generated the funding transaction and it doesn't match what it
8217-
// should, the client is really broken and we should just panic and
8218-
// tell them off. That said, because hash collisions happen with high
8219-
// probability in fuzzing mode, if we're fuzzing we just close the
8220-
// channel and move on.
8221-
#[cfg(not(fuzzing))]
8222-
panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!");
8223-
}
8224-
self.context.update_time_counter += 1;
8225-
let err_reason = "funding tx had wrong script/value or output index";
8226-
return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() });
8227-
} else {
8228-
if self.funding.is_outbound() {
8229-
if !tx.is_coinbase() {
8230-
for input in tx.input.iter() {
8231-
if input.witness.is_empty() {
8232-
// We generated a malleable funding transaction, implying we've
8233-
// just exposed ourselves to funds loss to our counterparty.
8234-
#[cfg(not(fuzzing))]
8235-
panic!("Client called ChannelManager::funding_transaction_generated with bogus transaction!");
8236-
}
8237-
}
8238-
}
8239-
}
8376+
for &(index_in_block, tx) in txdata.iter() {
8377+
let mut confirmed_tx = ConfirmedTransaction::from(tx);
8378+
8379+
// If we allow 1-conf funding, we may need to check for channel_ready or splice_locked here
8380+
// and send it immediately instead of waiting for a best_block_updated call (which may have
8381+
// already happened for this block).
8382+
let is_funding_tx_confirmed = self.context.check_for_funding_tx_confirmed(
8383+
&mut self.funding, block_hash, height, index_in_block, &mut confirmed_tx, logger,
8384+
)?;
82408385

8241-
// The acceptor of v1-established channels doesn't have the funding
8242-
// transaction until it is seen on chain. Set it so that minimum_depth
8243-
// checks can tell if the coinbase transaction was used.
8244-
if self.funding.funding_transaction.is_none() {
8245-
self.funding.funding_transaction = Some(tx.clone());
8246-
}
8386+
if is_funding_tx_confirmed {
8387+
for &(_, tx) in txdata.iter() {
8388+
self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?;
8389+
}
82478390

8248-
self.funding.funding_tx_confirmation_height = height;
8249-
self.funding.funding_tx_confirmed_in = Some(*block_hash);
8250-
self.funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) {
8251-
Ok(scid) => Some(scid),
8252-
Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"),
8253-
}
8254-
}
8255-
}
8256-
// If we allow 1-conf funding, we may need to check for channel_ready here and
8257-
// send it immediately instead of waiting for a best_block_updated call (which
8258-
// may have already happened for this block).
8259-
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
8260-
log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id);
8261-
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8262-
msgs = (Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs);
8391+
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
8392+
log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id);
8393+
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8394+
return Ok((Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs));
8395+
}
8396+
}
8397+
8398+
#[cfg(splicing)]
8399+
let mut confirmed_funding = None;
8400+
#[cfg(splicing)]
8401+
for funding in self.pending_funding.iter_mut() {
8402+
if self.context.check_for_funding_tx_confirmed(
8403+
funding, block_hash, height, index_in_block, &mut confirmed_tx, logger,
8404+
)? {
8405+
if confirmed_funding.is_some() {
8406+
let err_reason = "splice tx of another pending funding already confirmed";
8407+
return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() });
82638408
}
8409+
8410+
confirmed_funding = Some(funding);
8411+
}
8412+
}
8413+
8414+
#[cfg(splicing)]
8415+
if let Some(funding) = confirmed_funding {
8416+
let pending_splice = match self.pending_splice.as_mut() {
8417+
Some(pending_splice) => pending_splice,
8418+
None => {
8419+
// TODO: Move pending_funding into pending_splice?
8420+
debug_assert!(false);
8421+
// TODO: Error instead?
8422+
return Ok((None, None));
8423+
},
8424+
};
8425+
8426+
for &(_, tx) in txdata.iter() {
8427+
self.context.check_for_funding_tx_spent(funding, tx, logger)?;
82648428
}
8265-
for inp in tx.input.iter() {
8266-
if inp.previous_output == funding_txo.into_bitcoin_outpoint() {
8267-
log_info!(logger, "Detected channel-closing tx {} spending {}:{}, closing channel {}", tx.compute_txid(), inp.previous_output.txid, inp.previous_output.vout, &self.context.channel_id());
8268-
return Err(ClosureReason::CommitmentTxConfirmed);
8429+
8430+
if let Some(splice_locked) = self.context.check_get_splice_locked(pending_splice, funding, height, logger) {
8431+
log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id);
8432+
8433+
pending_splice.sent_funding_txid = Some(splice_locked.splice_txid);
8434+
if pending_splice.sent_funding_txid == pending_splice.received_funding_txid {
8435+
promote_splice_funding!(self, funding);
8436+
8437+
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8438+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), announcement_sigs));
82698439
}
8440+
8441+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), None));
82708442
}
82718443
}
8444+
8445+
self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?;
8446+
82728447
}
8273-
Ok(msgs)
8448+
8449+
Ok((None, None))
82748450
}
82758451

82768452
/// When a new block is connected, we check the height of the block against outbound holding
@@ -8363,6 +8539,49 @@ impl<SP: Deref> FundedChannel<SP> where
83638539
return Err(ClosureReason::FundingTimedOut);
83648540
}
83658541

8542+
#[cfg(splicing)]
8543+
let mut confirmed_funding = None;
8544+
#[cfg(splicing)]
8545+
for funding in self.pending_funding.iter_mut() {
8546+
if confirmed_funding.is_some() {
8547+
let err_reason = "splice tx of another pending funding already confirmed";
8548+
return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() });
8549+
}
8550+
8551+
confirmed_funding = Some(funding);
8552+
}
8553+
8554+
#[cfg(splicing)]
8555+
if let Some(funding) = confirmed_funding {
8556+
let pending_splice = match self.pending_splice.as_mut() {
8557+
Some(pending_splice) => pending_splice,
8558+
None => {
8559+
// TODO: Move pending_funding into pending_splice?
8560+
debug_assert!(false);
8561+
// TODO: Error instead?
8562+
return Ok((None, timed_out_htlcs, None));
8563+
},
8564+
};
8565+
8566+
if let Some(splice_locked) = self.context.check_get_splice_locked(pending_splice, funding, height, logger) {
8567+
log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id);
8568+
8569+
pending_splice.sent_funding_txid = Some(splice_locked.splice_txid);
8570+
if pending_splice.sent_funding_txid == pending_splice.received_funding_txid {
8571+
promote_splice_funding!(self, funding);
8572+
8573+
let mut announcement_sigs = None;
8574+
if let Some((chain_hash, node_signer, user_config)) = chain_node_signer {
8575+
announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8576+
}
8577+
8578+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), timed_out_htlcs, announcement_sigs));
8579+
}
8580+
8581+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), timed_out_htlcs, None));
8582+
}
8583+
}
8584+
83668585
let announcement_sigs = if let Some((chain_hash, node_signer, user_config)) = chain_node_signer {
83678586
self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger)
83688587
} else { None };
@@ -8694,6 +8913,8 @@ impl<SP: Deref> FundedChannel<SP> where
86948913

86958914
self.pending_splice = Some(PendingSplice {
86968915
our_funding_contribution: our_funding_contribution_satoshis,
8916+
sent_funding_txid: None,
8917+
received_funding_txid: None,
86978918
});
86988919

86998920
let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime);

0 commit comments

Comments
 (0)