From: Duncan Dean Date: Tue, 21 Mar 2023 19:12:53 +0000 (+0200) Subject: Implement interactive tx construction protocol via `InteractiveTxConstructor` X-Git-Tag: v0.0.123-beta~32^2 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=c56198ade5dc69f86429db69cd52a767fa5fbb52;p=rust-lightning Implement interactive tx construction protocol via `InteractiveTxConstructor` This implements the interactive construction protocol described at https://github.com/lightning/bolts/blob/78e5a6b066d3a8e235931dfc06aa325337874749/02-peer-protocol.md?plain=1#L92. Our implementation includes a state machine with typed states and transitions to ensure consumers have compile-time assurances that the protocol is upheld. States are tracked as in the `StateMachine` enum and can take on all possible states during the negotiation. The states are further divided into two categories, namely by the two traits they implement, either `ReceivedMsgState` or `SentMsgState`. The defined `StateTransitions` enforce the transitions that `ReceivedMsgState`, `SentMsgState`, and the `_TxComplete`s can go through. Co-authored-by: Wilmer Paulino <9447167+wpaulino@users.noreply.github.com> Co-authored-by: Duncan Dean Co-authored-by: Jurvis Tan <5944973+jurvis@users.noreply.github.com> --- diff --git a/lightning/src/events/bump_transaction.rs b/lightning/src/events/bump_transaction.rs index 44f44dd31..8b52af69f 100644 --- a/lightning/src/events/bump_transaction.rs +++ b/lightning/src/events/bump_transaction.rs @@ -41,11 +41,11 @@ use bitcoin::secp256k1; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::secp256k1::ecdsa::Signature; -const EMPTY_SCRIPT_SIG_WEIGHT: u64 = 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64; +pub(crate) const EMPTY_SCRIPT_SIG_WEIGHT: u64 = 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64; const BASE_INPUT_SIZE: u64 = 32 /* txid */ + 4 /* vout */ + 4 /* sequence */; -const BASE_INPUT_WEIGHT: u64 = BASE_INPUT_SIZE * WITNESS_SCALE_FACTOR as u64; +pub(crate) const BASE_INPUT_WEIGHT: u64 = BASE_INPUT_SIZE * WITNESS_SCALE_FACTOR as u64; /// A descriptor used to sign for a commitment transaction's anchor output. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs new file mode 100644 index 000000000..94311a367 --- /dev/null +++ b/lightning/src/ln/interactivetxs.rs @@ -0,0 +1,1384 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::io_extras::sink; +use crate::prelude::*; +use core::ops::Deref; + +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; +use bitcoin::consensus::Encodable; +use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; +use bitcoin::{ + absolute::LockTime as AbsoluteLockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, +}; + +use crate::chain::chaininterface::fee_for_weight; +use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT}; +use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; +use crate::ln::msgs::SerialId; +use crate::ln::{msgs, ChannelId}; +use crate::sign::EntropySource; +use crate::util::ser::TransactionU16LenLimited; + +/// The number of received `tx_add_input` messages during a negotiation at which point the +/// negotiation MUST be failed. +const MAX_RECEIVED_TX_ADD_INPUT_COUNT: u16 = 4096; + +/// The number of received `tx_add_output` messages during a negotiation at which point the +/// negotiation MUST be failed. +const MAX_RECEIVED_TX_ADD_OUTPUT_COUNT: u16 = 4096; + +/// The number of inputs or outputs that the state machine can have, before it MUST fail the +/// negotiation. +const MAX_INPUTS_OUTPUTS_COUNT: usize = 252; + +trait SerialIdExt { + fn is_for_initiator(&self) -> bool; + fn is_for_non_initiator(&self) -> bool; +} + +impl SerialIdExt for SerialId { + fn is_for_initiator(&self) -> bool { + self % 2 == 0 + } + + fn is_for_non_initiator(&self) -> bool { + !self.is_for_initiator() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AbortReason { + InvalidStateTransition, + UnexpectedCounterpartyMessage, + ReceivedTooManyTxAddInputs, + ReceivedTooManyTxAddOutputs, + IncorrectInputSequenceValue, + IncorrectSerialIdParity, + SerialIdUnknown, + DuplicateSerialId, + PrevTxOutInvalid, + ExceededMaximumSatsAllowed, + ExceededNumberOfInputsOrOutputs, + TransactionTooLarge, + BelowDustLimit, + InvalidOutputScript, + InsufficientFees, + OutputsValueExceedsInputsValue, + InvalidTx, +} + +#[derive(Debug)] +pub struct TxInputWithPrevOutput { + input: TxIn, + prev_output: TxOut, +} + +#[derive(Debug)] +struct NegotiationContext { + holder_is_initiator: bool, + received_tx_add_input_count: u16, + received_tx_add_output_count: u16, + inputs: HashMap, + prevtx_outpoints: HashSet, + outputs: HashMap, + tx_locktime: AbsoluteLockTime, + feerate_sat_per_kw: u32, + to_remote_value_satoshis: u64, +} + +impl NegotiationContext { + fn is_serial_id_valid_for_counterparty(&self, serial_id: &SerialId) -> bool { + // A received `SerialId`'s parity must match the role of the counterparty. + self.holder_is_initiator == serial_id.is_for_non_initiator() + } + + fn total_input_and_output_count(&self) -> usize { + self.inputs.len().saturating_add(self.outputs.len()) + } + + fn counterparty_inputs_contributed( + &self, + ) -> impl Iterator + Clone { + self.inputs + .iter() + .filter(move |(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) + .map(|(_, input_with_prevout)| input_with_prevout) + } + + fn counterparty_outputs_contributed(&self) -> impl Iterator + Clone { + self.outputs + .iter() + .filter(move |(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) + .map(|(_, output)| output) + } + + fn received_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> { + // The interactive-txs spec calls for us to fail negotiation if the `prevtx` we receive is + // invalid. However, we would not need to account for this explicit negotiation failure + // mode here since `PeerManager` would already disconnect the peer if the `prevtx` is + // invalid; implicitly ending the negotiation. + + if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + // The receiving node: + // - MUST fail the negotiation if: + // - the `serial_id` has the wrong parity + return Err(AbortReason::IncorrectSerialIdParity); + } + + self.received_tx_add_input_count += 1; + if self.received_tx_add_input_count > MAX_RECEIVED_TX_ADD_INPUT_COUNT { + // The receiving node: + // - MUST fail the negotiation if: + // - if has received 4096 `tx_add_input` messages during this negotiation + return Err(AbortReason::ReceivedTooManyTxAddInputs); + } + + if msg.sequence >= 0xFFFFFFFE { + // The receiving node: + // - MUST fail the negotiation if: + // - `sequence` is set to `0xFFFFFFFE` or `0xFFFFFFFF` + return Err(AbortReason::IncorrectInputSequenceValue); + } + + let transaction = msg.prevtx.as_transaction(); + let txid = transaction.txid(); + + if let Some(tx_out) = transaction.output.get(msg.prevtx_out as usize) { + if !tx_out.script_pubkey.is_witness_program() { + // The receiving node: + // - MUST fail the negotiation if: + // - the `scriptPubKey` is not a witness program + return Err(AbortReason::PrevTxOutInvalid); + } + + if !self.prevtx_outpoints.insert(OutPoint { txid, vout: msg.prevtx_out }) { + // The receiving node: + // - MUST fail the negotiation if: + // - the `prevtx` and `prevtx_vout` are identical to a previously added + // (and not removed) input's + return Err(AbortReason::PrevTxOutInvalid); + } + } else { + // The receiving node: + // - MUST fail the negotiation if: + // - `prevtx_vout` is greater or equal to the number of outputs on `prevtx` + return Err(AbortReason::PrevTxOutInvalid); + } + + let prev_out = if let Some(prev_out) = transaction.output.get(msg.prevtx_out as usize) { + prev_out.clone() + } else { + return Err(AbortReason::PrevTxOutInvalid); + }; + if self.inputs.iter().any(|(serial_id, _)| *serial_id == msg.serial_id) { + // The receiving node: + // - MUST fail the negotiation if: + // - the `serial_id` is already included in the transaction + return Err(AbortReason::DuplicateSerialId); + } + let prev_outpoint = OutPoint { txid, vout: msg.prevtx_out }; + self.inputs.entry(msg.serial_id).or_insert_with(|| TxInputWithPrevOutput { + input: TxIn { + previous_output: prev_outpoint.clone(), + sequence: Sequence(msg.sequence), + ..Default::default() + }, + prev_output: prev_out, + }); + self.prevtx_outpoints.insert(prev_outpoint); + Ok(()) + } + + fn received_tx_remove_input(&mut self, msg: &msgs::TxRemoveInput) -> Result<(), AbortReason> { + if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + return Err(AbortReason::IncorrectSerialIdParity); + } + + self.inputs + .remove(&msg.serial_id) + // The receiving node: + // - MUST fail the negotiation if: + // - the input or output identified by the `serial_id` was not added by the sender + // - the `serial_id` does not correspond to a currently added input + .ok_or(AbortReason::SerialIdUnknown) + .map(|_| ()) + } + + fn received_tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> Result<(), AbortReason> { + // The receiving node: + // - MUST fail the negotiation if: + // - the serial_id has the wrong parity + if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + return Err(AbortReason::IncorrectSerialIdParity); + } + + self.received_tx_add_output_count += 1; + if self.received_tx_add_output_count > MAX_RECEIVED_TX_ADD_OUTPUT_COUNT { + // The receiving node: + // - MUST fail the negotiation if: + // - if has received 4096 `tx_add_output` messages during this negotiation + return Err(AbortReason::ReceivedTooManyTxAddOutputs); + } + + if msg.sats < msg.script.dust_value().to_sat() { + // The receiving node: + // - MUST fail the negotiation if: + // - the sats amount is less than the dust_limit + return Err(AbortReason::BelowDustLimit); + } + + // Check that adding this output would not cause the total output value to exceed the total + // bitcoin supply. + let mut outputs_value: u64 = 0; + for output in self.outputs.iter() { + outputs_value = outputs_value.saturating_add(output.1.value); + } + if outputs_value.saturating_add(msg.sats) > TOTAL_BITCOIN_SUPPLY_SATOSHIS { + // The receiving node: + // - MUST fail the negotiation if: + // - the sats amount is greater than 2,100,000,000,000,000 (TOTAL_BITCOIN_SUPPLY_SATOSHIS) + return Err(AbortReason::ExceededMaximumSatsAllowed); + } + + // The receiving node: + // - MUST accept P2WSH, P2WPKH, P2TR scripts + // - MAY fail the negotiation if script is non-standard + // + // We can actually be a bit looser than the above as only witness version 0 has special + // length-based standardness constraints to match similar consensus rules. All witness scripts + // with witness versions V1 and up are always considered standard. Yes, the scripts can be + // anyone-can-spend-able, but if our counterparty wants to add an output like that then it's none + // of our concern really ¯\_(ツ)_/¯ + if !msg.script.is_v0_p2wpkh() + && !msg.script.is_v0_p2wsh() + && msg.script.witness_version().map(|v| v.to_num() < 1).unwrap_or(true) + { + return Err(AbortReason::InvalidOutputScript); + } + + if self.outputs.iter().any(|(serial_id, _)| *serial_id == msg.serial_id) { + // The receiving node: + // - MUST fail the negotiation if: + // - the `serial_id` is already included in the transaction + return Err(AbortReason::DuplicateSerialId); + } + + let output = TxOut { value: msg.sats, script_pubkey: msg.script.clone() }; + self.outputs.entry(msg.serial_id).or_insert(output); + Ok(()) + } + + fn received_tx_remove_output(&mut self, msg: &msgs::TxRemoveOutput) -> Result<(), AbortReason> { + if !self.is_serial_id_valid_for_counterparty(&msg.serial_id) { + return Err(AbortReason::IncorrectSerialIdParity); + } + if let Some(_) = self.outputs.remove(&msg.serial_id) { + Ok(()) + } else { + // The receiving node: + // - MUST fail the negotiation if: + // - the input or output identified by the `serial_id` was not added by the sender + // - the `serial_id` does not correspond to a currently added input + Err(AbortReason::SerialIdUnknown) + } + } + + fn sent_tx_add_input(&mut self, msg: &msgs::TxAddInput) { + let tx = msg.prevtx.as_transaction(); + let input = TxIn { + previous_output: OutPoint { txid: tx.txid(), vout: msg.prevtx_out }, + sequence: Sequence(msg.sequence), + ..Default::default() + }; + debug_assert!((msg.prevtx_out as usize) < tx.output.len()); + let prev_output = &tx.output[msg.prevtx_out as usize]; + self.prevtx_outpoints.insert(input.previous_output.clone()); + self.inputs.insert( + msg.serial_id, + TxInputWithPrevOutput { input, prev_output: prev_output.clone() }, + ); + } + + fn sent_tx_add_output(&mut self, msg: &msgs::TxAddOutput) { + self.outputs + .insert(msg.serial_id, TxOut { value: msg.sats, script_pubkey: msg.script.clone() }); + } + + fn sent_tx_remove_input(&mut self, msg: &msgs::TxRemoveInput) { + self.inputs.remove(&msg.serial_id); + } + + fn sent_tx_remove_output(&mut self, msg: &msgs::TxRemoveOutput) { + self.outputs.remove(&msg.serial_id); + } + + fn build_transaction(self) -> Result { + // The receiving node: + // MUST fail the negotiation if: + + // - the peer's total input satoshis is less than their outputs + let mut counterparty_inputs_value: u64 = 0; + let mut counterparty_outputs_value: u64 = 0; + for input in self.counterparty_inputs_contributed() { + counterparty_inputs_value = + counterparty_inputs_value.saturating_add(input.prev_output.value); + } + for output in self.counterparty_outputs_contributed() { + counterparty_outputs_value = counterparty_outputs_value.saturating_add(output.value); + } + // ...actually the counterparty might be splicing out, so that their balance also contributes + // to the total input value. + if counterparty_inputs_value.saturating_add(self.to_remote_value_satoshis) + < counterparty_outputs_value + { + return Err(AbortReason::OutputsValueExceedsInputsValue); + } + + // - there are more than 252 inputs + // - there are more than 252 outputs + if self.inputs.len() > MAX_INPUTS_OUTPUTS_COUNT + || self.outputs.len() > MAX_INPUTS_OUTPUTS_COUNT + { + return Err(AbortReason::ExceededNumberOfInputsOrOutputs); + } + + // TODO: How do we enforce their fees cover the witness without knowing its expected length? + const INPUT_WEIGHT: u64 = BASE_INPUT_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT; + + // - the peer's paid feerate does not meet or exceed the agreed feerate (based on the minimum fee). + let counterparty_output_weight_contributed: u64 = self + .counterparty_outputs_contributed() + .map(|output| { + (8 /* value */ + output.script_pubkey.consensus_encode(&mut sink()).unwrap() as u64) + * WITNESS_SCALE_FACTOR as u64 + }) + .sum(); + let counterparty_weight_contributed = counterparty_output_weight_contributed + + self.counterparty_inputs_contributed().count() as u64 * INPUT_WEIGHT; + let counterparty_fees_contributed = + counterparty_inputs_value.saturating_sub(counterparty_outputs_value); + let mut required_counterparty_contribution_fee = + fee_for_weight(self.feerate_sat_per_kw, counterparty_weight_contributed); + if !self.holder_is_initiator { + // if is the non-initiator: + // - the initiator's fees do not cover the common fields (version, segwit marker + flag, + // input count, output count, locktime) + let tx_common_fields_weight = + (4 /* version */ + 4 /* locktime */ + 1 /* input count */ + 1 /* output count */) * + WITNESS_SCALE_FACTOR as u64 + 2 /* segwit marker + flag */; + let tx_common_fields_fee = + fee_for_weight(self.feerate_sat_per_kw, tx_common_fields_weight); + required_counterparty_contribution_fee += tx_common_fields_fee; + } + if counterparty_fees_contributed < required_counterparty_contribution_fee { + return Err(AbortReason::InsufficientFees); + } + + // Inputs and outputs must be sorted by serial_id + let mut inputs = self.inputs.into_iter().collect::>(); + let mut outputs = self.outputs.into_iter().collect::>(); + inputs.sort_unstable_by_key(|(serial_id, _)| *serial_id); + outputs.sort_unstable_by_key(|(serial_id, _)| *serial_id); + + let tx_to_validate = Transaction { + version: 2, + lock_time: self.tx_locktime, + input: inputs.into_iter().map(|(_, input)| input.input).collect(), + output: outputs.into_iter().map(|(_, output)| output).collect(), + }; + if tx_to_validate.weight().to_wu() > MAX_STANDARD_TX_WEIGHT as u64 { + return Err(AbortReason::TransactionTooLarge); + } + + Ok(tx_to_validate) + } +} + +// The interactive transaction construction protocol allows two peers to collaboratively build a +// transaction for broadcast. +// +// The protocol is turn-based, so we define different states here that we store depending on whose +// turn it is to send the next message. The states are defined so that their types ensure we only +// perform actions (only send messages) via defined state transitions that do not violate the +// protocol. +// +// An example of a full negotiation and associated states follows: +// +// +------------+ +------------------+---- Holder state after message sent/received ----+ +// | |--(1)- tx_add_input ---->| | SentChangeMsg + +// | |<-(2)- tx_complete ------| | ReceivedTxComplete + +// | |--(3)- tx_add_output --->| | SentChangeMsg + +// | |<-(4)- tx_complete ------| | ReceivedTxComplete + +// | |--(5)- tx_add_input ---->| | SentChangeMsg + +// | Holder |<-(6)- tx_add_input -----| Counterparty | ReceivedChangeMsg + +// | |--(7)- tx_remove_output >| | SentChangeMsg + +// | |<-(8)- tx_add_output ----| | ReceivedChangeMsg + +// | |--(9)- tx_complete ----->| | SentTxComplete + +// | |<-(10) tx_complete ------| | NegotiationComplete + +// +------------+ +------------------+--------------------------------------------------+ + +/// Negotiation states that can send & receive `tx_(add|remove)_(input|output)` and `tx_complete` +trait State {} + +/// Category of states where we have sent some message to the counterparty, and we are waiting for +/// a response. +trait SentMsgState: State { + fn into_negotiation_context(self) -> NegotiationContext; +} + +/// Category of states that our counterparty has put us in after we receive a message from them. +trait ReceivedMsgState: State { + fn into_negotiation_context(self) -> NegotiationContext; +} + +// This macro is a helper for implementing the above state traits for various states subsequently +// defined below the macro. +macro_rules! define_state { + (SENT_MSG_STATE, $state: ident, $doc: expr) => { + define_state!($state, NegotiationContext, $doc); + impl SentMsgState for $state { + fn into_negotiation_context(self) -> NegotiationContext { + self.0 + } + } + }; + (RECEIVED_MSG_STATE, $state: ident, $doc: expr) => { + define_state!($state, NegotiationContext, $doc); + impl ReceivedMsgState for $state { + fn into_negotiation_context(self) -> NegotiationContext { + self.0 + } + } + }; + ($state: ident, $inner: ident, $doc: expr) => { + #[doc = $doc] + #[derive(Debug)] + struct $state($inner); + impl State for $state {} + }; +} + +define_state!( + SENT_MSG_STATE, + SentChangeMsg, + "We have sent a message to the counterparty that has affected our negotiation state." +); +define_state!( + SENT_MSG_STATE, + SentTxComplete, + "We have sent a `tx_complete` message and are awaiting the counterparty's." +); +define_state!( + RECEIVED_MSG_STATE, + ReceivedChangeMsg, + "We have received a message from the counterparty that has affected our negotiation state." +); +define_state!( + RECEIVED_MSG_STATE, + ReceivedTxComplete, + "We have received a `tx_complete` message and the counterparty is awaiting ours." +); +define_state!(NegotiationComplete, Transaction, "We have exchanged consecutive `tx_complete` messages with the counterparty and the transaction negotiation is complete."); +define_state!( + NegotiationAborted, + AbortReason, + "The negotiation has failed and cannot be continued." +); + +type StateTransitionResult = Result; + +trait StateTransition { + fn transition(self, data: TransitionData) -> StateTransitionResult; +} + +// This macro helps define the legal transitions between the states above by implementing +// the `StateTransition` trait for each of the states that follow this declaration. +macro_rules! define_state_transitions { + (SENT_MSG_STATE, [$(DATA $data: ty, TRANSITION $transition: ident),+]) => { + $( + impl StateTransition for S { + fn transition(self, data: $data) -> StateTransitionResult { + let mut context = self.into_negotiation_context(); + context.$transition(data)?; + Ok(ReceivedChangeMsg(context)) + } + } + )* + }; + (RECEIVED_MSG_STATE, [$(DATA $data: ty, TRANSITION $transition: ident),+]) => { + $( + impl StateTransition for S { + fn transition(self, data: $data) -> StateTransitionResult { + let mut context = self.into_negotiation_context(); + context.$transition(data); + Ok(SentChangeMsg(context)) + } + } + )* + }; + (TX_COMPLETE, $from_state: ident, $tx_complete_state: ident) => { + impl StateTransition for $tx_complete_state { + fn transition(self, _data: &msgs::TxComplete) -> StateTransitionResult { + let context = self.into_negotiation_context(); + let tx = context.build_transaction()?; + Ok(NegotiationComplete(tx)) + } + } + + impl StateTransition<$tx_complete_state, &msgs::TxComplete> for $from_state { + fn transition(self, _data: &msgs::TxComplete) -> StateTransitionResult<$tx_complete_state> { + Ok($tx_complete_state(self.into_negotiation_context())) + } + } + }; +} + +// State transitions when we have sent our counterparty some messages and are waiting for them +// to respond. +define_state_transitions!(SENT_MSG_STATE, [ + DATA &msgs::TxAddInput, TRANSITION received_tx_add_input, + DATA &msgs::TxRemoveInput, TRANSITION received_tx_remove_input, + DATA &msgs::TxAddOutput, TRANSITION received_tx_add_output, + DATA &msgs::TxRemoveOutput, TRANSITION received_tx_remove_output +]); +// State transitions when we have received some messages from our counterparty and we should +// respond. +define_state_transitions!(RECEIVED_MSG_STATE, [ + DATA &msgs::TxAddInput, TRANSITION sent_tx_add_input, + DATA &msgs::TxRemoveInput, TRANSITION sent_tx_remove_input, + DATA &msgs::TxAddOutput, TRANSITION sent_tx_add_output, + DATA &msgs::TxRemoveOutput, TRANSITION sent_tx_remove_output +]); +define_state_transitions!(TX_COMPLETE, SentChangeMsg, ReceivedTxComplete); +define_state_transitions!(TX_COMPLETE, ReceivedChangeMsg, SentTxComplete); + +#[derive(Debug)] +enum StateMachine { + Indeterminate, + SentChangeMsg(SentChangeMsg), + ReceivedChangeMsg(ReceivedChangeMsg), + SentTxComplete(SentTxComplete), + ReceivedTxComplete(ReceivedTxComplete), + NegotiationComplete(NegotiationComplete), + NegotiationAborted(NegotiationAborted), +} + +impl Default for StateMachine { + fn default() -> Self { + Self::Indeterminate + } +} + +// The `StateMachine` internally executes the actual transition between two states and keeps +// track of the current state. This macro defines _how_ those state transitions happen to +// update the internal state. +macro_rules! define_state_machine_transitions { + ($transition: ident, $msg: ty, [$(FROM $from_state: ident, TO $to_state: ident),+]) => { + fn $transition(self, msg: $msg) -> StateMachine { + match self { + $( + Self::$from_state(s) => match s.transition(msg) { + Ok(new_state) => StateMachine::$to_state(new_state), + Err(abort_reason) => StateMachine::NegotiationAborted(NegotiationAborted(abort_reason)), + } + )* + _ => StateMachine::NegotiationAborted(NegotiationAborted(AbortReason::UnexpectedCounterpartyMessage)), + } + } + }; +} + +impl StateMachine { + fn new( + feerate_sat_per_kw: u32, is_initiator: bool, tx_locktime: AbsoluteLockTime, + to_remote_value_satoshis: u64, + ) -> Self { + let context = NegotiationContext { + tx_locktime, + holder_is_initiator: is_initiator, + received_tx_add_input_count: 0, + received_tx_add_output_count: 0, + inputs: new_hash_map(), + prevtx_outpoints: new_hash_set(), + outputs: new_hash_map(), + feerate_sat_per_kw, + to_remote_value_satoshis, + }; + if is_initiator { + Self::ReceivedChangeMsg(ReceivedChangeMsg(context)) + } else { + Self::SentChangeMsg(SentChangeMsg(context)) + } + } + + // TxAddInput + define_state_machine_transitions!(sent_tx_add_input, &msgs::TxAddInput, [ + FROM ReceivedChangeMsg, TO SentChangeMsg, + FROM ReceivedTxComplete, TO SentChangeMsg + ]); + define_state_machine_transitions!(received_tx_add_input, &msgs::TxAddInput, [ + FROM SentChangeMsg, TO ReceivedChangeMsg, + FROM SentTxComplete, TO ReceivedChangeMsg + ]); + + // TxAddOutput + define_state_machine_transitions!(sent_tx_add_output, &msgs::TxAddOutput, [ + FROM ReceivedChangeMsg, TO SentChangeMsg, + FROM ReceivedTxComplete, TO SentChangeMsg + ]); + define_state_machine_transitions!(received_tx_add_output, &msgs::TxAddOutput, [ + FROM SentChangeMsg, TO ReceivedChangeMsg, + FROM SentTxComplete, TO ReceivedChangeMsg + ]); + + // TxRemoveInput + define_state_machine_transitions!(sent_tx_remove_input, &msgs::TxRemoveInput, [ + FROM ReceivedChangeMsg, TO SentChangeMsg, + FROM ReceivedTxComplete, TO SentChangeMsg + ]); + define_state_machine_transitions!(received_tx_remove_input, &msgs::TxRemoveInput, [ + FROM SentChangeMsg, TO ReceivedChangeMsg, + FROM SentTxComplete, TO ReceivedChangeMsg + ]); + + // TxRemoveOutput + define_state_machine_transitions!(sent_tx_remove_output, &msgs::TxRemoveOutput, [ + FROM ReceivedChangeMsg, TO SentChangeMsg, + FROM ReceivedTxComplete, TO SentChangeMsg + ]); + define_state_machine_transitions!(received_tx_remove_output, &msgs::TxRemoveOutput, [ + FROM SentChangeMsg, TO ReceivedChangeMsg, + FROM SentTxComplete, TO ReceivedChangeMsg + ]); + + // TxComplete + define_state_machine_transitions!(sent_tx_complete, &msgs::TxComplete, [ + FROM ReceivedChangeMsg, TO SentTxComplete, + FROM ReceivedTxComplete, TO NegotiationComplete + ]); + define_state_machine_transitions!(received_tx_complete, &msgs::TxComplete, [ + FROM SentChangeMsg, TO ReceivedTxComplete, + FROM SentTxComplete, TO NegotiationComplete + ]); +} + +pub struct InteractiveTxConstructor { + state_machine: StateMachine, + channel_id: ChannelId, + inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)>, + outputs_to_contribute: Vec<(SerialId, TxOut)>, +} + +pub enum InteractiveTxMessageSend { + TxAddInput(msgs::TxAddInput), + TxAddOutput(msgs::TxAddOutput), + TxComplete(msgs::TxComplete), +} + +// This macro executes a state machine transition based on a provided action. +macro_rules! do_state_transition { + ($self: ident, $transition: ident, $msg: expr) => {{ + let state_machine = core::mem::take(&mut $self.state_machine); + $self.state_machine = state_machine.$transition($msg); + match &$self.state_machine { + StateMachine::NegotiationAborted(state) => Err(state.0.clone()), + _ => Ok(()), + } + }}; +} + +fn generate_holder_serial_id(entropy_source: &ES, is_initiator: bool) -> SerialId +where + ES::Target: EntropySource, +{ + let rand_bytes = entropy_source.get_secure_random_bytes(); + let mut serial_id_bytes = [0u8; 8]; + serial_id_bytes.copy_from_slice(&rand_bytes[..8]); + let mut serial_id = u64::from_be_bytes(serial_id_bytes); + if serial_id.is_for_initiator() != is_initiator { + serial_id ^= 1; + } + serial_id +} + +pub enum HandleTxCompleteValue { + SendTxMessage(InteractiveTxMessageSend), + SendTxComplete(InteractiveTxMessageSend, Transaction), + NegotiationComplete(Transaction), +} + +impl InteractiveTxConstructor { + /// Instantiates a new `InteractiveTxConstructor`. + /// + /// If this is for a dual_funded channel then the `to_remote_value_satoshis` parameter should be set + /// to zero. + /// + /// A tuple is returned containing the newly instantiate `InteractiveTxConstructor` and optionally + /// an initial wrapped `Tx_` message which the holder needs to send to the counterparty. + pub fn new( + entropy_source: &ES, channel_id: ChannelId, feerate_sat_per_kw: u32, is_initiator: bool, + funding_tx_locktime: AbsoluteLockTime, + inputs_to_contribute: Vec<(TxIn, TransactionU16LenLimited)>, + outputs_to_contribute: Vec, to_remote_value_satoshis: u64, + ) -> (Self, Option) + where + ES::Target: EntropySource, + { + let state_machine = StateMachine::new( + feerate_sat_per_kw, + is_initiator, + funding_tx_locktime, + to_remote_value_satoshis, + ); + let mut inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)> = + inputs_to_contribute + .into_iter() + .map(|(input, tx)| { + let serial_id = generate_holder_serial_id(entropy_source, is_initiator); + (serial_id, input, tx) + }) + .collect(); + // We'll sort by the randomly generated serial IDs, effectively shuffling the order of the inputs + // as the user passed them to us to avoid leaking any potential categorization of transactions + // before we pass any of the inputs to the counterparty. + inputs_to_contribute.sort_unstable_by_key(|(serial_id, _, _)| *serial_id); + let mut outputs_to_contribute: Vec<(SerialId, TxOut)> = outputs_to_contribute + .into_iter() + .map(|output| { + let serial_id = generate_holder_serial_id(entropy_source, is_initiator); + (serial_id, output) + }) + .collect(); + // In the same manner and for the same rationale as the inputs above, we'll shuffle the outputs. + outputs_to_contribute.sort_unstable_by_key(|(serial_id, _)| *serial_id); + let mut constructor = + Self { state_machine, channel_id, inputs_to_contribute, outputs_to_contribute }; + let message_send = if is_initiator { + match constructor.maybe_send_message() { + Ok(msg_send) => Some(msg_send), + Err(_) => { + debug_assert!( + false, + "We should always be able to start our state machine successfully" + ); + None + }, + } + } else { + None + }; + (constructor, message_send) + } + + fn maybe_send_message(&mut self) -> Result { + // We first attempt to send inputs we want to add, then outputs. Once we are done sending + // them both, then we always send tx_complete. + if let Some((serial_id, input, prevtx)) = self.inputs_to_contribute.pop() { + let msg = msgs::TxAddInput { + channel_id: self.channel_id, + serial_id, + prevtx, + prevtx_out: input.previous_output.vout, + sequence: input.sequence.to_consensus_u32(), + }; + do_state_transition!(self, sent_tx_add_input, &msg)?; + Ok(InteractiveTxMessageSend::TxAddInput(msg)) + } else if let Some((serial_id, output)) = self.outputs_to_contribute.pop() { + let msg = msgs::TxAddOutput { + channel_id: self.channel_id, + serial_id, + sats: output.value, + script: output.script_pubkey, + }; + do_state_transition!(self, sent_tx_add_output, &msg)?; + Ok(InteractiveTxMessageSend::TxAddOutput(msg)) + } else { + let msg = msgs::TxComplete { channel_id: self.channel_id }; + do_state_transition!(self, sent_tx_complete, &msg)?; + Ok(InteractiveTxMessageSend::TxComplete(msg)) + } + } + + pub fn handle_tx_add_input( + &mut self, msg: &msgs::TxAddInput, + ) -> Result { + do_state_transition!(self, received_tx_add_input, msg)?; + self.maybe_send_message() + } + + pub fn handle_tx_remove_input( + &mut self, msg: &msgs::TxRemoveInput, + ) -> Result { + do_state_transition!(self, received_tx_remove_input, msg)?; + self.maybe_send_message() + } + + pub fn handle_tx_add_output( + &mut self, msg: &msgs::TxAddOutput, + ) -> Result { + do_state_transition!(self, received_tx_add_output, msg)?; + self.maybe_send_message() + } + + pub fn handle_tx_remove_output( + &mut self, msg: &msgs::TxRemoveOutput, + ) -> Result { + do_state_transition!(self, received_tx_remove_output, msg)?; + self.maybe_send_message() + } + + pub fn handle_tx_complete( + &mut self, msg: &msgs::TxComplete, + ) -> Result { + do_state_transition!(self, received_tx_complete, msg)?; + match &self.state_machine { + StateMachine::ReceivedTxComplete(_) => { + let msg_send = self.maybe_send_message()?; + return match &self.state_machine { + StateMachine::NegotiationComplete(s) => { + Ok(HandleTxCompleteValue::SendTxComplete(msg_send, s.0.clone())) + }, + StateMachine::SentChangeMsg(_) => { + Ok(HandleTxCompleteValue::SendTxMessage(msg_send)) + }, // We either had an input or output to contribute. + _ => { + debug_assert!(false, "We cannot transition to any other states after receiving `tx_complete` and responding"); + return Err(AbortReason::InvalidStateTransition); + }, + }; + }, + StateMachine::NegotiationComplete(s) => { + Ok(HandleTxCompleteValue::NegotiationComplete(s.0.clone())) + }, + _ => { + debug_assert!( + false, + "We cannot transition to any other states after receiving `tx_complete`" + ); + Err(AbortReason::InvalidStateTransition) + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; + use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; + use crate::ln::interactivetxs::{ + generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, + InteractiveTxMessageSend, MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, + }; + use crate::ln::ChannelId; + use crate::sign::EntropySource; + use crate::util::atomic_counter::AtomicCounter; + use crate::util::ser::TransactionU16LenLimited; + use bitcoin::blockdata::opcodes; + use bitcoin::blockdata::script::Builder; + use bitcoin::{ + absolute::LockTime as AbsoluteLockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, + }; + use core::ops::Deref; + + // A simple entropy source that works based on an atomic counter. + struct TestEntropySource(AtomicCounter); + impl EntropySource for TestEntropySource { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let mut res = [0u8; 32]; + let increment = self.0.get_increment(); + for i in 0..32 { + // Rotate the increment value by 'i' bits to the right, to avoid clashes + // when `generate_local_serial_id` does a parity flip on consecutive calls for the + // same party. + let rotated_increment = increment.rotate_right(i as u32); + res[i] = (rotated_increment & 0xff) as u8; + } + res + } + } + + // An entropy source that deliberately returns you the same seed every time. We use this + // to test if the constructor would catch inputs/outputs that are attempting to be added + // with duplicate serial ids. + struct DuplicateEntropySource; + impl EntropySource for DuplicateEntropySource { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let mut res = [0u8; 32]; + let count = 1u64; + res[0..8].copy_from_slice(&count.to_be_bytes()); + res + } + } + + #[derive(Debug, PartialEq, Eq)] + enum ErrorCulprit { + NodeA, + NodeB, + // Some error values are only checked at the end of the negotiation and are not easy to attribute + // to a particular party. Both parties would indicate an `AbortReason` in this case. + // e.g. Exceeded max inputs and outputs after negotiation. + Indeterminate, + } + + struct TestSession { + inputs_a: Vec<(TxIn, TransactionU16LenLimited)>, + outputs_a: Vec, + inputs_b: Vec<(TxIn, TransactionU16LenLimited)>, + outputs_b: Vec, + expect_error: Option<(AbortReason, ErrorCulprit)>, + } + + fn do_test_interactive_tx_constructor(session: TestSession) { + let entropy_source = TestEntropySource(AtomicCounter::new()); + do_test_interactive_tx_constructor_internal(session, &&entropy_source); + } + + fn do_test_interactive_tx_constructor_with_entropy_source( + session: TestSession, entropy_source: ES, + ) where + ES::Target: EntropySource, + { + do_test_interactive_tx_constructor_internal(session, &entropy_source); + } + + fn do_test_interactive_tx_constructor_internal( + session: TestSession, entropy_source: &ES, + ) where + ES::Target: EntropySource, + { + let channel_id = ChannelId(entropy_source.get_secure_random_bytes()); + let tx_locktime = AbsoluteLockTime::from_height(1337).unwrap(); + + let (mut constructor_a, first_message_a) = InteractiveTxConstructor::new( + entropy_source, + channel_id, + FEERATE_FLOOR_SATS_PER_KW * 10, + true, + tx_locktime, + session.inputs_a, + session.outputs_a, + 0, + ); + let (mut constructor_b, first_message_b) = InteractiveTxConstructor::new( + entropy_source, + channel_id, + FEERATE_FLOOR_SATS_PER_KW * 10, + false, + tx_locktime, + session.inputs_b, + session.outputs_b, + 0, + ); + + let handle_message_send = + |msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| { + match msg { + InteractiveTxMessageSend::TxAddInput(msg) => for_constructor + .handle_tx_add_input(&msg) + .map(|msg_send| (Some(msg_send), None)), + InteractiveTxMessageSend::TxAddOutput(msg) => for_constructor + .handle_tx_add_output(&msg) + .map(|msg_send| (Some(msg_send), None)), + InteractiveTxMessageSend::TxComplete(msg) => { + for_constructor.handle_tx_complete(&msg).map(|value| match value { + HandleTxCompleteValue::SendTxMessage(msg_send) => { + (Some(msg_send), None) + }, + HandleTxCompleteValue::SendTxComplete(msg_send, tx) => { + (Some(msg_send), Some(tx)) + }, + HandleTxCompleteValue::NegotiationComplete(tx) => (None, Some(tx)), + }) + }, + } + }; + + assert!(first_message_b.is_none()); + let mut message_send_a = first_message_a; + let mut message_send_b = None; + let mut final_tx_a = None; + let mut final_tx_b = None; + while final_tx_a.is_none() || final_tx_b.is_none() { + if let Some(message_send_a) = message_send_a.take() { + match handle_message_send(message_send_a, &mut constructor_b) { + Ok((msg_send, final_tx)) => { + message_send_b = msg_send; + final_tx_b = final_tx; + }, + Err(abort_reason) => { + let error_culprit = match abort_reason { + AbortReason::ExceededNumberOfInputsOrOutputs => { + ErrorCulprit::Indeterminate + }, + _ => ErrorCulprit::NodeA, + }; + assert_eq!(Some((abort_reason, error_culprit)), session.expect_error); + assert!(message_send_b.is_none()); + return; + }, + } + } + if let Some(message_send_b) = message_send_b.take() { + match handle_message_send(message_send_b, &mut constructor_a) { + Ok((msg_send, final_tx)) => { + message_send_a = msg_send; + final_tx_a = final_tx; + }, + Err(abort_reason) => { + let error_culprit = match abort_reason { + AbortReason::ExceededNumberOfInputsOrOutputs => { + ErrorCulprit::Indeterminate + }, + _ => ErrorCulprit::NodeB, + }; + assert_eq!(Some((abort_reason, error_culprit)), session.expect_error); + assert!(message_send_a.is_none()); + return; + }, + } + } + } + assert!(message_send_a.is_none()); + assert!(message_send_b.is_none()); + assert_eq!(final_tx_a, final_tx_b); + assert!(session.expect_error.is_none()); + } + + fn generate_tx(values: &[u64]) -> Transaction { + generate_tx_with_locktime(values, 1337) + } + + fn generate_tx_with_locktime(values: &[u64], locktime: u32) -> Transaction { + Transaction { + version: 2, + lock_time: AbsoluteLockTime::from_height(locktime).unwrap(), + input: vec![TxIn { ..Default::default() }], + output: values + .iter() + .map(|value| TxOut { + value: *value, + script_pubkey: Builder::new() + .push_opcode(opcodes::OP_TRUE) + .into_script() + .to_v0_p2wsh(), + }) + .collect(), + } + } + + fn generate_inputs(values: &[u64]) -> Vec<(TxIn, TransactionU16LenLimited)> { + let tx = generate_tx(values); + let txid = tx.txid(); + tx.output + .iter() + .enumerate() + .map(|(idx, _)| { + let input = TxIn { + previous_output: OutPoint { txid, vout: idx as u32 }, + script_sig: Default::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Default::default(), + }; + (input, TransactionU16LenLimited::new(tx.clone()).unwrap()) + }) + .collect() + } + + fn generate_outputs(values: &[u64]) -> Vec { + values + .iter() + .map(|value| TxOut { + value: *value, + script_pubkey: Builder::new() + .push_opcode(opcodes::OP_TRUE) + .into_script() + .to_v0_p2wsh(), + }) + .collect() + } + + fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, TransactionU16LenLimited)> { + // Generate transactions with a total `count` number of outputs such that no transaction has a + // serialized length greater than u16::MAX. + let max_outputs_per_prevtx = 1_500; + let mut remaining = count; + let mut inputs: Vec<(TxIn, TransactionU16LenLimited)> = Vec::with_capacity(count as usize); + + while remaining > 0 { + let tx_output_count = remaining.min(max_outputs_per_prevtx); + remaining -= tx_output_count; + + // Use unique locktime for each tx so outpoints are different across transactions + let tx = generate_tx_with_locktime( + &vec![1_000_000; tx_output_count as usize], + (1337 + remaining).into(), + ); + let txid = tx.txid(); + + let mut temp: Vec<(TxIn, TransactionU16LenLimited)> = tx + .output + .iter() + .enumerate() + .map(|(idx, _)| { + let input = TxIn { + previous_output: OutPoint { txid, vout: idx as u32 }, + script_sig: Default::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Default::default(), + }; + (input, TransactionU16LenLimited::new(tx.clone()).unwrap()) + }) + .collect(); + + inputs.append(&mut temp); + } + + inputs + } + + fn generate_fixed_number_of_outputs(count: u16) -> Vec { + // Set a constant value for each TxOut + generate_outputs(&vec![1_000_000; count as usize]) + } + + fn generate_non_witness_output(value: u64) -> TxOut { + TxOut { + value, + script_pubkey: Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2sh(), + } + } + + #[test] + fn test_interactive_tx_constructor() { + // No contributions. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + }); + // Single contribution, no initiator inputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)), + }); + // Single contribution, no initiator outputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[1_000_000]), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: None, + }); + // Single contribution, insufficient fees. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[1_000_000]), + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)), + }); + // Initiator contributes sufficient fees, but non-initiator does not. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[1_000_000]), + outputs_a: vec![], + inputs_b: generate_inputs(&[100_000]), + outputs_b: generate_outputs(&[100_000]), + expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeB)), + }); + // Multi-input-output contributions from both sides. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[1_000_000, 1_000_000]), + outputs_a: generate_outputs(&[1_000_000, 200_000]), + inputs_b: generate_inputs(&[1_000_000, 500_000]), + outputs_b: generate_outputs(&[1_000_000, 400_000]), + expect_error: None, + }); + + // Prevout from initiator is not a witness program + let non_segwit_output_tx = { + let mut tx = generate_tx(&[1_000_000]); + tx.output.push(TxOut { + script_pubkey: Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .into_script() + .to_p2sh(), + ..Default::default() + }); + + TransactionU16LenLimited::new(tx).unwrap() + }; + let non_segwit_input = TxIn { + previous_output: OutPoint { + txid: non_segwit_output_tx.as_transaction().txid(), + vout: 1, + }, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![(non_segwit_input, non_segwit_output_tx)], + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), + }); + + // Invalid input sequence from initiator. + let tx = TransactionU16LenLimited::new(generate_tx(&[1_000_000])).unwrap(); + let invalid_sequence_input = TxIn { + previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![(invalid_sequence_input, tx.clone())], + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::IncorrectInputSequenceValue, ErrorCulprit::NodeA)), + }); + // Duplicate prevout from initiator. + let duplicate_input = TxIn { + previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![(duplicate_input.clone(), tx.clone()), (duplicate_input, tx.clone())], + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), + }); + // Non-initiator uses same prevout as initiator. + let duplicate_input = TxIn { + previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 }, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![(duplicate_input.clone(), tx.clone())], + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![(duplicate_input.clone(), tx.clone())], + outputs_b: vec![], + expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeB)), + }); + // Initiator sends too many TxAddInputs + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_fixed_number_of_inputs(MAX_RECEIVED_TX_ADD_INPUT_COUNT + 1), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::ReceivedTooManyTxAddInputs, ErrorCulprit::NodeA)), + }); + // Attempt to queue up two inputs with duplicate serial ids. We use a deliberately bad + // entropy source, `DuplicateEntropySource` to simulate this. + do_test_interactive_tx_constructor_with_entropy_source( + TestSession { + inputs_a: generate_fixed_number_of_inputs(2), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)), + }, + &DuplicateEntropySource, + ); + // Initiator sends too many TxAddOutputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_fixed_number_of_outputs(MAX_RECEIVED_TX_ADD_OUTPUT_COUNT + 1), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::ReceivedTooManyTxAddOutputs, ErrorCulprit::NodeA)), + }); + // Initiator sends an output below dust value. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_outputs(&[1]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::BelowDustLimit, ErrorCulprit::NodeA)), + }); + // Initiator sends an output above maximum sats allowed. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_outputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::ExceededMaximumSatsAllowed, ErrorCulprit::NodeA)), + }); + // Initiator sends an output without a witness program. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: vec![generate_non_witness_output(1_000_000)], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::InvalidOutputScript, ErrorCulprit::NodeA)), + }); + // Attempt to queue up two outputs with duplicate serial ids. We use a deliberately bad + // entropy source, `DuplicateEntropySource` to simulate this. + do_test_interactive_tx_constructor_with_entropy_source( + TestSession { + inputs_a: vec![], + outputs_a: generate_fixed_number_of_outputs(2), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)), + }, + &DuplicateEntropySource, + ); + + // Peer contributed more output value than inputs + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[100_000]), + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)), + }); + + // Peer contributed more than allowed number of inputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_fixed_number_of_inputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(( + AbortReason::ExceededNumberOfInputsOrOutputs, + ErrorCulprit::Indeterminate, + )), + }); + // Peer contributed more than allowed number of outputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS]), + outputs_a: generate_fixed_number_of_outputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(( + AbortReason::ExceededNumberOfInputsOrOutputs, + ErrorCulprit::Indeterminate, + )), + }); + } + + #[test] + fn test_generate_local_serial_id() { + let entropy_source = TestEntropySource(AtomicCounter::new()); + + // Initiators should have even serial id, non-initiators should have odd serial id. + assert_eq!(generate_holder_serial_id(&&entropy_source, true) % 2, 0); + assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) + } +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 71b73390d..25b32c4c7 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -82,6 +82,8 @@ mod async_signer_tests; #[cfg(test)] #[allow(unused_mut)] mod offers_tests; +#[allow(dead_code)] // TODO(dual_funding): Exchange for dual_funding cfg +pub(crate) mod interactivetxs; pub use self::peer_channel_encryptor::LN_MAX_MSG_LEN; diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index d291ac866..3a49a86b4 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -396,6 +396,10 @@ pub struct ChannelReady { pub short_channel_id_alias: Option, } +/// A randomly chosen number that is used to identify inputs within an interactive transaction +/// construction. +pub type SerialId = u64; + /// An stfu (quiescence) message to be sent by or received from the stfu initiator. // TODO(splicing): Add spec link for `stfu`; still in draft, using from https://github.com/lightning/bolts/pull/863 #[derive(Clone, Debug, PartialEq, Eq)] @@ -459,7 +463,7 @@ pub struct TxAddInput { pub channel_id: ChannelId, /// A randomly chosen unique identifier for this input, which is even for initiators and odd for /// non-initiators. - pub serial_id: u64, + pub serial_id: SerialId, /// Serialized transaction that contains the output this input spends to verify that it is non /// malleable. pub prevtx: TransactionU16LenLimited, @@ -478,7 +482,7 @@ pub struct TxAddOutput { pub channel_id: ChannelId, /// A randomly chosen unique identifier for this output, which is even for initiators and odd for /// non-initiators. - pub serial_id: u64, + pub serial_id: SerialId, /// The satoshi value of the output pub sats: u64, /// The scriptPubKey for the output @@ -493,7 +497,7 @@ pub struct TxRemoveInput { /// The channel ID pub channel_id: ChannelId, /// The serial ID of the input to be removed - pub serial_id: u64, + pub serial_id: SerialId, } /// A tx_remove_output message for removing an output during interactive transaction construction. @@ -504,7 +508,7 @@ pub struct TxRemoveOutput { /// The channel ID pub channel_id: ChannelId, /// The serial ID of the output to be removed - pub serial_id: u64, + pub serial_id: SerialId, } /// A tx_complete message signalling the conclusion of a peer's transaction contributions during diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 7447c92d4..0611e6ffb 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1408,6 +1408,11 @@ impl TransactionU16LenLimited { pub fn into_transaction(self) -> Transaction { self.0 } + + /// Returns a reference to the contained `Transaction` + pub fn as_transaction(&self) -> &Transaction { + &self.0 + } } impl Writeable for TransactionU16LenLimited {