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, ScriptBuf, 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::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;
}
#[derive(Debug, Clone, PartialEq)]
-pub enum AbortReason {
+pub(crate) enum AbortReason {
InvalidStateTransition,
UnexpectedCounterpartyMessage,
ReceivedTooManyTxAddInputs,
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)]
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 get_output_weight(script_pubkey: &ScriptBuf) -> u64 {
- (8 /* value */ + script_pubkey.consensus_encode(&mut sink()).unwrap() as u64)
- * WITNESS_SCALE_FACTOR as u64
+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> {
},
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(())
},
// 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:
//
// 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:
Err(AbortReason::DuplicateSerialId)
},
hash_map::Entry::Vacant(entry) => {
- entry.insert(TxOut { value: msg.sats, script_pubkey: msg.script.clone() });
+ entry.insert(output);
Ok(())
},
}
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(())
}
}
fn check_counterparty_fees(
- &self, counterparty_inputs_value: u64, counterparty_outputs_value: u64,
+ &self, counterparty_fees_contributed: u64,
) -> Result<(), AbortReason> {
- let mut counterparty_weight_contributed: u64 = self
- .counterparty_outputs_contributed()
- .map(|output| get_output_weight(&output.script_pubkey))
- .sum();
- // We don't know the counterparty's witnesses ahead of time obviously, so we use the lower bounds
- // specified in BOLT 3.
- let mut total_inputs_weight: u64 = 0;
- for TxInputWithPrevOutput { prev_output, .. } in self.counterparty_inputs_contributed() {
- total_inputs_weight =
- total_inputs_weight.saturating_add(if prev_output.script_pubkey.is_v0_p2wpkh() {
- P2WPKH_INPUT_WEIGHT_LOWER_BOUND
- } else if prev_output.script_pubkey.is_v0_p2wsh() {
- P2WSH_INPUT_WEIGHT_LOWER_BOUND
- } else if prev_output.script_pubkey.is_v1_p2tr() {
- P2TR_INPUT_WEIGHT_LOWER_BOUND
- } else {
- UNKNOWN_SEGWIT_VERSION_INPUT_WEIGHT_LOWER_BOUND
- });
- }
- counterparty_weight_contributed =
- counterparty_weight_contributed.saturating_add(total_inputs_weight);
- let counterparty_fees_contributed =
- counterparty_inputs_value.saturating_sub(counterparty_outputs_value);
+ 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 {
Ok(())
}
- fn build_transaction(self) -> Result<Transaction, AbortReason> {
+ 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);
}
return Err(AbortReason::ExceededNumberOfInputsOrOutputs);
}
+ 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).
- self.check_counterparty_fees(counterparty_inputs_value, counterparty_outputs_value)?;
+ 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)
}
}
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,
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))
}
}
}
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 {
]);
}
-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),
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> {
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))
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::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 bitcoin::{PubkeyHash, ScriptBuf, WPubkeyHash, WScriptHash};
use core::ops::Deref;
use super::{
- get_output_weight, P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND,
+ 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,
};
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) {
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,
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,
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| {
"Test: {}",
session.description
);
- assert!(message_send_b.is_none());
+ assert!(message_send_b.is_none(), "Test: {}", session.description);
return;
},
}
"Test: {}",
session.description
);
- assert!(message_send_a.is_none());
+ assert!(message_send_a.is_none(), "Test: {}", session.description);
return;
},
}
}
assert!(message_send_a.is_none());
assert!(message_send_b.is_none());
- assert_eq!(final_tx_a, final_tx_b);
- assert!(session.expect_error.is_none(), "Test: {}", session.description);
+ 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.
fn generate_txout(output: &TestOutput) -> TxOut {
let secp_ctx = Secp256k1::new();
let (value, script_pubkey) = match output {
- TestOutput::P2WPKH(value) => {
- (*value, ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::from_slice(&[1; 20]).unwrap()))
- },
- TestOutput::P2WSH(value) => {
- (*value, ScriptBuf::new_v0_p2wsh(&WScriptHash::from_slice(&[2; 32]).unwrap()))
- },
+ TestOutput::P2WPKH(value) => (*value, generate_p2wpkh_script_pubkey()),
+ TestOutput::P2WSH(value) => (*value, generate_funding_script_pubkey()),
TestOutput::P2TR(value) => (
*value,
- ScriptBuf::new_v1_p2tr(
+ ScriptBuf::new_p2tr(
&secp_ctx,
UntweakedPublicKey::from_keypair(
- &KeyPair::from_seckey_slice(&secp_ctx, &[3; 32]).unwrap(),
+ &Keypair::from_seckey_slice(&secp_ctx, &[3; 32]).unwrap(),
)
.0,
None,
},
};
- TxOut { value, script_pubkey }
+ 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: outputs.iter().map(generate_txout).collect(),
}
fn generate_p2wsh_script_pubkey() -> ScriptBuf {
- Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_v0_p2wsh()
+ Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2wsh()
}
fn generate_p2wpkh_script_pubkey() -> ScriptBuf {
- ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::from_slice(&[1; 20]).unwrap())
+ 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))]
}
- fn generate_outputs(outputs: &[TestOutput]) -> Vec<TxOut> {
- outputs.iter().map(generate_txout).collect()
+ /// 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)> {
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![TestOutput::P2WPKH(1_000_000); count as usize])
}
Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2sh()
}
- fn generate_non_witness_output(value: u64) -> TxOut {
- TxOut { value, script_pubkey: generate_p2sh_script_pubkey() }
+ fn generate_non_witness_output(value: u64) -> OutputOwned {
+ OutputOwned::Single(TxOut {
+ value: Amount::from_sat(value),
+ script_pubkey: generate_p2sh_script_pubkey(),
+ })
}
#[test]
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,
});
do_test_interactive_tx_constructor(TestSession {
description: "Single contribution, no initiator inputs",
inputs_a: vec![],
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Single contribution, no initiator outputs",
outputs_a: vec![],
inputs_b: vec![],
outputs_b: vec![],
- expect_error: None,
+ 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_outputs(&[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()),
+ 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_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */
- )]),
+ 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_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2wpkh_fee - outputs_fee - tx_common_fields_fee,
- )]),
+ 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_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */
- )]),
+ 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_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2wsh_fee - outputs_fee - tx_common_fields_fee,
- )]),
+ 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)),
});
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 {
description: "Single contribution, with P2TR input, insufficient fees",
inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]),
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee + 1, /* makes fees insuffcient for initiator */
- )]),
+ 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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Single contribution with P2TR input, sufficient fees",
inputs_a: generate_inputs(&[TestOutput::P2TR(1_000_000)]),
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(
- 1_000_000 - p2tr_fee - outputs_fee - tx_common_fields_fee,
- )]),
+ 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(&[TestOutput::P2WPKH(100_000)]),
- outputs_b: generate_outputs(&[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,
});
do_test_interactive_tx_constructor(TestSession {
description: "Multi-input-output contributions from both sides",
inputs_a: generate_inputs(&[TestOutput::P2WPKH(1_000_000); 2]),
- outputs_a: generate_outputs(&[
- TestOutput::P2WPKH(1_000_000),
- TestOutput::P2WPKH(200_000),
- ]),
+ 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: generate_outputs(&[
- TestOutput::P2WPKH(1_000_000),
- TestOutput::P2WPKH(400_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)),
});
do_test_interactive_tx_constructor(TestSession {
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)),
});
let tx =
do_test_interactive_tx_constructor(TestSession {
description: "Invalid input sequence from initiator",
inputs_a: vec![(invalid_sequence_input, tx.clone())],
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(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)),
});
let duplicate_input = TxIn {
previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 },
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(&[TestOutput::P2WPKH(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 {
previous_output: OutPoint { txid: tx.as_transaction().txid(), vout: 0 },
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
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(&[TestOutput::P2WPKH(1_000_000)]),
+ 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_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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Initiator sends too many TxAddInputs",
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)),
});
do_test_interactive_tx_constructor_with_entropy_source(
TestSession {
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,
);
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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Initiator sends an output below dust value",
inputs_a: vec![],
- outputs_a: generate_outputs(&[TestOutput::P2WSH(
+ 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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Initiator sends an output above maximum sats allowed",
inputs_a: vec![],
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Initiator sends an output without a witness program",
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)),
});
do_test_interactive_tx_constructor_with_entropy_source(
TestSession {
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,
);
do_test_interactive_tx_constructor(TestSession {
description: "Peer contributed more output value than inputs",
inputs_a: generate_inputs(&[TestOutput::P2WPKH(100_000)]),
- outputs_a: generate_outputs(&[TestOutput::P2WPKH(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_funding_script_pubkey(), 0)),
});
do_test_interactive_tx_constructor(TestSession {
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)),
});
do_test_interactive_tx_constructor(TestSession {
description: "Peer contributed more than allowed number of outputs",
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,
});
}