diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 8ac02309981..0cf513180ed 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1698,6 +1698,52 @@ pub enum Event { /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice reply_path: Responder, }, + /// Indicates that a channel funding transaction constructed interactively is ready to be + /// signed. This event will only be triggered if at least one input was contributed. + /// + /// The transaction contains all inputs and outputs provided by both parties including the + /// channel's funding output and a change output if applicable. + /// + /// No part of the transaction should be changed before signing as the content of the transaction + /// has already been negotiated with the counterparty. + /// + /// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and + /// hence possible loss of funds. + /// + /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed + /// funding transaction. + /// + /// Generated in [`ChannelManager`] message handling. + /// + /// # Failure Behavior and Persistence + /// This event will eventually be replayed after failures-to-handle (i.e., the event handler + /// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + FundingTransactionReadyForSigning { + /// The `channel_id` of the channel which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + channel_id: ChannelId, + /// The counterparty's `node_id`, which you'll need to pass back into + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + counterparty_node_id: PublicKey, + /// The `user_channel_id` value passed in for outbound channels, or for inbound channels if + /// [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. Otherwise + /// `user_channel_id` will be randomized for inbound channels. + /// + /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels + user_channel_id: u128, + /// The unsigned transaction to be signed and passed back to + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed + unsigned_transaction: Transaction, + }, } impl Writeable for Event { @@ -2140,6 +2186,11 @@ impl Writeable for Event { 47u8.write(writer)?; // Never write StaticInvoiceRequested events as buffered onion messages aren't serialized. }, + &Event::FundingTransactionReadyForSigning { .. } => { + 49u8.write(writer)?; + // We never write out FundingTransactionReadyForSigning events as they will be regenerated when + // necessary. + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2722,6 +2773,8 @@ impl MaybeReadable for Event { // Note that we do not write a length-prefixed TLV for StaticInvoiceRequested events. #[cfg(async_payments)] 47u8 => Ok(None), + // Note that we do not write a length-prefixed TLV for FundingTransactionReadyForSigning events. + 49u8 => Ok(None), // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index bd4cdcd4a18..f0effedee10 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -14,7 +14,7 @@ use bitcoin::constants::ChainHash; use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash}; use bitcoin::sighash::EcdsaSighashType; use bitcoin::transaction::{Transaction, TxIn, TxOut}; -use bitcoin::Weight; +use bitcoin::{Weight, Witness}; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::sha256::Hash as Sha256; @@ -24,9 +24,9 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, sighash}; #[cfg(splicing)] -use bitcoin::{Sequence, Witness}; +use bitcoin::Sequence; +use bitcoin::{secp256k1, sighash}; use crate::chain::chaininterface::{ fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, @@ -38,7 +38,7 @@ use crate::chain::channelmonitor::{ use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::BestBlock; use crate::events::bump_transaction::BASE_INPUT_WEIGHT; -use crate::events::{ClosureReason, Event}; +use crate::events::ClosureReason; use crate::ln::chan_utils; #[cfg(splicing)] use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; @@ -1774,7 +1774,7 @@ where pub fn funding_tx_constructed( &mut self, logger: &L, - ) -> Result<(msgs::CommitmentSigned, Option), msgs::TxAbort> + ) -> Result where L::Target: Logger, { @@ -1786,7 +1786,7 @@ where .take() .expect("PendingV2Channel::interactive_tx_constructor should be set") .into_signing_session(); - let (commitment_signed, event) = chan.context.funding_tx_constructed( + let commitment_signed = chan.context.funding_tx_constructed( &mut chan.funding, &mut signing_session, false, @@ -1796,7 +1796,7 @@ where chan.interactive_tx_signing_session = Some(signing_session); - return Ok((commitment_signed, event)); + return Ok(commitment_signed); }, #[cfg(splicing)] ChannelPhase::Funded(chan) => { @@ -1809,7 +1809,7 @@ where { let mut signing_session = interactive_tx_constructor.into_signing_session(); - let (commitment_signed, event) = chan.context.funding_tx_constructed( + let commitment_signed = chan.context.funding_tx_constructed( &mut funding, &mut signing_session, true, @@ -1821,7 +1821,7 @@ where pending_splice.funding_negotiation = Some(FundingNegotiation::AwaitingSignatures(funding)); - return Ok((commitment_signed, event)); + return Ok(commitment_signed); } else { // Replace the taken state pending_splice.funding_negotiation = Some(funding_negotiation); @@ -2539,7 +2539,6 @@ where monitor_pending_failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, monitor_pending_finalized_fulfills: Vec<(HTLCSource, Option)>, monitor_pending_update_adds: Vec, - monitor_pending_tx_signatures: Option, /// If we went to send a revoke_and_ack but our signer was unable to give us a signature, /// we should retry at some point in the future when the signer indicates it may have a @@ -3259,7 +3258,6 @@ where monitor_pending_failures: Vec::new(), monitor_pending_finalized_fulfills: Vec::new(), monitor_pending_update_adds: Vec::new(), - monitor_pending_tx_signatures: None, signer_pending_revoke_and_ack: false, signer_pending_commitment_update: false, @@ -3498,7 +3496,6 @@ where monitor_pending_failures: Vec::new(), monitor_pending_finalized_fulfills: Vec::new(), monitor_pending_update_adds: Vec::new(), - monitor_pending_tx_signatures: None, signer_pending_revoke_and_ack: false, signer_pending_commitment_update: false, @@ -5523,7 +5520,7 @@ where fn funding_tx_constructed( &mut self, funding: &mut FundingScope, signing_session: &mut InteractiveTxSigningSession, is_splice: bool, holder_commitment_transaction_number: u64, logger: &L - ) -> Result<(msgs::CommitmentSigned, Option), msgs::TxAbort> + ) -> Result where L::Target: Logger { @@ -5581,44 +5578,7 @@ where }, }; - let funding_ready_for_sig_event = if signing_session.local_inputs_count() == 0 { - if signing_session.provide_holder_witnesses(self.channel_id, Vec::new()).is_err() { - debug_assert!( - false, - "Zero inputs were provided & zero witnesses were provided, but a count mismatch was somehow found", - ); - return Err(msgs::TxAbort { - channel_id: self.channel_id(), - data: "V2 channel rejected due to sender error".to_owned().into_bytes(), - }); - } - None - } else { - // TODO(dual_funding): Send event for signing if we've contributed funds. - // Inform the user that SIGHASH_ALL must be used for all signatures when contributing - // inputs/signatures. - // Also warn the user that we don't do anything to prevent the counterparty from - // providing non-standard witnesses which will prevent the funding transaction from - // confirming. This warning must appear in doc comments wherever the user is contributing - // funds, whether they are initiator or acceptor. - // - // The following warning can be used when the APIs allowing contributing inputs become available: - //
- // WARNING: LDK makes no attempt to prevent the counterparty from using non-standard inputs which - // will prevent the funding transaction from being relayed on the bitcoin network and hence being - // confirmed. - //
- debug_assert!( - false, - "We don't support users providing inputs but somehow we had more than zero inputs", - ); - return Err(msgs::TxAbort { - channel_id: self.channel_id(), - data: "V2 channel rejected due to sender error".to_owned().into_bytes(), - }); - }; - - Ok((commitment_signed, funding_ready_for_sig_event)) + Ok(commitment_signed) } /// Asserts that the commitment tx numbers have not advanced from their initial number. @@ -7017,15 +6977,7 @@ where log_info!(logger, "Received initial commitment_signed from peer for channel {}", &self.context.channel_id()); self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new()); - - if let Some(tx_signatures) = self.interactive_tx_signing_session.as_mut().and_then( - |session| session.received_commitment_signed() - ) { - // We're up first for submitting our tx_signatures, but our monitor has not persisted yet - // so they'll be sent as soon as that's done. - self.context.monitor_pending_tx_signatures = Some(tx_signatures); - } - + self.interactive_tx_signing_session.as_mut().expect("signing session should be present").received_commitment_signed(); Ok(channel_monitor) } @@ -7111,13 +7063,11 @@ where channel_id: Some(self.context.channel_id()), }; - let tx_signatures = self - .interactive_tx_signing_session + self.interactive_tx_signing_session .as_mut() .expect("Signing session must exist for negotiated pending splice") .received_commitment_signed(); self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new()); - self.context.monitor_pending_tx_signatures = tx_signatures; Ok(self.push_ret_blockable_mon_update(monitor_update)) } @@ -8027,10 +7977,43 @@ where } } + pub fn funding_transaction_signed( + &mut self, witnesses: Vec, + ) -> Result<(Option, Option), APIError> { + let (funding_tx_opt, tx_signatures_opt) = self + .interactive_tx_signing_session + .as_mut() + .ok_or_else(|| APIError::APIMisuseError { + err: format!( + "Channel {} not expecting funding signatures", + self.context.channel_id + ), + }) + .and_then(|signing_session| { + signing_session + .provide_holder_witnesses( + &self.context.secp_ctx, + self.context.channel_id, + witnesses, + ) + .map_err(|err| APIError::APIMisuseError { err }) + })?; + + if tx_signatures_opt.is_some() { + self.context.channel_state.set_our_tx_signatures_ready(); + } + + if funding_tx_opt.is_some() { + self.funding.funding_transaction = funding_tx_opt.clone(); + self.context.channel_state = + ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); + } + + Ok((tx_signatures_opt, funding_tx_opt)) + } + #[rustfmt::skip] - pub fn tx_signatures(&mut self, msg: &msgs::TxSignatures, logger: &L) -> Result<(Option, Option), ChannelError> - where L::Target: Logger - { + pub fn tx_signatures(&mut self, msg: &msgs::TxSignatures) -> Result<(Option, Option), ChannelError> { if !self.context.channel_state.is_interactive_signing() || self.context.channel_state.is_their_tx_signatures_sent() { @@ -8053,50 +8036,29 @@ where return Err(ChannelError::Close((msg.to_owned(), reason))); } - if msg.witnesses.len() != signing_session.remote_inputs_count() { - return Err(ChannelError::Warn( - "Witness count did not match contributed input count".to_string() - )); - } - for witness in &msg.witnesses { if witness.is_empty() { let msg = "Unexpected empty witness in tx_signatures received"; let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; return Err(ChannelError::Close((msg.to_owned(), reason))); } - - // TODO(dual_funding): Check all sigs are SIGHASH_ALL. - - // TODO(dual_funding): I don't see how we're going to be able to ensure witness-standardness - // for spending. Doesn't seem to be anything in rust-bitcoin. } let (holder_tx_signatures_opt, funding_tx_opt) = signing_session.received_tx_signatures(msg.clone()) - .map_err(|_| ChannelError::Warn("Witness count did not match contributed input count".to_string()))?; + .map_err(|msg| ChannelError::Warn(msg))?; // Set `THEIR_TX_SIGNATURES_SENT` flag after all potential errors. self.context.channel_state.set_their_tx_signatures_sent(); if funding_tx_opt.is_some() { + // TODO(splicing): Transition back to `ChannelReady` and not `AwaitingChannelReady` + // We will also need to use the pending `FundingScope` in the splicing case. + // // We have a finalized funding transaction, so we can set the funding transaction. self.funding.funding_transaction = funding_tx_opt.clone(); + self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); } - // Note that `holder_tx_signatures_opt` will be `None` if we sent `tx_signatures` first, so this - // case checks if there is a monitor persist in progress when we need to respond with our `tx_signatures` - // and sets it as pending. - if holder_tx_signatures_opt.is_some() && self.is_awaiting_initial_mon_persist() { - log_debug!(logger, "Not sending tx_signatures: a monitor update is in progress. Setting monitor_pending_tx_signatures."); - self.context.monitor_pending_tx_signatures = holder_tx_signatures_opt; - return Ok((None, None)); - } - - if holder_tx_signatures_opt.is_some() { - self.context.channel_state.set_our_tx_signatures_ready(); - } - - self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); Ok((funding_tx_opt, holder_tx_signatures_opt)) } else { let msg = "Unexpected tx_signatures. No funding transaction awaiting signatures"; @@ -8348,17 +8310,6 @@ where mem::swap(&mut finalized_claimed_htlcs, &mut self.context.monitor_pending_finalized_fulfills); let mut pending_update_adds = Vec::new(); mem::swap(&mut pending_update_adds, &mut self.context.monitor_pending_update_adds); - // For channels established with V2 establishment we won't send a `tx_signatures` when we're in - // MonitorUpdateInProgress (and we assume the user will never directly broadcast the funding - // transaction and waits for us to do it). - let tx_signatures = self.context.monitor_pending_tx_signatures.take(); - if tx_signatures.is_some() { - if self.context.channel_state.is_their_tx_signatures_sent() { - self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); - } else { - self.context.channel_state.set_our_tx_signatures_ready(); - } - } if self.context.channel_state.is_peer_disconnected() { self.context.monitor_pending_revoke_and_ack = false; @@ -8366,7 +8317,7 @@ where return MonitorRestoreUpdates { raa: None, commitment_update: None, order: RAACommitmentOrder::RevokeAndACKFirst, accepted_htlcs, failed_htlcs, finalized_claimed_htlcs, pending_update_adds, - funding_broadcastable, channel_ready, announcement_sigs, tx_signatures + funding_broadcastable, channel_ready, announcement_sigs, tx_signatures: None }; } @@ -8396,7 +8347,7 @@ where match order { RAACommitmentOrder::CommitmentFirst => "commitment", RAACommitmentOrder::RevokeAndACKFirst => "RAA"}); MonitorRestoreUpdates { raa, commitment_update, order, accepted_htlcs, failed_htlcs, finalized_claimed_htlcs, - pending_update_adds, funding_broadcastable, channel_ready, announcement_sigs, tx_signatures + pending_update_adds, funding_broadcastable, channel_ready, announcement_sigs, tx_signatures: None } } @@ -8882,7 +8833,6 @@ where update_fee: None, }) } else { None }; - // TODO(dual_funding): For async signing support we need to hold back `tx_signatures` until the `commitment_signed` is ready. let tx_signatures = if ( // if it has not received tx_signatures for that funding transaction AND // if it has already received commitment_signed AND it should sign first, as specified in the tx_signatures requirements: @@ -8891,19 +8841,13 @@ where // else if it has already received tx_signatures for that funding transaction: // MUST send its tx_signatures for that funding transaction. ) || self.context.channel_state.is_their_tx_signatures_sent() { - if self.context.channel_state.is_monitor_update_in_progress() { - // The `monitor_pending_tx_signatures` field should have already been set in `commitment_signed_initial_v2` - // if we were up first for signing and had a monitor update in progress, but check again just in case. - debug_assert!(self.context.monitor_pending_tx_signatures.is_some(), "monitor_pending_tx_signatures should already be set"); - log_debug!(logger, "Not sending tx_signatures: a monitor update is in progress. Setting monitor_pending_tx_signatures."); - if self.context.monitor_pending_tx_signatures.is_none() { - self.context.monitor_pending_tx_signatures = session.holder_tx_signatures().clone(); - } + // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent + // when the holder provides their witnesses as this will queue a `tx_signatures` if the + // holder must send one. + if session.holder_tx_signatures().is_none() { + log_debug!(logger, "Waiting for funding transaction signatures to be provided"); None } else { - // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent - // when the holder provides their witnesses as this will queue a `tx_signatures` if the - // holder must send one. session.holder_tx_signatures().clone() } } else { @@ -14005,7 +13949,6 @@ where monitor_pending_failures, monitor_pending_finalized_fulfills: monitor_pending_finalized_fulfills.unwrap(), monitor_pending_update_adds: monitor_pending_update_adds.unwrap_or_default(), - monitor_pending_tx_signatures: None, signer_pending_revoke_and_ack: false, signer_pending_commitment_update: false, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 455f3e9e6d9..c0917739891 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5925,6 +5925,126 @@ where 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. + /// + /// Do NOT broadcast the funding transaction yourself. When we have safely received our + /// counterparty's signature(s) the funding transaction will automatically be broadcast via the + /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. + /// + /// `SIGHASH_ALL` MUST be used for all signatures when providing signatures, otherwise your + /// funds can be held hostage! + /// + /// LDK checks the following: + /// * Each input spends an output that is one of P2WPKH, P2WSH, or P2TR. + /// These were already checked by LDK when the inputs to be contributed were provided. + /// * All signatures use the `SIGHASH_ALL` sighash type. + /// * P2WPKH and P2TR key path spends are valid (verifies signatures) + /// + /// NOTE: + /// * When checking P2WSH spends, LDK tries to decode 70-72 byte witness elements as ECDSA + /// signatures with a sighash flag. If the internal DER-decoding fails, then LDK just + /// assumes it wasn't a signature and carries with checks. If the element can be decoded + /// as an ECDSA signature, the the sighash flag must be `SIGHASH_ALL`. + /// * When checking P2TR script-path spends, LDK assumes all elements of exactly 65 bytes + /// with the last byte matching any valid sighash flag byte are schnorr signatures and checks + /// that the sighash type is `SIGHASH_ALL`. If the last byte is not any valid sighash flag, the + /// element is assumed not to be a signature and is ignored. Elements of 64 bytes are not + /// checked because if they were schnorr signatures then they would implicitly be `SIGHASH_DEFAULT` + /// which is an alias of `SIGHASH_ALL`. + /// + /// 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 + /// signatures or if any of the checks described above fail. + /// + /// [`FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning + /// [`ChannelUnavailable`]: APIError::ChannelUnavailable + /// [`APIMisuseError`]: APIError::APIMisuseError + pub fn funding_transaction_signed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction, + ) -> 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 witnesses: Vec<_> = transaction + .input + .into_iter() + .map(|input| input.witness) + .filter(|witness| witness.is_empty()) + .collect(); + match chan.funding_transaction_signed(witnesses) { + Ok((Some(tx_signatures), funding_tx_opt)) => { + if let Some(funding_tx) = funding_tx_opt { + self.broadcast_interactive_funding(chan, &funding_tx); + } + peer_state.pending_msg_events.push( + MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }, + ); + return NotifyOption::DoPersist; + }, + Err(err) => { + result = Err(err); + return NotifyOption::SkipPersistNoEvents; + }, + _ => { + return NotifyOption::SkipPersistNoEvents; + }, + } + }, + None => { + result = Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding signatures", + 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 + } + + fn broadcast_interactive_funding( + &self, channel: &mut FundedChannel, funding_tx: &Transaction, + ) { + self.tx_broadcaster.broadcast_transactions(&[funding_tx]); + { + let mut pending_events = self.pending_events.lock().unwrap(); + emit_channel_pending_event!(pending_events, channel); + } + } + /// Atomically applies partial updates to the [`ChannelConfig`] of the given channels. /// /// Once the updates are applied, each eligible channel (advertised with a known short channel @@ -8839,6 +8959,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ msg, }); } + // TODO(dual_funding): For async signing support we need to hold back `tx_signatures` until the `commitment_signed` is ready. if let Some(msg) = tx_signatures { pending_msg_events.push(MessageSendEvent::SendTxSignatures { node_id: counterparty_node_id, @@ -8899,6 +9020,46 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + if let Some(signing_session) = &mut channel.interactive_tx_signing_session { + if signing_session.local_inputs_count() > 0 + && signing_session.holder_tx_signatures().is_none() + { + let mut pending_events = self.pending_events.lock().unwrap(); + let unsigned_transaction = signing_session.unsigned_tx().build_unsigned_tx(); + let event_action = ( + Event::FundingTransactionReadyForSigning { + unsigned_transaction, + counterparty_node_id, + channel_id: channel.context.channel_id(), + user_channel_id: channel.context.get_user_id(), + }, + None, + ); + + if pending_events.contains(&event_action) { + debug_assert!(false, "FundingTransactionReadyForSigning should not have been queued already"); + } else { + pending_events.push_back(event_action); + } + } else if signing_session.local_inputs_count() == 0 && signing_session.holder_tx_signatures().is_none() { + match channel.funding_transaction_signed(vec![]) { + Ok((Some(tx_signatures), funding_tx_opt)) => { + if let Some(funding_tx) = funding_tx_opt { + self.broadcast_interactive_funding(channel, &funding_tx); + } + pending_msg_events.push(MessageSendEvent::SendTxSignatures { + node_id: counterparty_node_id, + msg: tx_signatures, + }); + }, + Ok((None, _)) => { + debug_assert!(false, "If our tx_signatures is empty, then we should send it first!"); + }, + Err(err) => debug_assert!(false, "We should not error here but we got: {:?}", err), + } + } + } + { let mut pending_events = self.pending_events.lock().unwrap(); emit_channel_pending_event!(pending_events, channel); @@ -9774,11 +9935,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ peer_state.pending_msg_events.push(msg_send_event); }; if negotiation_complete { - let (commitment_signed, funding_ready_for_sig_event_opt) = match chan_entry + let commitment_signed = match chan_entry .get_mut() .funding_tx_constructed(&self.logger) { - Ok((commitment_signed, event)) => (commitment_signed, event), + Ok(commitment_signed) => commitment_signed, Err(tx_abort) => { if chan_entry.get().is_funded() { peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort { @@ -9795,10 +9956,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, }; - if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt { - let mut pending_events = self.pending_events.lock().unwrap(); - pending_events.push_back((funding_ready_for_sig_event, None)); - } peer_state.pending_msg_events.push(MessageSendEvent::UpdateHTLCs { node_id: counterparty_node_id, channel_id: msg.channel_id, @@ -9837,8 +9994,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ hash_map::Entry::Occupied(mut chan_entry) => { match chan_entry.get_mut().as_funded_mut() { Some(chan) => { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let (funding_tx_opt, tx_signatures_opt) = try_channel_entry!(self, peer_state, chan.tx_signatures(msg, &&logger), chan_entry); + let (funding_tx_opt, tx_signatures_opt) = try_channel_entry!(self, peer_state, chan.tx_signatures(msg), chan_entry); if let Some(tx_signatures) = tx_signatures_opt { peer_state.pending_msg_events.push(MessageSendEvent::SendTxSignatures { node_id: *counterparty_node_id, diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 3fcf3f4ee01..b3b3d6df112 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -14,10 +14,15 @@ use bitcoin::absolute::LockTime as AbsoluteLockTime; use bitcoin::amount::Amount; use bitcoin::consensus::Encodable; use bitcoin::constants::WITNESS_SCALE_FACTOR; +use bitcoin::key::Secp256k1; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{Message, PublicKey}; +use bitcoin::sighash::SighashCache; use bitcoin::transaction::Version; -use bitcoin::{OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness}; +use bitcoin::{ + sighash, EcdsaSighashType, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, + TxOut, Txid, Weight, Witness, XOnlyPublicKey, +}; use crate::chain::chaininterface::fee_for_weight; use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT}; @@ -210,12 +215,24 @@ pub(crate) struct NegotiatedTxInput { txin: TxIn, // The weight of the input including an estimate of its witness weight. weight: Weight, + prev_output: TxOut, +} + +impl NegotiatedTxInput { + pub(super) fn is_local(&self, holder_is_initiator: bool) -> bool { + !is_serial_id_valid_for_counterparty(holder_is_initiator, self.serial_id) + } + + pub(super) fn prev_output(&self) -> &TxOut { + &self.prev_output + } } impl_writeable_tlv_based!(NegotiatedTxInput, { (1, serial_id, required), (3, txin, required), (5, weight, required), + (7, prev_output, required), }); impl_writeable_tlv_based!(ConstructedTransaction, { @@ -361,6 +378,10 @@ impl ConstructedTransaction { .zip(witnesses) .for_each(|(input, witness)| input.witness = witness); } + + pub fn holder_is_initiator(&self) -> bool { + self.holder_is_initiator + } } /// The InteractiveTxSigningSession coordinates the signing flow of interactively constructed @@ -375,6 +396,7 @@ pub(crate) struct InteractiveTxSigningSession { unsigned_tx: ConstructedTransaction, holder_sends_tx_signatures_first: bool, has_received_commitment_signed: bool, + has_received_tx_signatures: bool, holder_tx_signatures: Option, } @@ -395,13 +417,8 @@ impl InteractiveTxSigningSession { &self.holder_tx_signatures } - pub fn received_commitment_signed(&mut self) -> Option { + pub fn received_commitment_signed(&mut self) { self.has_received_commitment_signed = true; - if self.holder_sends_tx_signatures_first { - self.holder_tx_signatures.clone() - } else { - None - } } /// Handles a `tx_signatures` message received from the counterparty. @@ -414,14 +431,18 @@ impl InteractiveTxSigningSession { /// transaction will be finalized and returned as Some, otherwise None. /// /// Returns an error if the witness count does not equal the counterparty's input count in the - /// unsigned transaction. + /// unsigned transaction or if the counterparty already provided their `tx_signatures`. pub fn received_tx_signatures( &mut self, tx_signatures: TxSignatures, - ) -> Result<(Option, Option), ()> { + ) -> Result<(Option, Option), String> { + if self.has_received_tx_signatures { + return Err("Already received a tx_signatures message".to_string()); + } if self.remote_inputs_count() != tx_signatures.witnesses.len() { - return Err(()); + return Err("Witness count did not match contributed input count".to_string()); } self.unsigned_tx.add_remote_witnesses(tx_signatures.witnesses.clone()); + self.has_received_tx_signatures = true; let holder_tx_signatures = if !self.holder_sends_tx_signatures_first { self.holder_tx_signatures.clone() @@ -446,21 +467,37 @@ impl InteractiveTxSigningSession { /// Returns an error if the witness count does not equal the holder's input count in the /// unsigned transaction. pub fn provide_holder_witnesses( - &mut self, channel_id: ChannelId, witnesses: Vec, - ) -> Result<(), ()> { - if self.local_inputs_count() != witnesses.len() { - return Err(()); - } + &mut self, secp_ctx: &Secp256k1, channel_id: ChannelId, + witnesses: Vec, + ) -> Result<(Option, Option), String> { + let local_inputs_count = self.local_inputs_count(); + if local_inputs_count != witnesses.len() { + return Err(format!( + "Provided witness count of {} does not match required count for {} inputs", + witnesses.len(), + local_inputs_count + )); + } + if self.holder_tx_signatures.is_some() { + return Err("Holder witnesses were already provided".to_string()); + } + self.verify_interactive_tx_signatures(secp_ctx, &witnesses)?; self.unsigned_tx.add_local_witnesses(witnesses.clone()); self.holder_tx_signatures = Some(TxSignatures { channel_id, + witnesses, tx_hash: self.unsigned_tx.compute_txid(), - witnesses: witnesses.into_iter().collect(), shared_input_signature: None, }); - Ok(()) + let funding_tx_opt = self.has_received_tx_signatures.then(|| self.finalize_funding_tx()); + let holder_tx_signatures = + (self.holder_sends_tx_signatures_first || self.has_received_tx_signatures).then(|| { + debug_assert!(self.has_received_commitment_signed); + self.holder_tx_signatures.clone().expect("Holder tx_signatures were just provided") + }); + Ok((funding_tx_opt, holder_tx_signatures)) } pub fn remote_inputs_count(&self) -> usize { @@ -500,6 +537,161 @@ impl InteractiveTxSigningSession { output: outputs.iter().cloned().map(|output| output.into_tx_out()).collect(), } } + + pub fn verify_interactive_tx_signatures( + &self, secp_ctx: &Secp256k1, witnesses: &Vec, + ) -> Result<(), String> { + let unsigned_tx = self.unsigned_tx(); + let built_tx = unsigned_tx.build_unsigned_tx(); + let prev_outputs: Vec<&TxOut> = + unsigned_tx.inputs().map(|input| input.prev_output()).collect::>(); + let all_prevouts = sighash::Prevouts::All(&prev_outputs[..]); + + let mut cache = SighashCache::new(&built_tx); + + let script_pubkeys = unsigned_tx + .inputs() + .enumerate() + .filter(|(_, input)| input.is_local(unsigned_tx.holder_is_initiator())); + + for ((input_idx, input), witness) in script_pubkeys.zip(witnesses) { + if witness.is_empty() { + let err = format!("The witness for input at index {input_idx} is empty"); + return Err(err); + } + + let prev_output = input.prev_output(); + let script_pubkey = &prev_output.script_pubkey; + + // P2WPKH + if script_pubkey.is_p2wpkh() { + if witness.len() != 2 { + let err = format!("The witness for input at index {input_idx} does not have the correct number of elements for a P2WPKH spend. Expected 2 got {}", witness.len()); + return Err(err); + } + let pubkey = PublicKey::from_slice(&witness[1]).map_err(|_| { + format!("The witness for input at index {input_idx} contains an invalid ECDSA public key") + })?; + + let sig = + bitcoin::ecdsa::Signature::from_slice(&witness[0]).map_err(|_| { + format!("The witness for input at index {input_idx} contains an invalid signature") + })?; + if !matches!(sig.sighash_type, EcdsaSighashType::All) { + let err = format!("Signature does not use SIGHASH_ALL for input at index {input_idx} for P2WPKH spend"); + return Err(err); + } + + let sighash = cache + .p2wpkh_signature_hash( + input_idx, + script_pubkey, + prev_output.value, + EcdsaSighashType::All, + ) + .map_err(|_| { + debug_assert!(false, "Funding transaction sighash should be calculable"); + "The transaction sighash could not be calculated".to_string() + })?; + let msg = Message::from_digest_slice(&sighash[..]) + .expect("Sighash is a SHA256 which is 32 bytes long"); + secp_ctx.verify_ecdsa(&msg, &sig.signature, &pubkey).map_err(|_| { + format!("Failed signature verification for input at index {input_idx} for P2WPKH spend") + })?; + + continue; + } + + // P2TR key path spend witness includes signature and optional annex + if script_pubkey.is_p2tr() && witness.len() == 1 { + let pubkey = match script_pubkey.instructions().nth(1) { + Some(Ok(bitcoin::script::Instruction::PushBytes(push_bytes))) => { + XOnlyPublicKey::from_slice(push_bytes.as_bytes()) + }, + _ => { + let err = format!("The scriptPubKey of the previous output for input at index {input_idx} for a P2TR key path spend is invalid"); + return Err(err) + }, + }.map_err(|_| { + format!("The scriptPubKey of the previous output for input at index {input_idx} for a P2TR key path spend has an invalid public key") + })?; + + let sig = bitcoin::taproot::Signature::from_slice(&witness[0]).map_err(|_| { + format!("The witness for input at index {input_idx} for a P2TR key path spend has an invalid signature") + })?; + if !matches!(sig.sighash_type, TapSighashType::Default | TapSighashType::All) { + let err = format!("Signature does not use SIGHASH_DEFAULT or SIGHASH_ALL for input at index {input_idx} for P2TR key path spend"); + return Err(err); + } + + let sighash = cache + .taproot_key_spend_signature_hash(input_idx, &all_prevouts, sig.sighash_type) + .map_err(|_| { + debug_assert!(false, "Funding transaction sighash should be calculable"); + "The transaction sighash could not be calculated".to_string() + })?; + let msg = Message::from_digest_slice(&sighash[..]) + .expect("Sighash is a SHA256 which is 32 bytes long"); + secp_ctx.verify_schnorr(&sig.signature, &msg, &pubkey).map_err(|_| { + format!("Failed signature verification for input at index {input_idx} for P2TR key path spend") + })?; + + continue; + } + + // P2WSH - No validation just sighash checks + if script_pubkey.is_p2wsh() { + for element in witness { + match element.len() { + // Possibly a DER-encoded ECDSA signature with a sighash type byte assuming low-S + 70..=73 => { + if !bitcoin::ecdsa::Signature::from_slice(element) + .map(|sig| matches!(sig.sighash_type, EcdsaSighashType::All)) + .unwrap_or(true) + { + let err = format!("An ECDSA signature in the witness for input {input_idx} does not use SIGHASH_ALL"); + return Err(err); + } + }, + _ => (), + } + } + continue; + } + + // P2TR script path - No validation, just sighash checks + if script_pubkey.is_p2tr() { + for element in witness { + match element.len() { + // Schnorr sig + sighash type byte. + // If this were just 64 bytes, it would implicitly be SIGHASH_DEFAULT (= SIGHASH_ALL) + 65 => { + if !bitcoin::taproot::Signature::from_slice(element) + .map(|sig| matches!(sig.sighash_type, TapSighashType::All)) + .unwrap_or(true) + { + let err = format!("A (likely) Schnorr signature in the witness for input {input_idx} does not use SIGHASH_DEFAULT or SIGHASH_ALL"); + return Err(err); + } + }, + _ => (), + } + } + continue; + } + + debug_assert!( + false, + "We don't allow contributing inputs that are not spending P2WPKH, P2WSH, or P2TR" + ); + let err = format!( + "Input at index {input_idx} does not spend from one of P2WPKH, P2WSH, or P2TR" + ); + return Err(err); + } + + Ok(()) + } } impl_writeable_tlv_based!(InteractiveTxSigningSession, { @@ -507,6 +699,7 @@ impl_writeable_tlv_based!(InteractiveTxSigningSession, { (3, holder_sends_tx_signatures_first, required), (5, has_received_commitment_signed, required), (7, holder_tx_signatures, required), + (9, has_received_tx_signatures, required), }); #[derive(Debug)] @@ -1090,6 +1283,7 @@ macro_rules! define_state_transitions { holder_sends_tx_signatures_first: tx.holder_sends_tx_signatures_first, unsigned_tx: tx, has_received_commitment_signed: false, + has_received_tx_signatures: false, holder_tx_signatures: None, }; Ok(NegotiationComplete(signing_session)) @@ -1302,13 +1496,6 @@ impl InputOwned { } } - pub fn into_tx_in(self) -> TxIn { - match self { - InputOwned::Single(single) => single.input, - InputOwned::Shared(shared) => shared.input, - } - } - pub fn value(&self) -> u64 { match self { InputOwned::Single(single) => single.prev_output.value.to_sat(), @@ -1349,6 +1536,13 @@ impl InputOwned { InputOwned::Shared(shared) => estimate_input_weight(&shared.prev_output), } } + + fn into_tx_in_with_prev_output(self) -> (TxIn, TxOut) { + match self { + InputOwned::Single(single) => (single.input, single.prev_output), + InputOwned::Shared(shared) => (shared.input, shared.prev_output), + } + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1498,10 +1692,6 @@ impl InteractiveTxInput { self.input.tx_in_mut() } - pub fn into_txin(self) -> TxIn { - self.input.into_tx_in() - } - pub fn value(&self) -> u64 { self.input.value() } @@ -1520,7 +1710,8 @@ impl InteractiveTxInput { fn into_negotiated_input(self) -> NegotiatedTxInput { let weight = self.input.estimate_input_weight(); - NegotiatedTxInput { serial_id: self.serial_id, txin: self.input.into_tx_in(), weight } + let (txin, prev_output) = self.input.into_tx_in_with_prev_output(); + NegotiatedTxInput { serial_id: self.serial_id, txin, weight, prev_output } } } @@ -1958,19 +2149,21 @@ mod tests { use bitcoin::absolute::LockTime as AbsoluteLockTime; use bitcoin::amount::Amount; use bitcoin::hashes::Hash; - use bitcoin::key::UntweakedPublicKey; - use bitcoin::opcodes; + use bitcoin::hex::FromHex; + use bitcoin::key::{TweakedPublicKey, UntweakedPublicKey}; use bitcoin::script::Builder; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; use bitcoin::transaction::Version; + use bitcoin::{opcodes, WScriptHash, Weight, XOnlyPublicKey}; use bitcoin::{ OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, }; use core::ops::Deref; use super::{ - get_output_weight, P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND, - P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, + get_output_weight, AddingRole, ConstructedTransaction, InteractiveTxOutput, + InteractiveTxSigningSession, NegotiatedTxInput, OutputOwned, P2TR_INPUT_WEIGHT_LOWER_BOUND, + P2WPKH_INPUT_WEIGHT_LOWER_BOUND, P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, }; const TEST_FEERATE_SATS_PER_KW: u32 = FEERATE_FLOOR_SATS_PER_KW * 10; @@ -3058,4 +3251,330 @@ mod tests { Ok(Some(4060)), ); } + + fn do_verify_tx_signatures( + transaction: Transaction, prev_outputs: Vec, + ) -> Result<(), String> { + let inputs: Vec = transaction + .input + .iter() + .cloned() + .zip(prev_outputs.into_iter()) + .enumerate() + .map(|(idx, (txin, prev_output))| { + NegotiatedTxInput { + serial_id: idx as u64, // even values will be holder (initiator in this test) + txin, + weight: Weight::from_wu(0), // N/A for test + prev_output, + } + }) + .collect(); + + let outputs: Vec = transaction + .output + .iter() + .cloned() + .map(|txout| InteractiveTxOutput { + serial_id: 0, // N/A for test + added_by: AddingRole::Local, + output: OutputOwned::Single(txout), + }) + .collect(); + + let unsigned_tx = ConstructedTransaction { + holder_is_initiator: true, + inputs, + outputs, + local_inputs_value_satoshis: 0, // N/A for test + local_outputs_value_satoshis: 0, // N/A for test + remote_inputs_value_satoshis: 0, // N/A for test + remote_outputs_value_satoshis: 0, // N/A for test + lock_time: transaction.lock_time, + holder_sends_tx_signatures_first: false, + }; + + let secp_ctx = Secp256k1::new(); + + InteractiveTxSigningSession { + unsigned_tx, + holder_sends_tx_signatures_first: false, // N/A for test + has_received_commitment_signed: false, // N/A for test + has_received_tx_signatures: false, // N/A for test + holder_tx_signatures: None, + } + .verify_interactive_tx_signatures( + &secp_ctx, + &transaction + .input + .into_iter() + .enumerate() + .filter(|(idx, _)| idx % 2 == 0) // we only want initiator inputs (corresponds to even serial_id) + .map(|(_, txin)| txin.witness) + .collect(), + ) + } + + #[test] + fn test_verify_tx_signatures_p2tr_key_path_p2wsh_no_sig() { + // Uses transaction https://mempool.space/tx/c28d01b47b8426039306e4209534fc5235da4a31406179639c54c48212be7655 + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("02000000000105d08ef8a4eac88a9568d660732d6e1bd8f216fecb46b7ebc7fc7b5a85e3ba1da50000000000ffffffff3ae09cc085873112f0602cac61e005827e7f21ce03595c6bf1e5ab41643e2e240000000000ffffffff030d20d2b28c4f27797e90ab2259392e99070307f0ee14a621025f8adc9054720000000000100000007d2e78b06110de8ac2298e71fa6fd96e24a287597f3a3fbfaa60837e40453a990000000000100000007d2e78b06110de8ac2298e71fa6fd96e24a287597f3a3fbfaa60837e40453a990100000000100000000104310d01000000002251207434164bd41e2185651f084b6a79e11ce57abe69093b7f939bb1c8786e5d233b0140e612c3728bcc6ed6c4ef67238e57f0332fa77a4c2e76db183e28b7f3cea5eab6b235b6f0cbab8035fd79b3c1990c5c3f3a56e2c7d5e4609b390ddaad8ac1c1d7024730440220036e88464b21c8bd819d97ae746622da00053ec1374a932f33aa1ab60170c9da022041cabc146ebdd12f6316a2f72f870771e8e6ff51f3cadad4027eab2e443770110121030c7196376bc1df61b6da6ee711868fd30e370dd273332bfb02a2287d11e2e9c50200282102fd481d39bdbc090313b530fddfd1aa004a9e3263da1406cf806670fdeb8ebb91ac736460b2680200282102092f44ee333630b985e490dbbc69865e499853cba15a51426d0f4e5906087e55ac736460b26802002821021dadb5ffb2cb74f5427f039e2913738e5cd8e93cc0d12db4cfa4f555005c326aac736460b26800000000").unwrap(); + let prev_outputs = + vec![ + // Added by holder + TxOut { + value: Amount::from_sat(17414236), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(XOnlyPublicKey::from_slice( + &<[u8; 32]>::from_hex( + "7434164bd41e2185651f084b6a79e11ce57abe69093b7f939bb1c8786e5d233b", + ) + .unwrap(), + ).unwrap()), + ), + }, + // Added by remote (corresponding input should not be checked) + TxOut { + value: Amount::from_sat(227321), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array( + <[u8; 20]>::from_hex("92b8c3a56fac121ddcdffbc85b02fb9ef681038a").unwrap(), + )), + }, + // Added by holder + TxOut { + value: Amount::from_sat(330), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "97a4f4b73947411e18486b7182063f160f9b3a238664b91ff70a56eaffca8b9d", + ) + .unwrap(), + )), + }, + // Added by remote (corresponding input should not be checked) + TxOut { + value: Amount::from_sat(330), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "0d0f49839e6bbf78271ea31d979895758ed66312b4fbab215da8a68a951f36ee", + ) + .unwrap(), + )), + }, + // Added by holder + TxOut { + value: Amount::from_sat(330), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "f2c42991382f63a20308c35ce67133cd8564ede8f8615062d814ec69112ddd46", + ) + .unwrap(), + )), + } + ]; + + assert!(do_verify_tx_signatures(transaction, prev_outputs).is_ok()); + } + + #[test] + fn test_verify_tx_signatures_p2wpkh_anyonecanpay_should_fail() { + // Using on-chain transaction: https://mempool.space/tx/fe62d242fbdd57a3bdb0d158b80e3c77754f17653eb23e3b64203076e6966cae + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("020000000001010889a9a8424c16e069d0690b10a035f166ecb0788434703776b8ccf3209cb6c00000000000fdffffff052302000000000000160014bd42e2a4f83e5d905bccf4dcff7bb88e514749054a01000000000000220020003d7374616d703a7b2270223a227372632d3230222c226f70223a227472616e4a0100000000000022002073666572222c227469636b223a2249524f4e42222c22616d74223a3130307d0064530300000000002251202838c8f586f4dcdb5fb080a9c28497287e46cab65c8dcf9de27e659afe2564a61423000000000000160014cc054f448ca15a5aa1b21f2adb6607fec4410b6d02483045022100d84f8fb0f82c22128ba75b54e6c1be27aeee967acfe0a6e624a47acdf20cf3c102200248271599dba21f24ab8593529ca95a2f27aebd953b12c5f8aff3809c9743998121025ede2bca4b5a86da349fb8827eec4bb95afb513bb8c260867bbd55e7d0a2f48d00000000").unwrap(); + + let prev_outputs = vec![ + // Added by holder + TxOut { + value: Amount::from_sat(228980), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array( + <[u8; 20]>::from_hex("cc054f448ca15a5aa1b21f2adb6607fec4410b6d").unwrap(), + )), + }, + ]; + + match do_verify_tx_signatures(transaction, prev_outputs) { + Ok(_) => panic!("Should not be valid"), + Err(err) => { + assert_eq!( + &err, + "Signature does not use SIGHASH_ALL for input at index 0 for P2WPKH spend" + ); + }, + } + } + + #[test] + fn test_verify_tx_signatures_p2wsh_with_anyonecanpay_should_fail() { + // Using on-chain transaction: https://mempool.space/tx/c28d01b47b8426039306e4209534fc5235da4a31406179639c54c48212be7655 + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("0200000000010163c80d9fe4cfd02c6e0521a3818ecc1593573c85f0026dd0a57f16c61101d2a10000000000fdffffff02838ef72e0000000022002054313a8b88c0b1f408f8e4ba2a7c71909ebb35ec3e5cc81518c5a797afb48e9d00000000000000000a6a0853594d423a62643304473044022039c1263f05745d0a1c3c7afe40cbaf39a0445f66985c700b5bb7161ac8eece54022057a20fe506aadc254efd4686dd15037be22be965f758ab83498c86893bf8f4a68101010103d55388632103024f3166b9833e75cb2d0695b221e7a86170b9900d43aaa9d62172c51f796fe1ac675321030d8e88d0f843d2671f0762cd8010cb6e96ddf3d1558f593d607f7f261b1b031b210388b5390d3d2a24762d0680474dd26149ab1ae050e18b01ec831cbf0a5914537721023a0ddacf091d5d9430467be66f4a0ecb6ced6bb255ae89b626b9fa74966d42ea21023a0363a3f5afcf71ae05c84f09edb48c2101625a08abc3b8467854cc100187f521029e7fdb5297ff32dd34c52b99aeb09ca64015c787f5e0958c68c25eb8c5de265955ae6800000000").unwrap(); + + let prev_outputs = vec![ + // Added by holder + TxOut { + value: Amount::from_sat(787976283), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "54313a8b88c0b1f408f8e4ba2a7c71909ebb35ec3e5cc81518c5a797afb48e9d", + ) + .unwrap(), + )), + }, + ]; + + match do_verify_tx_signatures(transaction, prev_outputs) { + Ok(_) => panic!("Should not be valid"), + Err(err) => { + assert_eq!( + &err, + "An ECDSA signature in the witness for input 0 does not use SIGHASH_ALL" + ); + }, + } + } + + #[test] + fn test_verify_tx_signatures_p2tr_key_path_anyonecanpay_should_fail() { + // Using on-chain transaction: https://mempool.space/tx/f7636876156f3a8a48a6cddb150e07363c1641495f4b319faab1e8c4527e58db + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("02000000000102977aba41d493f93acc890e49c292dad6cbe423cb1356c6e6191cb93eed3f60c20200000000ffffffff1d956f8838a87c551c308f49fe80da594bfb888209ae8159bc77c6f471dd3b540000000000ffffffff022202000000000000225120fcb2498c6a6a335951f4c96fc89266c388e1ef4c416a2c6fca438a2f5cbb7ffe26d0030000000000225120cbc74f986822b48c4801ef5a1cadc44b27f7d23e699d8244c391d5defd69802a0141b7b9685f6b790e24392670fa06b9af34331bd3308a58b4d8b2cd86a4bcea19a2a780565b410062b58fbff026ab74513f0bac00711eba9f80e3d6b2a7cf3887a1810140a5ae4d75b89e54cfe470eb152e527a403e30b2fb3fdf5dcad1019f015827a1871431dd7202cb520ddcd3b0205cc2b9aafcb6b52522562d381d05cac4522f258100000000").unwrap(); + + let prev_outputs = + vec![ + // Added by holder (SIGHASH_ALL | ACP) + TxOut { + value: Amount::from_sat(546), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(XOnlyPublicKey::from_slice( + &<[u8; 32]>::from_hex( + "cbc74f986822b48c4801ef5a1cadc44b27f7d23e699d8244c391d5defd69802a", + ) + .unwrap(), + ).unwrap()), + ), + }, + // Added by remote (corresponding input should not be checked) + TxOut { + value: Amount::from_sat(250148), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(XOnlyPublicKey::from_slice( + &<[u8; 32]>::from_hex( + "56cee5ccf725d94a428100de365fdfa134ff4deb1a0dca14470e70b4a64ff32b", + ) + .unwrap(), + ).unwrap()), + ), + }, + ]; + + match do_verify_tx_signatures(transaction, prev_outputs) { + Ok(_) => panic!("Should not be valid"), + Err(err) => { + assert_eq!( + &err, + "Signature does not use SIGHASH_DEFAULT or SIGHASH_ALL for input at index 0 for P2TR key path spend" + ); + }, + } + } + + #[test] + fn test_verify_tx_signatures_p2wsh_multisig() { + // Using on-chain transaction: https://mempool.space/tx/c28d01b47b8426039306e4209534fc5235da4a31406179639c54c48212be7655 + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("02000000000101d457eb6d1d7b9d0921d24449aede5f45a1e14e8f90aefd12f429e80741c0410d0100000000fdffffff02242c00000000000022002033476d89781e0006ce6a15f0b916cd5d53cf1a0f34d9d44273821148f8299db550340300000000001600146283a887af0a60239b5e18c5409a60cdf0404b8f0400473044022045fa871b357509376288e1933c010c988a55c370182a3d82cf4541a4850ee28c02203f22903165cccc06a124d5058c07c097fca3faf6048b62a9770a2c2eb23078810147304402202bd94f4be066f81ec8dffdef137c8ea99fdd6dcb8f69ee6ce95f8b4b237566f20220080e392372323b383efde7e013f570545103cdb04bf641587350d11cb4c4585b01695221030e17c0365f9f933b5fe08711069fb0e83af497eff9aa69488e13d04698e12c95210383b4b6d2e49bc7f3d211393193ae4a8b0d34d076632764950feb0f11451dcad22103de6d5d777364d5a7b92cf1ae3f45d2f71f2ec88289938e267f2cbc8a88eff33253aeb4d60d00").unwrap(); + + let prev_outputs = vec![ + // Added by holder + TxOut { + value: Amount::from_sat(221691), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "dca8b773bb8a3beb76dff2c2998642449ec989d158ce049ec94a1af29b69b008", + ) + .unwrap(), + )), + }, + ]; + + assert!(do_verify_tx_signatures(transaction, prev_outputs).is_ok()); + } + + #[test] + fn test_verify_tx_signatures_p2wpkh() { + // Using on-chain transaction: https://mempool.space/tx/3db96e0b60bb823c55e35560521ec4bb05962ac109400f1e5c56b8fe642958e6 + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("02000000000102939dd4cba8ca232c39647e7366c4f1a05ad0102a563b1df4f3befc351ca8c65d0000000000ffffffff58f776fcc31c6b9bfbd5839ea7a6b5da1bfc9bcc9e948f3b4be0792483b7d0e10100000000ffffffff029ad30000000000001600140b29de1e14f8ebc26b65d307b66d521ceb8d40b0e97d01000000000017a9149be05916325c1333820ebc00c80d6bf5c60a52b48702483045022100867147fa982ec6bc73fea19f68efea136f85de113d782bd6810d8b441454c89d02206e14c424bfc99c4edc041c226b1298bdeb2637733fa994d1aba5124befc2f04a012103c297c5a04f842757d1b4a8115c86dedf6e271afff8185eb73d21b45fe3d00e8402483045022100ccc1e6d1b30afa65069e5228e8e962552177e60f25b56f17d31363baaf9c7a5c022056d661653a5ad31760ec8e0bb1be50faa23883aebf4646fabadcdb628766d8a6012103c297c5a04f842757d1b4a8115c86dedf6e271afff8185eb73d21b45fe3d00e8400000000").unwrap(); + + let prev_outputs = vec![ + // Added by holder + TxOut { + value: Amount::from_sat(104127), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array( + <[u8; 20]>::from_hex("0b29de1e14f8ebc26b65d307b66d521ceb8d40b0").unwrap(), + )), + }, + // Added by remote (corresponding input should not be checked) + TxOut { + value: Amount::from_sat(48509), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array( + <[u8; 20]>::from_hex("0b29de1e14f8ebc26b65d307b66d521ceb8d40b0").unwrap(), + )), + }, + ]; + + assert!(do_verify_tx_signatures(transaction, prev_outputs).is_ok()); + } + + #[test] + fn test_verify_tx_signatures_p2tr_key_path() { + // Using on-chain transaction: https://mempool.space/tx/d26108e025ada641e4f1163e372c74087c0e471f3756bd3c736854bee9b5a06a + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("020000000001025f17ea06dd80e90a7c59bf2710903d938561f45c08cd3187d379e988f282d3c30000000000fdffffff10146764d4bb7e5fe0503df41a042ff39b175070ab1dd05345cbe1a12ac6fbbd0100000000fdffffff01a04f020000000000220020d55050579d2bcdf9ecfdf75df7741b8ac16d572b5cdf326028b4f3538ad34b5e0140e769ec44d5e30fe84ff5d873ed20d1a8ffa8b444e208b0584e24cb94b798286f46f4ba0d4dedfa279870c6b2e43aee45802128e7227e45a043c6193743c1c3240400483045022100cdf0cedd4e35d23af24e0c786bce5bbb47147e867e72fdb49b165a0fa7cac668022035493bb8f280115846c3475f51f7e1b56ec67dfb73744faa74b47d049e20436f01473044022034e60933f7a42effe174dbbb33ec60c1e4b06df1f0356caffbfae053944b552702207b59f352bb8a6100ca14c6dc486eb17145bde714c26766cd8bc2e0a139b06789014752210329a0c88d99fa89cb9497205a237da07b26737e5382dafca6cf40a3fd454b955021032e80b176382ccb76832cd773cf76cbb89883ea74a5b1bb5fa0e30b0bfc87ed8452ae7ed70d00").unwrap(); + + let prev_outputs = + vec![ + // Added by holder + TxOut { + value: Amount::from_sat(25841), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(XOnlyPublicKey::from_slice( + &<[u8; 32]>::from_hex( + "ce78617dd8b31b96b24e89140639f9d87b6c6cf3b2cc8f3ff2b3afa0e505d7ec", + ) + .unwrap(), + ).unwrap()), + ), + }, + // Added by remote (corresponding input should not be checked) + TxOut { + value: Amount::from_sat(126239), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array( + <[u8; 32]>::from_hex( + "c9b4e860479f930f054949e5a0be58d25958204e819cc1c62f89c48216eaab27", + ) + .unwrap(), + )), + }, + ]; + + assert!(do_verify_tx_signatures(transaction, prev_outputs).is_ok()); + } + + #[test] + fn test_verify_tx_signatures_p2tr_script_path() { + // Using on-chain transaction: https://mempool.space/tx/905ecdf95a84804b192f4dc221cfed4d77959b81ed66013a7e41a6e61e7ed530 + let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex("02000000000101b41b20295ac85fd2ae3e3d02900f1a1e7ddd6139b12e341386189c03d6f5795b0000000000fdffffff0100000000000000003c6a3a546878205361746f7368692120e2889e2f32316d696c20466972737420546170726f6f74206d756c7469736967207370656e64202d426974476f044123b1d4ff27b16af4b0fcb9672df671701a1a7f5a6bb7352b051f461edbc614aa6068b3e5313a174f90f3d95dc4e06f69bebd9cf5a3098fde034b01e69e8e788901400fd4a0d3f36a1f1074cb15838a48f572dc18d412d0f0f0fc1eeda9fa4820c942abb77e4d1a3c2b99ccf4ad29d9189e6e04a017fe611748464449f681bc38cf394420febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4ad20d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15ac41c02e44c9e47eaeb4bb313adecd11012dfad435cd72ce71f525329f24d75c5b9432774e148e9209baf3f1656a46986d5f38ddf4e20912c6ac28f48d6bf747469fb100000000").unwrap(); + + let prev_outputs = + vec![ + // Added by holder + TxOut { + value: Amount::from_sat(7500), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(XOnlyPublicKey::from_slice( + &<[u8; 32]>::from_hex( + "2fcad7470279652cc5f88b8908678d6f4d57af5627183b03fc8404cb4e16d889", + ) + .unwrap(), + ).unwrap()), + ), + }, + ]; + + assert!(do_verify_tx_signatures(transaction, prev_outputs).is_ok()); + } }