Merge pull request #3129 from optout21/splicing-msgs-update
[rust-lightning] / lightning / src / ln / interactivetxs.rs
index 836258b5893be6e5651f22572b0a3a7ca787da23..b6ed64aa8182d855022c08ec881a42668c36f654 100644 (file)
@@ -9,23 +9,26 @@
 
 use crate::io_extras::sink;
 use crate::prelude::*;
-use core::ops::Deref;
 
+use bitcoin::absolute::LockTime as AbsoluteLockTime;
+use bitcoin::amount::Amount;
 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 bitcoin::transaction::Version;
+use bitcoin::{OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Weight};
 
 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;
 use crate::ln::msgs::SerialId;
-use crate::ln::{msgs, ChannelId};
-use crate::sign::EntropySource;
+use crate::ln::types::ChannelId;
+use crate::sign::{EntropySource, P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
 use crate::util::ser::TransactionU16LenLimited;
 
+use core::ops::Deref;
+
 /// 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;
@@ -38,6 +41,29 @@ const MAX_RECEIVED_TX_ADD_OUTPUT_COUNT: u16 = 4096;
 /// negotiation.
 const MAX_INPUTS_OUTPUTS_COUNT: usize = 252;
 
+/// The total weight of the common fields whose fee is paid by the initiator of the interactive
+/// transaction construction protocol.
+const TX_COMMON_FIELDS_WEIGHT: u64 = (4 /* version */ + 4 /* locktime */ + 1 /* input count */ +
+       1 /* output count */) * WITNESS_SCALE_FACTOR as u64 + 2 /* segwit marker + flag */;
+
+// BOLT 3 - Lower bounds for input weights
+
+/// Lower bound for P2WPKH input weight
+pub(crate) const P2WPKH_INPUT_WEIGHT_LOWER_BOUND: u64 =
+       BASE_INPUT_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT + P2WPKH_WITNESS_WEIGHT;
+
+/// Lower bound for P2WSH input weight is chosen as same as P2WPKH input weight in BOLT 3
+pub(crate) const P2WSH_INPUT_WEIGHT_LOWER_BOUND: u64 = P2WPKH_INPUT_WEIGHT_LOWER_BOUND;
+
+/// Lower bound for P2TR input weight is chosen as the key spend path.
+/// Not specified in BOLT 3, but a reasonable lower bound.
+pub(crate) const P2TR_INPUT_WEIGHT_LOWER_BOUND: u64 =
+       BASE_INPUT_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT + P2TR_KEY_PATH_WITNESS_WEIGHT;
+
+/// Lower bound for unknown segwit version input weight is chosen the same as P2WPKH in BOLT 3
+pub(crate) const UNKNOWN_SEGWIT_VERSION_INPUT_WEIGHT_LOWER_BOUND: u64 =
+       P2WPKH_INPUT_WEIGHT_LOWER_BOUND;
+
 trait SerialIdExt {
        fn is_for_initiator(&self) -> bool;
        fn is_for_non_initiator(&self) -> bool;
@@ -54,7 +80,7 @@ impl SerialIdExt for SerialId {
 }
 
 #[derive(Debug, Clone, PartialEq)]
-pub enum AbortReason {
+pub(crate) enum AbortReason {
        InvalidStateTransition,
        UnexpectedCounterpartyMessage,
        ReceivedTooManyTxAddInputs,
@@ -72,12 +98,86 @@ pub enum AbortReason {
        InsufficientFees,
        OutputsValueExceedsInputsValue,
        InvalidTx,
+       /// No funding (shared) output found.
+       MissingFundingOutput,
+       /// More than one funding (shared) output found.
+       DuplicateFundingOutput,
+       /// The intended local part of the funding output is higher than the actual shared funding output,
+       /// if funding output is provided by the peer this is an interop error,
+       /// if provided by the same node than internal input consistency error.
+       InvalidLowFundingOutputValue,
 }
 
-#[derive(Debug)]
-pub struct TxInputWithPrevOutput {
-       input: TxIn,
-       prev_output: TxOut,
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct ConstructedTransaction {
+       holder_is_initiator: bool,
+
+       inputs: Vec<InteractiveTxInput>,
+       outputs: Vec<InteractiveTxOutput>,
+
+       local_inputs_value_satoshis: u64,
+       local_outputs_value_satoshis: u64,
+
+       remote_inputs_value_satoshis: u64,
+       remote_outputs_value_satoshis: u64,
+
+       lock_time: AbsoluteLockTime,
+}
+
+impl ConstructedTransaction {
+       fn new(context: NegotiationContext) -> Self {
+               let local_inputs_value_satoshis = context
+                       .inputs
+                       .iter()
+                       .fold(0u64, |value, (_, input)| value.saturating_add(input.local_value()));
+
+               let local_outputs_value_satoshis = context
+                       .outputs
+                       .iter()
+                       .fold(0u64, |value, (_, output)| value.saturating_add(output.local_value()));
+
+               Self {
+                       holder_is_initiator: context.holder_is_initiator,
+
+                       local_inputs_value_satoshis,
+                       local_outputs_value_satoshis,
+
+                       remote_inputs_value_satoshis: context.remote_inputs_value(),
+                       remote_outputs_value_satoshis: context.remote_outputs_value(),
+
+                       inputs: context.inputs.into_values().collect(),
+                       outputs: context.outputs.into_values().collect(),
+
+                       lock_time: context.tx_locktime,
+               }
+       }
+
+       pub fn weight(&self) -> Weight {
+               let inputs_weight = self.inputs.iter().fold(Weight::from_wu(0), |weight, input| {
+                       weight.checked_add(estimate_input_weight(input.prev_output())).unwrap_or(Weight::MAX)
+               });
+               let outputs_weight = self.outputs.iter().fold(Weight::from_wu(0), |weight, output| {
+                       weight.checked_add(get_output_weight(&output.script_pubkey())).unwrap_or(Weight::MAX)
+               });
+               Weight::from_wu(TX_COMMON_FIELDS_WEIGHT)
+                       .checked_add(inputs_weight)
+                       .and_then(|weight| weight.checked_add(outputs_weight))
+                       .unwrap_or(Weight::MAX)
+       }
+
+       pub fn into_unsigned_tx(self) -> Transaction {
+               // Inputs and outputs must be sorted by serial_id
+               let ConstructedTransaction { mut inputs, mut outputs, .. } = self;
+
+               inputs.sort_unstable_by_key(|input| input.serial_id());
+               outputs.sort_unstable_by_key(|output| output.serial_id);
+
+               let input: Vec<TxIn> = inputs.into_iter().map(|input| input.txin().clone()).collect();
+               let output: Vec<TxOut> =
+                       outputs.into_iter().map(|output| output.tx_out().clone()).collect();
+
+               Transaction { version: Version::TWO, lock_time: self.lock_time, input, output }
+       }
 }
 
 #[derive(Debug)]
@@ -85,37 +185,121 @@ struct NegotiationContext {
        holder_is_initiator: bool,
        received_tx_add_input_count: u16,
        received_tx_add_output_count: u16,
-       inputs: HashMap<SerialId, TxInputWithPrevOutput>,
+       inputs: HashMap<SerialId, InteractiveTxInput>,
+       /// The output script intended to be the new funding output script.
+       /// The script pubkey is used to determine which output is the funding output.
+       /// When an output with the same script pubkey is added by any of the nodes, it will be
+       /// treated as the shared output.
+       /// The value is the holder's intended contribution to the shared funding output.
+       /// The rest is the counterparty's contribution.
+       /// When the funding output is added (recognized by its output script pubkey), it will be marked
+       /// as shared, and split between the peers according to the local value.
+       /// If the local value is found to be larger than the actual funding output, an error is generated.
+       expected_shared_funding_output: (ScriptBuf, u64),
+       /// The actual new funding output, set only after the output has actually been added.
+       /// NOTE: this output is also included in `outputs`.
+       actual_new_funding_output: Option<SharedOwnedOutput>,
        prevtx_outpoints: HashSet<OutPoint>,
-       outputs: HashMap<SerialId, TxOut>,
+       /// The outputs added so far.
+       outputs: HashMap<SerialId, InteractiveTxOutput>,
+       /// The locktime of the funding transaction.
        tx_locktime: AbsoluteLockTime,
+       /// The fee rate used for the transaction
        feerate_sat_per_kw: u32,
 }
 
+pub(crate) fn estimate_input_weight(prev_output: &TxOut) -> Weight {
+       Weight::from_wu(if prev_output.script_pubkey.is_p2wpkh() {
+               P2WPKH_INPUT_WEIGHT_LOWER_BOUND
+       } else if prev_output.script_pubkey.is_p2wsh() {
+               P2WSH_INPUT_WEIGHT_LOWER_BOUND
+       } else if prev_output.script_pubkey.is_p2tr() {
+               P2TR_INPUT_WEIGHT_LOWER_BOUND
+       } else {
+               UNKNOWN_SEGWIT_VERSION_INPUT_WEIGHT_LOWER_BOUND
+       })
+}
+
+pub(crate) fn get_output_weight(script_pubkey: &ScriptBuf) -> Weight {
+       Weight::from_wu(
+               (8 /* value */ + script_pubkey.consensus_encode(&mut sink()).unwrap() as u64)
+                       * WITNESS_SCALE_FACTOR as u64,
+       )
+}
+
+fn is_serial_id_valid_for_counterparty(holder_is_initiator: bool, serial_id: &SerialId) -> bool {
+       // A received `SerialId`'s parity must match the role of the counterparty.
+       holder_is_initiator == serial_id.is_for_non_initiator()
+}
+
 impl NegotiationContext {
+       fn new(
+               holder_is_initiator: bool, expected_shared_funding_output: (ScriptBuf, u64),
+               tx_locktime: AbsoluteLockTime, feerate_sat_per_kw: u32,
+       ) -> Self {
+               NegotiationContext {
+                       holder_is_initiator,
+                       received_tx_add_input_count: 0,
+                       received_tx_add_output_count: 0,
+                       inputs: new_hash_map(),
+                       expected_shared_funding_output,
+                       actual_new_funding_output: None,
+                       prevtx_outpoints: new_hash_set(),
+                       outputs: new_hash_map(),
+                       tx_locktime,
+                       feerate_sat_per_kw,
+               }
+       }
+
+       fn set_actual_new_funding_output(
+               &mut self, tx_out: TxOut,
+       ) -> Result<SharedOwnedOutput, AbortReason> {
+               if self.actual_new_funding_output.is_some() {
+                       return Err(AbortReason::DuplicateFundingOutput);
+               }
+               let value = tx_out.value.to_sat();
+               let local_owned = self.expected_shared_funding_output.1;
+               // Sanity check
+               if local_owned > value {
+                       return Err(AbortReason::InvalidLowFundingOutputValue);
+               }
+               let shared_output = SharedOwnedOutput::new(tx_out, local_owned);
+               self.actual_new_funding_output = Some(shared_output.clone());
+               Ok(shared_output)
+       }
+
        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()
+               is_serial_id_valid_for_counterparty(self.holder_is_initiator, serial_id)
        }
 
-       fn total_input_and_output_count(&self) -> usize {
-               self.inputs.len().saturating_add(self.outputs.len())
+       fn remote_inputs_value(&self) -> u64 {
+               self.inputs.iter().fold(0u64, |acc, (_, input)| acc.saturating_add(input.remote_value()))
        }
 
-       fn counterparty_inputs_contributed(
-               &self,
-       ) -> impl Iterator<Item = &TxInputWithPrevOutput> + 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 remote_outputs_value(&self) -> u64 {
+               self.outputs.iter().fold(0u64, |acc, (_, output)| acc.saturating_add(output.remote_value()))
        }
 
-       fn counterparty_outputs_contributed(&self) -> impl Iterator<Item = &TxOut> + Clone {
-               self.outputs
-                       .iter()
-                       .filter(move |(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id))
-                       .map(|(_, output)| output)
+       fn remote_inputs_weight(&self) -> Weight {
+               Weight::from_wu(
+                       self.inputs
+                               .iter()
+                               .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id))
+                               .fold(0u64, |weight, (_, input)| {
+                                       weight.saturating_add(estimate_input_weight(input.prev_output()).to_wu())
+                               }),
+               )
+       }
+
+       fn remote_outputs_weight(&self) -> Weight {
+               Weight::from_wu(
+                       self.outputs
+                               .iter()
+                               .filter(|(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id))
+                               .fold(0u64, |weight, (_, output)| {
+                                       weight.saturating_add(get_output_weight(&output.script_pubkey()).to_wu())
+                               }),
+               )
        }
 
        fn received_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> {
@@ -185,14 +369,15 @@ impl NegotiationContext {
                        },
                        hash_map::Entry::Vacant(entry) => {
                                let prev_outpoint = OutPoint { txid, vout: msg.prevtx_out };
-                               entry.insert(TxInputWithPrevOutput {
+                               entry.insert(InteractiveTxInput::Remote(LocalOrRemoteInput {
+                                       serial_id: msg.serial_id,
                                        input: TxIn {
                                                previous_output: prev_outpoint,
                                                sequence: Sequence(msg.sequence),
                                                ..Default::default()
                                        },
                                        prev_output: prev_out,
-                               });
+                               }));
                                self.prevtx_outpoints.insert(prev_outpoint);
                                Ok(())
                        },
@@ -241,7 +426,7 @@ impl NegotiationContext {
                // bitcoin supply.
                let mut outputs_value: u64 = 0;
                for output in self.outputs.iter() {
-                       outputs_value = outputs_value.saturating_add(output.1.value);
+                       outputs_value = outputs_value.saturating_add(output.1.value());
                }
                if outputs_value.saturating_add(msg.sats) > TOTAL_BITCOIN_SUPPLY_SATOSHIS {
                        // The receiving node:
@@ -262,14 +447,31 @@ impl NegotiationContext {
                //
                // TODO: The last check would be simplified when https://github.com/rust-bitcoin/rust-bitcoin/commit/1656e1a09a1959230e20af90d20789a4a8f0a31b
                // hits the next release of rust-bitcoin.
-               if !(msg.script.is_v0_p2wpkh()
-                       || msg.script.is_v0_p2wsh()
+               if !(msg.script.is_p2wpkh()
+                       || msg.script.is_p2wsh()
                        || (msg.script.is_witness_program()
                                && msg.script.witness_version().map(|v| v.to_num() >= 1).unwrap_or(false)))
                {
                        return Err(AbortReason::InvalidOutputScript);
                }
 
+               let txout = TxOut { value: Amount::from_sat(msg.sats), script_pubkey: msg.script.clone() };
+               let is_shared = msg.script == self.expected_shared_funding_output.0;
+               let output = if is_shared {
+                       // this is a shared funding output
+                       let shared_output = self.set_actual_new_funding_output(txout)?;
+                       InteractiveTxOutput {
+                               serial_id: msg.serial_id,
+                               added_by: AddingRole::Remote,
+                               output: OutputOwned::Shared(shared_output),
+                       }
+               } else {
+                       InteractiveTxOutput {
+                               serial_id: msg.serial_id,
+                               added_by: AddingRole::Remote,
+                               output: OutputOwned::Single(txout),
+                       }
+               };
                match self.outputs.entry(msg.serial_id) {
                        hash_map::Entry::Occupied(_) => {
                                // The receiving node:
@@ -278,7 +480,7 @@ impl NegotiationContext {
                                Err(AbortReason::DuplicateSerialId)
                        },
                        hash_map::Entry::Vacant(entry) => {
-                               entry.insert(TxOut { value: msg.sats, script_pubkey: msg.script.clone() });
+                               entry.insert(output);
                                Ok(())
                        },
                }
@@ -301,24 +503,45 @@ impl NegotiationContext {
 
        fn sent_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> {
                let tx = msg.prevtx.as_transaction();
-               let input = TxIn {
+               let txin = TxIn {
                        previous_output: OutPoint { txid: tx.txid(), vout: msg.prevtx_out },
                        sequence: Sequence(msg.sequence),
                        ..Default::default()
                };
-               let prev_output =
-                       tx.output.get(msg.prevtx_out as usize).ok_or(AbortReason::PrevTxOutInvalid)?.clone();
-               if !self.prevtx_outpoints.insert(input.previous_output) {
+               if !self.prevtx_outpoints.insert(txin.previous_output.clone()) {
                        // We have added an input that already exists
                        return Err(AbortReason::PrevTxOutInvalid);
                }
-               self.inputs.insert(msg.serial_id, TxInputWithPrevOutput { input, prev_output });
+               let vout = txin.previous_output.vout as usize;
+               let prev_output = tx.output.get(vout).ok_or(AbortReason::PrevTxOutInvalid)?.clone();
+               let input = InteractiveTxInput::Local(LocalOrRemoteInput {
+                       serial_id: msg.serial_id,
+                       input: txin,
+                       prev_output,
+               });
+               self.inputs.insert(msg.serial_id, input);
                Ok(())
        }
 
        fn sent_tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> Result<(), AbortReason> {
-               self.outputs
-                       .insert(msg.serial_id, TxOut { value: msg.sats, script_pubkey: msg.script.clone() });
+               let txout = TxOut { value: Amount::from_sat(msg.sats), script_pubkey: msg.script.clone() };
+               let is_shared = msg.script == self.expected_shared_funding_output.0;
+               let output = if is_shared {
+                       // this is a shared funding output
+                       let shared_output = self.set_actual_new_funding_output(txout)?;
+                       InteractiveTxOutput {
+                               serial_id: msg.serial_id,
+                               added_by: AddingRole::Local,
+                               output: OutputOwned::Shared(shared_output),
+                       }
+               } else {
+                       InteractiveTxOutput {
+                               serial_id: msg.serial_id,
+                               added_by: AddingRole::Local,
+                               output: OutputOwned::Single(txout),
+                       }
+               };
+               self.outputs.insert(msg.serial_id, output);
                Ok(())
        }
 
@@ -332,21 +555,37 @@ impl NegotiationContext {
                Ok(())
        }
 
-       fn build_transaction(self) -> Result<Transaction, AbortReason> {
+       fn check_counterparty_fees(
+               &self, counterparty_fees_contributed: u64,
+       ) -> Result<(), AbortReason> {
+               let counterparty_weight_contributed = self
+                       .remote_inputs_weight()
+                       .to_wu()
+                       .saturating_add(self.remote_outputs_weight().to_wu());
+               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_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);
+               }
+               Ok(())
+       }
+
+       fn validate_tx(self) -> Result<ConstructedTransaction, AbortReason> {
                // 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);
-               }
-               if counterparty_inputs_value < counterparty_outputs_value {
+               let remote_inputs_value = self.remote_inputs_value();
+               let remote_outputs_value = self.remote_outputs_value();
+               if remote_inputs_value < remote_outputs_value {
                        return Err(AbortReason::OutputsValueExceedsInputsValue);
                }
 
@@ -358,55 +597,20 @@ impl NegotiationContext {
                        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;
+               if self.actual_new_funding_output.is_none() {
+                       return Err(AbortReason::MissingFundingOutput);
+               }
 
                // - the peer's paid feerate does not meet or exceed the agreed feerate (based on the minimum fee).
-               let mut counterparty_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();
-               counterparty_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);
-               }
+               self.check_counterparty_fees(remote_inputs_value.saturating_sub(remote_outputs_value))?;
 
-               // Inputs and outputs must be sorted by serial_id
-               let mut inputs = self.inputs.into_iter().collect::<Vec<_>>();
-               let mut outputs = self.outputs.into_iter().collect::<Vec<_>>();
-               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 {
+               let constructed_tx = ConstructedTransaction::new(self);
+
+               if constructed_tx.weight().to_wu() > MAX_STANDARD_TX_WEIGHT as u64 {
                        return Err(AbortReason::TransactionTooLarge);
                }
 
-               Ok(tx_to_validate)
+               Ok(constructed_tx)
        }
 }
 
@@ -494,7 +698,7 @@ define_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!(NegotiationComplete, ConstructedTransaction, "We have exchanged consecutive `tx_complete` messages with the counterparty and the transaction negotiation is complete.");
 define_state!(
        NegotiationAborted,
        AbortReason,
@@ -536,7 +740,7 @@ macro_rules! define_state_transitions {
                impl StateTransition<NegotiationComplete, &msgs::TxComplete> for $tx_complete_state {
                        fn transition(self, _data: &msgs::TxComplete) -> StateTransitionResult<NegotiationComplete> {
                                let context = self.into_negotiation_context();
-                               let tx = context.build_transaction()?;
+                               let tx = context.validate_tx()?;
                                Ok(NegotiationComplete(tx))
                        }
                }
@@ -605,17 +809,16 @@ macro_rules! define_state_machine_transitions {
 }
 
 impl StateMachine {
-       fn new(feerate_sat_per_kw: u32, is_initiator: bool, tx_locktime: AbsoluteLockTime) -> Self {
-               let context = NegotiationContext {
+       fn new(
+               feerate_sat_per_kw: u32, is_initiator: bool, tx_locktime: AbsoluteLockTime,
+               expected_shared_funding_output: (ScriptBuf, u64),
+       ) -> Self {
+               let context = NegotiationContext::new(
+                       is_initiator,
+                       expected_shared_funding_output,
                        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,
-               };
+               );
                if is_initiator {
                        Self::ReceivedChangeMsg(ReceivedChangeMsg(context))
                } else {
@@ -674,14 +877,185 @@ impl StateMachine {
        ]);
 }
 
-pub struct InteractiveTxConstructor {
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum AddingRole {
+       Local,
+       Remote,
+}
+
+/// Represents an input -- local or remote (both have the same fields)
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct LocalOrRemoteInput {
+       serial_id: SerialId,
+       input: TxIn,
+       prev_output: TxOut,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum InteractiveTxInput {
+       Local(LocalOrRemoteInput),
+       Remote(LocalOrRemoteInput),
+       // TODO(splicing) SharedInput should be added
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SharedOwnedOutput {
+       tx_out: TxOut,
+       local_owned: u64,
+}
+
+impl SharedOwnedOutput {
+       fn new(tx_out: TxOut, local_owned: u64) -> SharedOwnedOutput {
+               debug_assert!(
+                       local_owned <= tx_out.value.to_sat(),
+                       "SharedOwnedOutput: Inconsistent local_owned value {}, larger than output value {}",
+                       local_owned,
+                       tx_out.value
+               );
+               SharedOwnedOutput { tx_out, local_owned }
+       }
+
+       fn remote_owned(&self) -> u64 {
+               self.tx_out.value.to_sat().saturating_sub(self.local_owned)
+       }
+}
+
+/// Represents an output, with information about
+/// its control -- exclusive by the adder or shared --, and
+/// its ownership -- value fully owned by the adder or jointly
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum OutputOwned {
+       /// Belongs to local node -- controlled exclusively and fully belonging to local node
+       Single(TxOut),
+       /// Output with shared control, but fully belonging to local node
+       SharedControlFullyOwned(TxOut),
+       /// Output with shared control and joint ownership
+       Shared(SharedOwnedOutput),
+}
+
+impl OutputOwned {
+       fn tx_out(&self) -> &TxOut {
+               match self {
+                       OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => tx_out,
+                       OutputOwned::Shared(output) => &output.tx_out,
+               }
+       }
+
+       fn value(&self) -> u64 {
+               self.tx_out().value.to_sat()
+       }
+
+       fn is_shared(&self) -> bool {
+               match self {
+                       OutputOwned::Single(_) => false,
+                       OutputOwned::SharedControlFullyOwned(_) => true,
+                       OutputOwned::Shared(_) => true,
+               }
+       }
+
+       fn local_value(&self, local_role: AddingRole) -> u64 {
+               match self {
+                       OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => {
+                               match local_role {
+                                       AddingRole::Local => tx_out.value.to_sat(),
+                                       AddingRole::Remote => 0,
+                               }
+                       },
+                       OutputOwned::Shared(output) => output.local_owned,
+               }
+       }
+
+       fn remote_value(&self, local_role: AddingRole) -> u64 {
+               match self {
+                       OutputOwned::Single(tx_out) | OutputOwned::SharedControlFullyOwned(tx_out) => {
+                               match local_role {
+                                       AddingRole::Local => 0,
+                                       AddingRole::Remote => tx_out.value.to_sat(),
+                               }
+                       },
+                       OutputOwned::Shared(output) => output.remote_owned(),
+               }
+       }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct InteractiveTxOutput {
+       serial_id: SerialId,
+       added_by: AddingRole,
+       output: OutputOwned,
+}
+
+impl InteractiveTxOutput {
+       fn tx_out(&self) -> &TxOut {
+               self.output.tx_out()
+       }
+
+       fn value(&self) -> u64 {
+               self.tx_out().value.to_sat()
+       }
+
+       fn local_value(&self) -> u64 {
+               self.output.local_value(self.added_by)
+       }
+
+       fn remote_value(&self) -> u64 {
+               self.output.remote_value(self.added_by)
+       }
+
+       fn script_pubkey(&self) -> &ScriptBuf {
+               &self.output.tx_out().script_pubkey
+       }
+}
+
+impl InteractiveTxInput {
+       pub fn serial_id(&self) -> SerialId {
+               match self {
+                       InteractiveTxInput::Local(input) => input.serial_id,
+                       InteractiveTxInput::Remote(input) => input.serial_id,
+               }
+       }
+
+       pub fn txin(&self) -> &TxIn {
+               match self {
+                       InteractiveTxInput::Local(input) => &input.input,
+                       InteractiveTxInput::Remote(input) => &input.input,
+               }
+       }
+
+       pub fn prev_output(&self) -> &TxOut {
+               match self {
+                       InteractiveTxInput::Local(input) => &input.prev_output,
+                       InteractiveTxInput::Remote(input) => &input.prev_output,
+               }
+       }
+
+       pub fn value(&self) -> u64 {
+               self.prev_output().value.to_sat()
+       }
+
+       pub fn local_value(&self) -> u64 {
+               match self {
+                       InteractiveTxInput::Local(input) => input.prev_output.value.to_sat(),
+                       InteractiveTxInput::Remote(_input) => 0,
+               }
+       }
+
+       pub fn remote_value(&self) -> u64 {
+               match self {
+                       InteractiveTxInput::Local(_input) => 0,
+                       InteractiveTxInput::Remote(input) => input.prev_output.value.to_sat(),
+               }
+       }
+}
+
+pub(crate) struct InteractiveTxConstructor {
        state_machine: StateMachine,
        channel_id: ChannelId,
        inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)>,
-       outputs_to_contribute: Vec<(SerialId, TxOut)>,
+       outputs_to_contribute: Vec<(SerialId, OutputOwned)>,
 }
 
-pub enum InteractiveTxMessageSend {
+pub(crate) enum InteractiveTxMessageSend {
        TxAddInput(msgs::TxAddInput),
        TxAddOutput(msgs::TxAddOutput),
        TxComplete(msgs::TxComplete),
@@ -713,66 +1087,113 @@ where
        serial_id
 }
 
-pub enum HandleTxCompleteValue {
+pub(crate) enum HandleTxCompleteValue {
        SendTxMessage(InteractiveTxMessageSend),
-       SendTxComplete(InteractiveTxMessageSend, Transaction),
-       NegotiationComplete(Transaction),
+       SendTxComplete(InteractiveTxMessageSend, ConstructedTransaction),
+       NegotiationComplete(ConstructedTransaction),
 }
 
 impl InteractiveTxConstructor {
        /// Instantiates a new `InteractiveTxConstructor`.
        ///
+       /// `expected_remote_shared_funding_output`: In the case when the local node doesn't
+       /// add a shared output, but it expects a shared output to be added by the remote node,
+       /// it has to specify the script pubkey, used to determine the shared output,
+       /// and its (local) contribution from the shared output:
+       ///   0 when the whole value belongs to the remote node, or
+       ///   positive if owned also by local.
+       /// Note: The local value cannot be larger that the actual shared output.
+       ///
        /// 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<ES: Deref>(
                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<TxOut>,
-       ) -> (Self, Option<InteractiveTxMessageSend>)
+               outputs_to_contribute: Vec<OutputOwned>,
+               expected_remote_shared_funding_output: Option<(ScriptBuf, u64)>,
+       ) -> Result<(Self, Option<InteractiveTxMessageSend>), AbortReason>
        where
                ES::Target: EntropySource,
        {
-               let state_machine =
-                       StateMachine::new(feerate_sat_per_kw, is_initiator, funding_tx_locktime);
-               let mut inputs_to_contribute: Vec<(SerialId, TxIn, TransactionU16LenLimited)> =
-                       inputs_to_contribute
+               // Sanity check: There can be at most one shared output, local-added or remote-added
+               let mut expected_shared_funding_output: Option<(ScriptBuf, u64)> = None;
+               for output in &outputs_to_contribute {
+                       let new_output = match output {
+                               OutputOwned::Single(_tx_out) => None,
+                               OutputOwned::SharedControlFullyOwned(tx_out) => {
+                                       Some((tx_out.script_pubkey.clone(), tx_out.value.to_sat()))
+                               },
+                               OutputOwned::Shared(output) => {
+                                       // Sanity check
+                                       if output.local_owned > output.tx_out.value.to_sat() {
+                                               return Err(AbortReason::InvalidLowFundingOutputValue);
+                                       }
+                                       Some((output.tx_out.script_pubkey.clone(), output.local_owned))
+                               },
+                       };
+                       if new_output.is_some() {
+                               if expected_shared_funding_output.is_some()
+                                       || expected_remote_shared_funding_output.is_some()
+                               {
+                                       // more than one local-added shared output or
+                                       // one local-added and one remote-expected shared output
+                                       return Err(AbortReason::DuplicateFundingOutput);
+                               }
+                               expected_shared_funding_output = new_output;
+                       }
+               }
+               if let Some(expected_remote_shared_funding_output) = expected_remote_shared_funding_output {
+                       expected_shared_funding_output = Some(expected_remote_shared_funding_output);
+               }
+               if let Some(expected_shared_funding_output) = expected_shared_funding_output {
+                       let state_machine = StateMachine::new(
+                               feerate_sat_per_kw,
+                               is_initiator,
+                               funding_tx_locktime,
+                               expected_shared_funding_output,
+                       );
+                       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<_> = outputs_to_contribute
                                .into_iter()
-                               .map(|(input, tx)| {
+                               .map(|output| {
                                        let serial_id = generate_holder_serial_id(entropy_source, is_initiator);
-                                       (serial_id, input, tx)
+                                       (serial_id, output)
                                })
                                .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
-                               },
-                       }
+                       // 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
+                       };
+                       Ok((constructor, message_send))
                } else {
-                       None
-               };
-               (constructor, message_send)
+                       Err(AbortReason::MissingFundingOutput)
+               }
        }
 
        fn maybe_send_message(&mut self) -> Result<InteractiveTxMessageSend, AbortReason> {
@@ -785,6 +1206,7 @@ impl InteractiveTxConstructor {
                                prevtx,
                                prevtx_out: input.previous_output.vout,
                                sequence: input.sequence.to_consensus_u32(),
+                               shared_input_txid: None,
                        };
                        do_state_transition!(self, sent_tx_add_input, &msg)?;
                        Ok(InteractiveTxMessageSend::TxAddInput(msg))
@@ -792,8 +1214,8 @@ impl InteractiveTxConstructor {
                        let msg = msgs::TxAddOutput {
                                channel_id: self.channel_id,
                                serial_id,
-                               sats: output.value,
-                               script: output.script_pubkey,
+                               sats: output.tx_out().value.to_sat(),
+                               script: output.tx_out().script_pubkey.clone(),
                        };
                        do_state_transition!(self, sent_tx_add_output, &msg)?;
                        Ok(InteractiveTxMessageSend::TxAddOutput(msg))
@@ -868,24 +1290,38 @@ impl InteractiveTxConstructor {
 
 #[cfg(test)]
 mod tests {
-       use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
+       use crate::chain::chaininterface::{fee_for_weight, 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::ln::types::ChannelId;
        use crate::sign::EntropySource;
        use crate::util::atomic_counter::AtomicCounter;
        use crate::util::ser::TransactionU16LenLimited;
+       use bitcoin::absolute::LockTime as AbsoluteLockTime;
+       use bitcoin::amount::Amount;
        use bitcoin::blockdata::opcodes;
        use bitcoin::blockdata::script::Builder;
+       use bitcoin::hashes::Hash;
+       use bitcoin::key::UntweakedPublicKey;
+       use bitcoin::secp256k1::{Keypair, Secp256k1};
+       use bitcoin::transaction::Version;
        use bitcoin::{
-               absolute::LockTime as AbsoluteLockTime, OutPoint, Sequence, Transaction, TxIn, TxOut,
+               OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash,
        };
        use core::ops::Deref;
 
+       use super::{
+               get_output_weight, AddingRole, OutputOwned, SharedOwnedOutput,
+               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;
+
        // A simple entropy source that works based on an atomic counter.
        struct TestEntropySource(AtomicCounter);
        impl EntropySource for TestEntropySource {
@@ -927,11 +1363,16 @@ mod tests {
        }
 
        struct TestSession {
+               description: &'static str,
                inputs_a: Vec<(TxIn, TransactionU16LenLimited)>,
-               outputs_a: Vec<TxOut>,
+               outputs_a: Vec<OutputOwned>,
                inputs_b: Vec<(TxIn, TransactionU16LenLimited)>,
-               outputs_b: Vec<TxOut>,
+               outputs_b: Vec<OutputOwned>,
                expect_error: Option<(AbortReason, ErrorCulprit)>,
+               /// A node adds no shared output, but expects the peer to add one, with the specific script pubkey, and local contribution
+               a_expected_remote_shared_output: Option<(ScriptBuf, u64)>,
+               /// B node adds no shared output, but expects the peer to add one, with the specific script pubkey, and local contribution
+               b_expected_remote_shared_output: Option<(ScriptBuf, u64)>,
        }
 
        fn do_test_interactive_tx_constructor(session: TestSession) {
@@ -955,24 +1396,103 @@ mod tests {
                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(
+               // funding output sanity check
+               let shared_outputs_by_a: Vec<_> =
+                       session.outputs_a.iter().filter(|o| o.is_shared()).collect();
+               if shared_outputs_by_a.len() > 1 {
+                       println!("Test warning: Expected at most one shared output. NodeA");
+               }
+               let shared_output_by_a = if shared_outputs_by_a.len() >= 1 {
+                       Some(shared_outputs_by_a[0].value())
+               } else {
+                       None
+               };
+               let shared_outputs_by_b: Vec<_> =
+                       session.outputs_b.iter().filter(|o| o.is_shared()).collect();
+               if shared_outputs_by_b.len() > 1 {
+                       println!("Test warning: Expected at most one shared output. NodeB");
+               }
+               let shared_output_by_b = if shared_outputs_by_b.len() >= 1 {
+                       Some(shared_outputs_by_b[0].value())
+               } else {
+                       None
+               };
+               if session.a_expected_remote_shared_output.is_some()
+                       || session.b_expected_remote_shared_output.is_some()
+               {
+                       let expected_by_a = if let Some(a_expected_remote_shared_output) =
+                               &session.a_expected_remote_shared_output
+                       {
+                               a_expected_remote_shared_output.1
+                       } else {
+                               if shared_outputs_by_a.len() >= 1 {
+                                       shared_outputs_by_a[0].local_value(AddingRole::Local)
+                               } else {
+                                       0
+                               }
+                       };
+                       let expected_by_b = if let Some(b_expected_remote_shared_output) =
+                               &session.b_expected_remote_shared_output
+                       {
+                               b_expected_remote_shared_output.1
+                       } else {
+                               if shared_outputs_by_b.len() >= 1 {
+                                       shared_outputs_by_b[0].local_value(AddingRole::Local)
+                               } else {
+                                       0
+                               }
+                       };
+
+                       let expected_sum = expected_by_a + expected_by_b;
+                       let actual_shared_output =
+                               shared_output_by_a.unwrap_or(shared_output_by_b.unwrap_or(0));
+                       if expected_sum != actual_shared_output {
+                               println!("Test warning: Sum of expected shared output values does not match actual shared output value, {} {}   {} {}   {} {}", expected_sum, actual_shared_output, expected_by_a, expected_by_b, shared_output_by_a.unwrap_or(0), shared_output_by_b.unwrap_or(0));
+                       }
+               }
+
+               let (mut constructor_a, first_message_a) = match InteractiveTxConstructor::new(
                        entropy_source,
                        channel_id,
-                       FEERATE_FLOOR_SATS_PER_KW * 10,
+                       TEST_FEERATE_SATS_PER_KW,
                        true,
                        tx_locktime,
                        session.inputs_a,
-                       session.outputs_a,
-               );
-               let (mut constructor_b, first_message_b) = InteractiveTxConstructor::new(
+                       session.outputs_a.iter().map(|o| o.clone()).collect(),
+                       session.a_expected_remote_shared_output,
+               ) {
+                       Ok(r) => r,
+                       Err(abort_reason) => {
+                               assert_eq!(
+                                       Some((abort_reason, ErrorCulprit::NodeA)),
+                                       session.expect_error,
+                                       "Test: {}",
+                                       session.description
+                               );
+                               return;
+                       },
+               };
+               let (mut constructor_b, first_message_b) = match InteractiveTxConstructor::new(
                        entropy_source,
                        channel_id,
-                       FEERATE_FLOOR_SATS_PER_KW * 10,
+                       TEST_FEERATE_SATS_PER_KW,
                        false,
                        tx_locktime,
                        session.inputs_b,
-                       session.outputs_b,
-               );
+                       session.outputs_b.iter().map(|o| o.clone()).collect(),
+                       session.b_expected_remote_shared_output,
+               ) {
+                       Ok(r) => r,
+                       Err(abort_reason) => {
+                               assert_eq!(
+                                       Some((abort_reason, ErrorCulprit::NodeB)),
+                                       session.expect_error,
+                                       "Test: {}",
+                                       session.description
+                               );
+                               return;
+                       },
+               };
 
                let handle_message_send =
                        |msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| {
@@ -1016,8 +1536,13 @@ mod tests {
                                                        },
                                                        _ => ErrorCulprit::NodeA,
                                                };
-                                               assert_eq!(Some((abort_reason, error_culprit)), session.expect_error);
-                                               assert!(message_send_b.is_none());
+                                               assert_eq!(
+                                                       Some((abort_reason, error_culprit)),
+                                                       session.expect_error,
+                                                       "Test: {}",
+                                                       session.description
+                                               );
+                                               assert!(message_send_b.is_none(), "Test: {}", session.description);
                                                return;
                                        },
                                }
@@ -1035,8 +1560,13 @@ mod tests {
                                                        },
                                                        _ => ErrorCulprit::NodeB,
                                                };
-                                               assert_eq!(Some((abort_reason, error_culprit)), session.expect_error);
-                                               assert!(message_send_a.is_none());
+                                               assert_eq!(
+                                                       Some((abort_reason, error_culprit)),
+                                                       session.expect_error,
+                                                       "Test: {}",
+                                                       session.description
+                                               );
+                                               assert!(message_send_a.is_none(), "Test: {}", session.description);
                                                return;
                                        },
                                }
@@ -1044,34 +1574,64 @@ mod tests {
                }
                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());
+               assert_eq!(final_tx_a.unwrap().into_unsigned_tx(), final_tx_b.unwrap().into_unsigned_tx());
+               assert!(
+                       session.expect_error.is_none(),
+                       "Missing expected error {:?}, Test: {}",
+                       session.expect_error,
+                       session.description,
+               );
+       }
+
+       #[derive(Debug, Clone, Copy)]
+       enum TestOutput {
+               P2WPKH(u64),
+               /// P2WSH, but with the specific script used for the funding output
+               P2WSH(u64),
+               P2TR(u64),
+               // Non-witness type to test rejection.
+               P2PKH(u64),
        }
 
-       fn generate_tx(values: &[u64]) -> Transaction {
-               generate_tx_with_locktime(values, 1337)
+       fn generate_tx(outputs: &[TestOutput]) -> Transaction {
+               generate_tx_with_locktime(outputs, 1337)
        }
 
-       fn generate_tx_with_locktime(values: &[u64], locktime: u32) -> Transaction {
+       fn generate_txout(output: &TestOutput) -> TxOut {
+               let secp_ctx = Secp256k1::new();
+               let (value, script_pubkey) = match output {
+                       TestOutput::P2WPKH(value) => (*value, generate_p2wpkh_script_pubkey()),
+                       TestOutput::P2WSH(value) => (*value, generate_funding_script_pubkey()),
+                       TestOutput::P2TR(value) => (
+                               *value,
+                               ScriptBuf::new_p2tr(
+                                       &secp_ctx,
+                                       UntweakedPublicKey::from_keypair(
+                                               &Keypair::from_seckey_slice(&secp_ctx, &[3; 32]).unwrap(),
+                                       )
+                                       .0,
+                                       None,
+                               ),
+                       ),
+                       TestOutput::P2PKH(value) => {
+                               (*value, ScriptBuf::new_p2pkh(&PubkeyHash::from_slice(&[4; 20]).unwrap()))
+                       },
+               };
+
+               TxOut { value: Amount::from_sat(value), script_pubkey }
+       }
+
+       fn generate_tx_with_locktime(outputs: &[TestOutput], locktime: u32) -> Transaction {
                Transaction {
-                       version: 2,
+                       version: Version::TWO,
                        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(),
+                       output: outputs.iter().map(generate_txout).collect(),
                }
        }
 
-       fn generate_inputs(values: &[u64]) -> Vec<(TxIn, TransactionU16LenLimited)> {
-               let tx = generate_tx(values);
+       fn generate_inputs(outputs: &[TestOutput]) -> Vec<(TxIn, TransactionU16LenLimited)> {
+               let tx = generate_tx(outputs);
                let txid = tx.txid();
                tx.output
                        .iter()
@@ -1088,17 +1648,47 @@ mod tests {
                        .collect()
        }
 
-       fn generate_outputs(values: &[u64]) -> Vec<TxOut> {
-               values
-                       .iter()
-                       .map(|value| TxOut {
-                               value: *value,
-                               script_pubkey: Builder::new()
-                                       .push_opcode(opcodes::OP_TRUE)
-                                       .into_script()
-                                       .to_v0_p2wsh(),
-                       })
-                       .collect()
+       fn generate_p2wsh_script_pubkey() -> ScriptBuf {
+               Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2wsh()
+       }
+
+       fn generate_p2wpkh_script_pubkey() -> ScriptBuf {
+               ScriptBuf::new_p2wpkh(&WPubkeyHash::from_slice(&[1; 20]).unwrap())
+       }
+
+       fn generate_funding_script_pubkey() -> ScriptBuf {
+               Builder::new().push_int(33).into_script().to_p2wsh()
+       }
+
+       fn generate_output_nonfunding_one(output: &TestOutput) -> OutputOwned {
+               OutputOwned::Single(generate_txout(output))
+       }
+
+       fn generate_outputs(outputs: &[TestOutput]) -> Vec<OutputOwned> {
+               outputs.iter().map(|o| generate_output_nonfunding_one(o)).collect()
+       }
+
+       /// Generate a single output that is the funding output
+       fn generate_output(output: &TestOutput) -> Vec<OutputOwned> {
+               vec![OutputOwned::SharedControlFullyOwned(generate_txout(output))]
+       }
+
+       /// Generate a single P2WSH output that is the funding output
+       fn generate_funding_output(value: u64) -> Vec<OutputOwned> {
+               generate_output(&TestOutput::P2WSH(value))
+       }
+
+       /// Generate a single P2WSH output with shared contribution that is the funding output
+       fn generate_shared_funding_output_one(value: u64, local_value: u64) -> OutputOwned {
+               OutputOwned::Shared(SharedOwnedOutput {
+                       tx_out: generate_txout(&TestOutput::P2WSH(value)),
+                       local_owned: local_value,
+               })
+       }
+
+       /// Generate a single P2WSH output with shared contribution that is the funding output
+       fn generate_shared_funding_output(value: u64, local_value: u64) -> Vec<OutputOwned> {
+               vec![generate_shared_funding_output_one(value, local_value)]
        }
 
        fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, TransactionU16LenLimited)> {
@@ -1114,7 +1704,7 @@ mod tests {
 
                        // 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],
+                               &vec![TestOutput::P2WPKH(1_000_000); tx_output_count as usize],
                                (1337 + remaining).into(),
                        );
                        let txid = tx.txid();
@@ -1140,123 +1730,214 @@ mod tests {
                inputs
        }
 
-       fn generate_fixed_number_of_outputs(count: u16) -> Vec<TxOut> {
+       fn generate_fixed_number_of_outputs(count: u16) -> Vec<OutputOwned> {
                // Set a constant value for each TxOut
-               generate_outputs(&vec![1_000_000; count as usize])
+               generate_outputs(&vec![TestOutput::P2WPKH(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(),
-               }
+       fn generate_p2sh_script_pubkey() -> ScriptBuf {
+               Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2sh()
+       }
+
+       fn generate_non_witness_output(value: u64) -> OutputOwned {
+               OutputOwned::Single(TxOut {
+                       value: Amount::from_sat(value),
+                       script_pubkey: generate_p2sh_script_pubkey(),
+               })
        }
 
        #[test]
        fn test_interactive_tx_constructor() {
-               // No contributions.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "No contributions",
                        inputs_a: vec![],
                        outputs_a: vec![],
                        inputs_b: vec![],
                        outputs_b: vec![],
-                       expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)),
+                       expect_error: Some((AbortReason::MissingFundingOutput, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: None,
                });
-               // Single contribution, no initiator inputs.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution, no initiator inputs",
                        inputs_a: vec![],
-                       outputs_a: generate_outputs(&[1_000_000]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
-               // Single contribution, no initiator outputs.
                do_test_interactive_tx_constructor(TestSession {
-                       inputs_a: generate_inputs(&[1_000_000]),
+                       description: "Single contribution, no initiator outputs",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
                        outputs_a: vec![],
                        inputs_b: vec![],
                        outputs_b: vec![],
+                       expect_error: Some((AbortReason::MissingFundingOutput, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: None,
+               });
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution, no fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+               });
+               let p2wpkh_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2WPKH_INPUT_WEIGHT_LOWER_BOUND);
+               let outputs_fee = fee_for_weight(
+                       TEST_FEERATE_SATS_PER_KW,
+                       get_output_weight(&generate_p2wpkh_script_pubkey()).to_wu(),
+               );
+               let tx_common_fields_fee =
+                       fee_for_weight(TEST_FEERATE_SATS_PER_KW, TX_COMMON_FIELDS_WEIGHT);
+
+               let amount_adjusted_with_p2wpkh_fee =
+                       1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee;
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution, with P2WPKH input, insufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(
+                               amount_adjusted_with_p2wpkh_fee + 1, /* makes fees insuffcient for initiator */
+                       )),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+               });
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution with P2WPKH input, sufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2wpkh_fee)),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
+                       expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+               });
+               let p2wsh_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2WSH_INPUT_WEIGHT_LOWER_BOUND);
+               let amount_adjusted_with_p2wsh_fee =
+                       1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee;
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution, with P2WSH input, insufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2WSH(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(
+                               amount_adjusted_with_p2wsh_fee + 1, /* makes fees insuffcient for initiator */
+                       )),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+               });
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Single contribution with P2WSH input, sufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2WSH(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2wsh_fee)),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
                        expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
-               // Single contribution, insufficient fees.
+               let p2tr_fee = fee_for_weight(TEST_FEERATE_SATS_PER_KW, P2TR_INPUT_WEIGHT_LOWER_BOUND);
+               let amount_adjusted_with_p2tr_fee =
+                       1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee;
                do_test_interactive_tx_constructor(TestSession {
-                       inputs_a: generate_inputs(&[1_000_000]),
-                       outputs_a: generate_outputs(&[1_000_000]),
+                       description: "Single contribution, with P2TR input, insufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(
+                               amount_adjusted_with_p2tr_fee + 1, /* makes fees insuffcient for initiator */
+                       )),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
-               // Initiator contributes sufficient fees, but non-initiator does not.
                do_test_interactive_tx_constructor(TestSession {
-                       inputs_a: generate_inputs(&[1_000_000]),
+                       description: "Single contribution with P2TR input, sufficient fees",
+                       inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(amount_adjusted_with_p2tr_fee)),
+                       inputs_b: vec![],
+                       outputs_b: vec![],
+                       expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+               });
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator contributes sufficient fees, but non-initiator does not",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
                        outputs_a: vec![],
-                       inputs_b: generate_inputs(&[100_000]),
-                       outputs_b: generate_outputs(&[100_000]),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(100_000)]),
+                       outputs_b: generate_output(&TestOutput::P2WPKH(100_000)),
                        expect_error: Some((AbortReason::InsufficientFees, ErrorCulprit::NodeB)),
+                       a_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: None,
                });
-               // 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]),
+                       description: "Multi-input-output contributions from both sides",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000); 2]),
+                       outputs_a: vec![
+                               generate_shared_funding_output_one(1_000_000, 200_000),
+                               generate_output_nonfunding_one(&TestOutput::P2WPKH(200_000)),
+                       ],
+                       inputs_b: generate_inputs(&[
+                               TestOutput::P2WPKH(1_000_000),
+                               TestOutput::P2WPKH(500_000),
+                       ]),
+                       outputs_b: vec![generate_output_nonfunding_one(&TestOutput::P2WPKH(400_000))],
                        expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 800_000)),
                });
 
-               // 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)],
+                       description: "Prevout from initiator is not a witness program",
+                       inputs_a: generate_inputs(&[TestOutput::P2PKH(1_000_000)]),
                        outputs_a: vec![],
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
 
-               // Invalid input sequence from initiator.
-               let tx = TransactionU16LenLimited::new(generate_tx(&[1_000_000])).unwrap();
+               let tx =
+                       TransactionU16LenLimited::new(generate_tx(&[TestOutput::P2WPKH(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 {
+                       description: "Invalid input sequence from initiator",
                        inputs_a: vec![(invalid_sequence_input, tx.clone())],
-                       outputs_a: generate_outputs(&[1_000_000]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::IncorrectInputSequenceValue, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
-               // 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 {
+                       description: "Duplicate prevout from initiator",
                        inputs_a: vec![(duplicate_input.clone(), tx.clone()), (duplicate_input, tx.clone())],
-                       outputs_a: generate_outputs(&[1_000_000]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeB)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
                // Non-initiator uses same prevout as initiator.
                let duplicate_input = TxIn {
@@ -1265,88 +1946,124 @@ mod tests {
                        ..Default::default()
                };
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Non-initiator uses same prevout as initiator",
+                       inputs_a: vec![(duplicate_input.clone(), tx.clone())],
+                       outputs_a: generate_shared_funding_output(1_000_000, 905_000),
+                       inputs_b: vec![(duplicate_input.clone(), tx.clone())],
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 95_000)),
+               });
+               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 {
+                       description: "Non-initiator uses same prevout as initiator",
                        inputs_a: vec![(duplicate_input.clone(), tx.clone())],
-                       outputs_a: generate_outputs(&[1_000_000]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
                        inputs_b: vec![(duplicate_input.clone(), tx.clone())],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_p2wpkh_script_pubkey(), 0)),
                });
-               // Initiator sends too many TxAddInputs
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator sends too many TxAddInputs",
                        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)),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // 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 {
+                               // We use a deliberately bad entropy source, `DuplicateEntropySource` to simulate this.
+                               description: "Attempt to queue up two inputs with duplicate serial ids",
                                inputs_a: generate_fixed_number_of_inputs(2),
                                outputs_a: vec![],
                                inputs_b: vec![],
                                outputs_b: vec![],
                                expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)),
+                               a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                               b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                        },
                        &DuplicateEntropySource,
                );
