Skip to content

Commit 5fc20b7

Browse files
committed
WIP: Check all funding transactions
1 parent 95a82c7 commit 5fc20b7

File tree

2 files changed

+304
-62
lines changed

2 files changed

+304
-62
lines changed

lightning/src/ln/channel.rs

Lines changed: 282 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,32 @@ impl FundingScope {
18521852
#[cfg(splicing)]
18531853
struct PendingSplice {
18541854
pub our_funding_contribution: i64,
1855+
sent_funding_txid: Option<Txid>,
1856+
received_funding_txid: Option<Txid>,
1857+
}
1858+
1859+
/// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`].
1860+
struct ConfirmedTransaction<'a> {
1861+
tx: &'a Transaction,
1862+
txid: Option<Txid>,
1863+
}
1864+
1865+
impl<'a> ConfirmedTransaction<'a> {
1866+
/// Returns the underlying [`Transaction`].
1867+
pub fn tx(&self) -> &'a Transaction {
1868+
self.tx
1869+
}
1870+
1871+
/// Returns the [`Txid`], computing and caching it if necessary.
1872+
pub fn txid(&mut self) -> Txid {
1873+
*self.txid.get_or_insert_with(|| self.tx.compute_txid())
1874+
}
1875+
}
1876+
1877+
impl<'a> From<&'a Transaction> for ConfirmedTransaction<'a> {
1878+
fn from(tx: &'a Transaction) -> Self {
1879+
ConfirmedTransaction { tx, txid: None }
1880+
}
18551881
}
18561882

18571883
/// Contains everything about the channel including state, and various flags.
@@ -4854,6 +4880,40 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
48544880
self.get_initial_counterparty_commitment_signature(funding, logger)
48554881
}
48564882

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

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

48925053
// Internal utility functions for channels
@@ -5067,6 +5228,16 @@ pub(super) struct FundedChannel<SP: Deref> where SP::Target: SignerProvider {
50675228
pending_splice: Option<PendingSplice>,
50685229
}
50695230

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

8236-
// The acceptor of v1-established channels doesn't have the funding
8237-
// transaction until it is seen on chain. Set it so that minimum_depth
8238-
// checks can tell if the coinbase transaction was used.
8239-
if self.funding.funding_transaction.is_none() {
8240-
self.funding.funding_transaction = Some(tx.clone());
8241-
}
8381+
if is_funding_tx_confirmed {
8382+
for &(_, tx) in txdata.iter() {
8383+
self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?;
8384+
}
82428385

8243-
self.funding.funding_tx_confirmation_height = height;
8244-
self.funding.funding_tx_confirmed_in = Some(*block_hash);
8245-
self.funding.short_channel_id = match scid_from_parts(height as u64, index_in_block as u64, txo_idx as u64) {
8246-
Ok(scid) => Some(scid),
8247-
Err(_) => panic!("Block was bogus - either height was > 16 million, had > 16 million transactions, or had > 65k outputs"),
8248-
}
8249-
}
8250-
}
8251-
// If we allow 1-conf funding, we may need to check for channel_ready here and
8252-
// send it immediately instead of waiting for a best_block_updated call (which
8253-
// may have already happened for this block).
8254-
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
8255-
log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id);
8256-
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8257-
msgs = (Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs);
8386+
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
8387+
log_info!(logger, "Sending a channel_ready to our peer for channel {}", &self.context.channel_id);
8388+
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8389+
return Ok((Some(FundingConfirmedMessage::Establishment(channel_ready)), announcement_sigs));
8390+
}
8391+
}
8392+
8393+
#[cfg(splicing)]
8394+
let mut confirmed_funding = None;
8395+
#[cfg(splicing)]
8396+
for funding in self.pending_funding.iter_mut() {
8397+
if self.context.check_for_funding_tx_confirmed(
8398+
funding, block_hash, height, index_in_block, &mut confirmed_tx, logger,
8399+
)? {
8400+
if confirmed_funding.is_some() {
8401+
let err_reason = "splice tx of another pending funding already confirmed";
8402+
return Err(ClosureReason::ProcessingError { err: err_reason.to_owned() });
82588403
}
8404+
8405+
confirmed_funding = Some(funding);
8406+
}
8407+
}
8408+
8409+
#[cfg(splicing)]
8410+
if let Some(funding) = confirmed_funding {
8411+
let pending_splice = match self.pending_splice.as_mut() {
8412+
Some(pending_splice) => pending_splice,
8413+
None => {
8414+
// TODO: Move pending_funding into pending_splice?
8415+
debug_assert!(false);
8416+
// TODO: Error instead?
8417+
return Ok((None, None));
8418+
},
8419+
};
8420+
8421+
for &(_, tx) in txdata.iter() {
8422+
self.context.check_for_funding_tx_spent(funding, tx, logger)?;
82598423
}
8260-
for inp in tx.input.iter() {
8261-
if inp.previous_output == funding_txo.into_bitcoin_outpoint() {
8262-
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());
8263-
return Err(ClosureReason::CommitmentTxConfirmed);
8424+
8425+
if let Some(splice_locked) = self.context.check_get_splice_locked(pending_splice, funding, height, logger) {
8426+
log_info!(logger, "Sending a splice_locked to our peer for channel {}", &self.context.channel_id);
8427+
8428+
pending_splice.sent_funding_txid = Some(splice_locked.splice_txid);
8429+
if pending_splice.sent_funding_txid == pending_splice.received_funding_txid {
8430+
promote_splice_funding!(self, funding);
8431+
8432+
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, height, logger);
8433+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), announcement_sigs));
82648434
}
8435+
8436+
return Ok((Some(FundingConfirmedMessage::Splice(splice_locked)), None));
82658437
}
82668438
}
8439+
8440+
self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?;
8441+
82678442
}
8268-
Ok(msgs)
8443+
8444+
Ok((None, None))
82698445
}
82708446

82718447
/// When a new block is connected, we check the height of the block against outbound holding
@@ -8358,6 +8534,49 @@ impl<SP: Deref> FundedChannel<SP> where
83588534
return Err(ClosureReason::FundingTimedOut);
83598535
}
83608536

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

86908909
self.pending_splice = Some(PendingSplice {
86918910
our_funding_contribution: our_funding_contribution_satoshis,
8911+
sent_funding_txid: None,
8912+
received_funding_txid: None,
86928913
});
86938914

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

0 commit comments

Comments
 (0)