-               // Initiator sends too many TxAddOutputs.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator sends too many TxAddOutputs",
                        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)),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // Initiator sends an output below dust value.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator sends an output below dust value",
                        inputs_a: vec![],
-                       outputs_a: generate_outputs(&[1]),
+                       outputs_a: generate_funding_output(
+                               generate_p2wsh_script_pubkey().dust_value().to_sat() - 1,
+                       ),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::BelowDustLimit, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // Initiator sends an output above maximum sats allowed.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator sends an output above maximum sats allowed",
                        inputs_a: vec![],
-                       outputs_a: generate_outputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1)),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::ExceededMaximumSatsAllowed, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // Initiator sends an output without a witness program.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Initiator sends an output without a witness program",
                        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)),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // 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 {
+                               // We use a deliberately bad entropy source, `DuplicateEntropySource` to simulate this.
+                               description: "Attempt to queue up two outputs with duplicate serial ids",
                                inputs_a: vec![],
                                outputs_a: generate_fixed_number_of_outputs(2),
                                inputs_b: vec![],
                                outputs_b: vec![],
                                expect_error: Some((AbortReason::DuplicateSerialId, ErrorCulprit::NodeA)),
+                               a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                               b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                        },
                        &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]),
+                       description: "Peer contributed more output value than inputs",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000)]),
+                       outputs_a: generate_output(&TestOutput::P2WPKH(1_000_000)),
                        inputs_b: vec![],
                        outputs_b: vec![],
                        expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
 
-               // Peer contributed more than allowed number of inputs.
                do_test_interactive_tx_constructor(TestSession {
+                       description: "Peer contributed more than allowed number of inputs",
                        inputs_a: generate_fixed_number_of_inputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1),
                        outputs_a: vec![],
                        inputs_b: vec![],
@@ -1355,10 +2072,12 @@ mod tests {
                                AbortReason::ExceededNumberOfInputsOrOutputs,
                                ErrorCulprit::Indeterminate,
                        )),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
                });
-               // Peer contributed more than allowed number of outputs.
                do_test_interactive_tx_constructor(TestSession {
-                       inputs_a: generate_inputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS]),
+                       description: "Peer contributed more than allowed number of outputs",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(TOTAL_BITCOIN_SUPPLY_SATOSHIS)]),
                        outputs_a: generate_fixed_number_of_outputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1),
                        inputs_b: vec![],
                        outputs_b: vec![],
@@ -1366,6 +2085,121 @@ mod tests {
                                AbortReason::ExceededNumberOfInputsOrOutputs,
                                ErrorCulprit::Indeterminate,
                        )),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+               });
+
+               // Adding multiple outputs to the funding output pubkey is an error
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Adding two outputs to the funding output pubkey",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000)]),
+                       outputs_a: generate_funding_output(100_000),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(1_001_000)]),
+                       outputs_b: generate_funding_output(100_000),
+                       expect_error: Some((AbortReason::DuplicateFundingOutput, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: None,
+               });
+
+               // We add the funding output, but we contribute a little
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Funding output by us, small contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(12_000)]),
+                       outputs_a: generate_shared_funding_output(1_000_000, 10_000),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(992_000)]),
+                       outputs_b: vec![],
+                       expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 990_000)),
+               });
+
+               // They add the funding output, and we contribute a little
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Funding output by them, small contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(12_000)]),
+                       outputs_a: vec![],
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(992_000)]),
+                       outputs_b: generate_shared_funding_output(1_000_000, 990_000),
+                       expect_error: None,
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 10_000)),
+                       b_expected_remote_shared_output: None,
+               });
+
+               // We add the funding output, and we contribute most
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Funding output by us, large contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(992_000)]),
+                       outputs_a: generate_shared_funding_output(1_000_000, 990_000),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(12_000)]),
+                       outputs_b: vec![],
+                       expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 10_000)),
+               });
+
+               // They add the funding output, but we contribute most
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Funding output by them, large contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(992_000)]),
+                       outputs_a: vec![],
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(12_000)]),
+                       outputs_b: generate_shared_funding_output(1_000_000, 10_000),
+                       expect_error: None,
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 990_000)),
+                       b_expected_remote_shared_output: None,
+               });
+
+               // During a splice-out, with peer providing more output value than input value
+               // but still pays enough fees due to their to_remote_value_satoshis portion in
+               // the shared input.
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Splice out with sufficient initiator balance",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(50_000)]),
+                       outputs_a: generate_funding_output(120_000),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(50_000)]),
+                       outputs_b: vec![],
+                       expect_error: None,
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+               });
+
+               // During a splice-out, with peer providing more output value than input value
+               // and the to_remote_value_satoshis portion in
+               // the shared input cannot cover fees
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Splice out with insufficient initiator balance",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]),
+                       outputs_a: generate_funding_output(120_000),
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]),
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::OutputsValueExceedsInputsValue, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 0)),
+               });
+
+               // The actual funding output value is lower than the intended local contribution by the same node
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Splice in, invalid intended local contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]),
+                       outputs_a: generate_shared_funding_output(100_000, 120_000), // local value is higher than the output value
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]),
+                       outputs_b: vec![],
+                       expect_error: Some((AbortReason::InvalidLowFundingOutputValue, ErrorCulprit::NodeA)),
+                       a_expected_remote_shared_output: None,
+                       b_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 20_000)),
+               });
+
+               // The actual funding output value is lower than the intended local contribution of the other node
+               do_test_interactive_tx_constructor(TestSession {
+                       description: "Splice in, invalid intended local contribution",
+                       inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000), TestOutput::P2WPKH(15_000)]),
+                       outputs_a: vec![],
+                       inputs_b: generate_inputs(&[TestOutput::P2WPKH(85_000)]),
+                       outputs_b: generate_funding_output(100_000),
+                       // The error is caused by NodeA, it occurs when nodeA prepares the message to be sent to NodeB, that's why here it shows up as NodeB
+                       expect_error: Some((AbortReason::InvalidLowFundingOutputValue, ErrorCulprit::NodeB)),
+                       a_expected_remote_shared_output: Some((generate_funding_script_pubkey(), 120_000)), // this is higher than the actual output value
+                       b_expected_remote_shared_output: None,
                });
        }