diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index dfa15c0a846..5b26a75e0df 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -66,6 +66,7 @@ pub enum AbortReason { InvalidOutputScript, InsufficientFees, OutputsValueExceedsInputsValue, + InvalidTx, } #[derive(Debug)] @@ -106,7 +107,7 @@ impl NegotiationContext { self.outputs .iter() .filter(move |(serial_id, _)| self.is_serial_id_valid_for_counterparty(serial_id)) - .map(|(_, input_with_prevout)| input_with_prevout) + .map(|(_, output)| output) } fn received_tx_add_input(&mut self, msg: &msgs::TxAddInput) -> Result<(), AbortReason> { @@ -146,7 +147,9 @@ impl NegotiationContext { // - MUST fail the negotiation if: // - the `scriptPubKey` is not a witness program return Err(AbortReason::PrevTxOutInvalid); - } else if !self.prevtx_outpoints.insert(OutPoint { txid, vout: msg.prevtx_out }) { + } + + if !self.prevtx_outpoints.insert(OutPoint { txid, vout: msg.prevtx_out }) { // The receiving node: // - MUST fail the negotiation if: // - the `prevtx` and `prevtx_vout` are identical to a previously added @@ -172,17 +175,14 @@ impl NegotiationContext { return Err(AbortReason::DuplicateSerialId); } let prev_outpoint = OutPoint { txid, vout: msg.prevtx_out }; - self.inputs.insert( - msg.serial_id, - TxInputWithPrevOutput { - input: TxIn { - previous_output: prev_outpoint.clone(), - sequence: Sequence(msg.sequence), - ..Default::default() - }, - prev_output: prev_out, + self.inputs.entry(msg.serial_id).or_insert_with(|| TxInputWithPrevOutput { + input: TxIn { + previous_output: prev_outpoint.clone(), + sequence: Sequence(msg.sequence), + ..Default::default() }, - ); + prev_output: prev_out, + }); self.prevtx_outpoints.insert(prev_outpoint); Ok(()) } @@ -192,15 +192,14 @@ impl NegotiationContext { return Err(AbortReason::IncorrectSerialIdParity); } - if let Some(_) = self.inputs.remove(&msg.serial_id) { - Ok(()) - } else { + self.inputs + .remove(&msg.serial_id) // The receiving node: // - MUST fail the negotiation if: // - the input or output identified by the `serial_id` was not added by the sender // - the `serial_id` does not correspond to a currently added input - Err(AbortReason::SerialIdUnknown) - } + .ok_or(AbortReason::SerialIdUnknown) + .map(|_| ()) } fn received_tx_add_output(&mut self, msg: &msgs::TxAddOutput) -> Result<(), AbortReason> { @@ -225,7 +224,14 @@ impl NegotiationContext { // - the sats amount is less than the dust_limit return Err(AbortReason::BelowDustLimit); } - if msg.sats > TOTAL_BITCOIN_SUPPLY_SATOSHIS { + + // Check that adding this output would not cause the total output value to exceed the total + // bitcoin supply. + let mut outputs_value: u64 = 0; + for output in self.outputs.iter() { + outputs_value = outputs_value.saturating_add(output.1.value); + } + if outputs_value.saturating_add(msg.sats) > TOTAL_BITCOIN_SUPPLY_SATOSHIS { // The receiving node: // - MUST fail the negotiation if: // - the sats amount is greater than 2,100,000,000,000,000 (TOTAL_BITCOIN_SUPPLY_SATOSHIS) @@ -256,7 +262,7 @@ impl NegotiationContext { } let output = TxOut { value: msg.sats, script_pubkey: msg.script.clone() }; - self.outputs.insert(msg.serial_id, output); + self.outputs.entry(msg.serial_id).or_insert(output); Ok(()) } @@ -386,17 +392,45 @@ impl NegotiationContext { } } -/// Channel states that can send & receive `tx_(add|remove)_(input|output)` and `tx_complete` +// The interactive transaction construction protocol allows two peers to collaboratively build a +// transaction for broadcast. +// +// The protocol is turn-based, so we define different states here that we store depending on whose +// turn it is to send the next message. The states are defined so that their types ensure we only +// perform actions (only send messages) via defined state transitions that do not violate the +// protocol. +// +// An example of a full negotiation and associated states follows: +// +// +------------+ +------------------+---- Holder state after message sent/received ----+ +// | |--(1)- tx_add_input ---->| | MsgSentChange + +// | |<-(2)- tx_complete ------| | ReceivedTxComplete + +// | |--(3)- tx_add_output --->| | MsgSentChange + +// | |<-(4)- tx_complete ------| | ReceivedTxComplete + +// | |--(5)- tx_add_input ---->| | MsgSentChange + +// | Holder |<-(6)- tx_add_input -----| Counterparty | MsgReceivedChange + +// | |--(7)- tx_remove_output >| | MsgSentChange + +// | |<-(8)- tx_add_output ----| | MsgReceivedChange + +// | |--(9)- tx_complete ----->| | SentTxComplete + +// | |<-(10) tx_complete ------| | NegotiationComplete + +// +------------+ +------------------+--------------------------------------------------+ + +/// Negotiation states that can send & receive `tx_(add|remove)_(input|output)` and `tx_complete` trait State {} +/// Category of states where we have sent some message to the counterparty, and we are waiting for +/// a response. trait MsgSentState: State { fn into_negotiation_context(self) -> NegotiationContext; } +/// Category of states that our counterparty has put us in after we receive a message from them. trait MsgReceivedState: State { fn into_negotiation_context(self) -> NegotiationContext; } +// This macro is a helper for implementing the above state traits for various states subsequently +// defined below the macro. macro_rules! define_state { (MSG_SENT_STATE, $state: ident, $doc: expr) => { define_state!($state, NegotiationContext, $doc); @@ -455,6 +489,8 @@ trait StateTransition { fn transition(self, data: TransitionData) -> StateTransitionResult; } +// This macro helps define the legal transitions between the states above by implementing +// the `StateTransition` trait for each of the states that follow this declaration. macro_rules! define_state_transitions { (MSG_SENT_STATE, [$(DATA $data: ty, TRANSITION $transition: ident),+]) => { $( @@ -495,12 +531,16 @@ macro_rules! define_state_transitions { }; } +// State transitions when we have sent our counterparty some messages and are waiting for them +// to respond. define_state_transitions!(MSG_SENT_STATE, [ DATA &msgs::TxAddInput, TRANSITION received_tx_add_input, DATA &msgs::TxRemoveInput, TRANSITION received_tx_remove_input, DATA &msgs::TxAddOutput, TRANSITION received_tx_add_output, DATA &msgs::TxRemoveOutput, TRANSITION received_tx_remove_output ]); +// State transitions when we have received some messages from our counterparty and we should +// respond. define_state_transitions!(MSG_RECEIVED_STATE, [ DATA &msgs::TxAddInput, TRANSITION sent_tx_add_input, DATA &msgs::TxRemoveInput, TRANSITION sent_tx_remove_input, @@ -527,6 +567,9 @@ impl Default for StateMachine { } } +// The `StateMachine` internally executes the actual transition between two states and keeps +// track of the current state. This macro defines _how_ those state transitions happen to +// update the internal state. macro_rules! define_state_machine_transitions { ($transition: ident, $msg: ty, [$(FROM $from_state: ident, TO $to_state: ident),+]) => { fn $transition(self, msg: $msg) -> StateMachine { @@ -541,16 +584,6 @@ macro_rules! define_state_machine_transitions { } } }; - (MSG_SENT_OR_RECEIVED_CHANGE, $to_sent_transition: ident, $to_received_transition: ident, $msg: ty) => { - define_state_machine_transitions!($to_sent_transition, $msg, [ - FROM MsgReceivedChange, TO MsgSentChange, - FROM ReceivedTxComplete, TO MsgSentChange - ]); - define_state_machine_transitions!($to_received_transition, $msg, [ - FROM MsgSentChange, TO MsgReceivedChange, - FROM SentTxComplete, TO MsgReceivedChange - ]); - }; } impl StateMachine { @@ -576,30 +609,47 @@ impl StateMachine { } } - define_state_machine_transitions!( - MSG_SENT_OR_RECEIVED_CHANGE, - sent_tx_add_input, - received_tx_add_input, - &msgs::TxAddInput - ); - define_state_machine_transitions!( - MSG_SENT_OR_RECEIVED_CHANGE, - sent_tx_add_output, - received_tx_add_output, - &msgs::TxAddOutput - ); - define_state_machine_transitions!( - MSG_SENT_OR_RECEIVED_CHANGE, - sent_tx_remove_input, - received_tx_remove_input, - &msgs::TxRemoveInput - ); - define_state_machine_transitions!( - MSG_SENT_OR_RECEIVED_CHANGE, - sent_tx_remove_output, - received_tx_remove_output, - &msgs::TxRemoveOutput - ); + // TxAddInput + define_state_machine_transitions!(sent_tx_add_input, &msgs::TxAddInput, [ + FROM MsgReceivedChange, TO MsgSentChange, + FROM ReceivedTxComplete, TO MsgSentChange + ]); + define_state_machine_transitions!(received_tx_add_input, &msgs::TxAddInput, [ + FROM MsgSentChange, TO MsgReceivedChange, + FROM SentTxComplete, TO MsgReceivedChange + ]); + + // TxAddOutput + define_state_machine_transitions!(sent_tx_add_output, &msgs::TxAddOutput, [ + FROM MsgReceivedChange, TO MsgSentChange, + FROM ReceivedTxComplete, TO MsgSentChange + ]); + define_state_machine_transitions!(received_tx_add_output, &msgs::TxAddOutput, [ + FROM MsgSentChange, TO MsgReceivedChange, + FROM SentTxComplete, TO MsgReceivedChange + ]); + + // TxRemoveInput + define_state_machine_transitions!(sent_tx_remove_input, &msgs::TxRemoveInput, [ + FROM MsgReceivedChange, TO MsgSentChange, + FROM ReceivedTxComplete, TO MsgSentChange + ]); + define_state_machine_transitions!(received_tx_remove_input, &msgs::TxRemoveInput, [ + FROM MsgSentChange, TO MsgReceivedChange, + FROM SentTxComplete, TO MsgReceivedChange + ]); + + // TxRemoveOutput + define_state_machine_transitions!(sent_tx_remove_output, &msgs::TxRemoveOutput, [ + FROM MsgReceivedChange, TO MsgSentChange, + FROM ReceivedTxComplete, TO MsgSentChange + ]); + define_state_machine_transitions!(received_tx_remove_output, &msgs::TxRemoveOutput, [ + FROM MsgSentChange, TO MsgReceivedChange, + FROM SentTxComplete, TO MsgReceivedChange + ]); + + // TxComplete define_state_machine_transitions!(sent_tx_complete, &msgs::TxComplete, [ FROM MsgReceivedChange, TO SentTxComplete, FROM ReceivedTxComplete, TO NegotiationComplete @@ -623,6 +673,7 @@ pub enum InteractiveTxMessageSend { TxComplete(msgs::TxComplete), } +// This macro executes a state machine transition based on a provided action. macro_rules! do_state_transition { ($self: ident, $transition: ident, $msg: expr) => {{ let state_machine = core::mem::take(&mut $self.state_machine); @@ -788,8 +839,11 @@ impl InteractiveTxConstructor { #[cfg(test)] mod tests { use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; + use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::interactivetxs::{ - AbortReason, InteractiveTxConstructor, InteractiveTxMessageSend, + generate_holder_serial_id, AbortReason, InteractiveTxConstructor, InteractiveTxMessageSend, + MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, }; use crate::ln::ChannelId; use crate::sign::EntropySource; @@ -800,17 +854,38 @@ mod tests { use bitcoin::{ absolute::LockTime as AbsoluteLockTime, OutPoint, Sequence, Transaction, TxIn, TxOut, }; - use core::default::Default; + use core::ops::Deref; + // A simple entropy source that works based on an atomic counter. struct TestEntropySource(AtomicCounter); impl EntropySource for TestEntropySource { fn get_secure_random_bytes(&self) -> [u8; 32] { - let bytes = self.0.get_increment().to_be_bytes(); let mut res = [0u8; 32]; - res[0..8].copy_from_slice(&bytes); + let increment = self.0.get_increment(); + for i in 0..32 { + // Rotate the increment value by 'i' bits to the right, to avoid clashes + // when `generate_local_serial_id` does a parity flip on consecutive calls for the + // same party. + let rotated_increment = increment.rotate_right(i as u32); + res[i] = (rotated_increment & 0xff) as u8; + } + res + } + } + + // An entropy source that deliberately returns you the same seed every time. We use this + // to test if the constructor would catch inputs/outputs that are attempting to be added + // with duplicate serial ids. + struct DuplicateEntropySource; + impl EntropySource for DuplicateEntropySource { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let mut res = [0u8; 32]; + let count = 1u64; + res[0..8].copy_from_slice(&count.to_be_bytes()); res } } + struct TestSession { inputs_a: Vec<(TxIn, TransactionU16LenLimited)>, outputs_a: Vec, @@ -821,11 +896,27 @@ mod tests { fn do_test_interactive_tx_constructor(session: TestSession) { let entropy_source = TestEntropySource(AtomicCounter::new()); + do_test_interactive_tx_constructor_internal(session, &&entropy_source); + } + + fn do_test_interactive_tx_constructor_with_entropy_source( + session: TestSession, entropy_source: ES, + ) where + ES::Target: EntropySource, + { + do_test_interactive_tx_constructor_internal(session, &entropy_source); + } + + fn do_test_interactive_tx_constructor_internal( + session: TestSession, entropy_source: &ES, + ) where + ES::Target: EntropySource, + { let channel_id = ChannelId(entropy_source.get_secure_random_bytes()); let tx_locktime = AbsoluteLockTime::from_height(1337).unwrap(); let (mut constructor_a, first_message_a) = InteractiveTxConstructor::new( - &&entropy_source, + entropy_source, channel_id, FEERATE_FLOOR_SATS_PER_KW * 10, true, @@ -835,7 +926,7 @@ mod tests { 0, ); let (mut constructor_b, first_message_b) = InteractiveTxConstructor::new( - &&entropy_source, + entropy_source, channel_id, FEERATE_FLOOR_SATS_PER_KW * 10, false, @@ -898,9 +989,13 @@ mod tests { } fn generate_tx(values: &[u64]) -> Transaction { + generate_tx_with_locktime(values, 1337) + } + + fn generate_tx_with_locktime(values: &[u64], locktime: u32) -> Transaction { Transaction { version: 2, - lock_time: AbsoluteLockTime::from_height(1337).unwrap(), + lock_time: AbsoluteLockTime::from_height(locktime).unwrap(), input: vec![TxIn { ..Default::default() }], output: values .iter() @@ -933,10 +1028,67 @@ mod tests { .collect() } - fn generate_output(value: u64) -> TxOut { + fn generate_outputs(values: &[u64]) -> Vec { + values + .iter() + .map(|value| TxOut { + value: *value, + script_pubkey: Builder::new() + .push_opcode(opcodes::OP_TRUE) + .into_script() + .to_v0_p2wsh(), + }) + .collect() + } + + fn generate_fixed_number_of_inputs(count: u16) -> Vec<(TxIn, TransactionU16LenLimited)> { + // Generate transactions with a total `count` number of outputs such that no transaction has a + // serialized length greater than u16::MAX. + let max_outputs_per_prevtx = 1_500; + let mut remaining = count; + let mut inputs: Vec<(TxIn, TransactionU16LenLimited)> = Vec::with_capacity(count as usize); + + while remaining > 0 { + let tx_output_count = remaining.min(max_outputs_per_prevtx); + remaining -= tx_output_count; + + // Use unique locktime for each tx so outpoints are different across transactions + let tx = generate_tx_with_locktime( + &vec![1_000_000; tx_output_count as usize], + (1337 + remaining).into(), + ); + let txid = tx.txid(); + + let mut temp: Vec<(TxIn, TransactionU16LenLimited)> = tx + .output + .iter() + .enumerate() + .map(|(idx, _)| { + let input = TxIn { + previous_output: OutPoint { txid, vout: idx as u32 }, + script_sig: Default::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Default::default(), + }; + (input, TransactionU16LenLimited::new(tx.clone()).unwrap()) + }) + .collect(); + + inputs.append(&mut temp); + } + + inputs + } + + fn generate_fixed_number_of_outputs(count: u16) -> Vec { + // Set a constant value for each TxOut + generate_outputs(&vec![1_000_000; count as usize]) + } + + fn generate_non_witness_output(value: u64) -> TxOut { TxOut { value, - script_pubkey: Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_v0_p2wsh(), + script_pubkey: Builder::new().push_opcode(opcodes::OP_TRUE).into_script().to_p2sh(), } } @@ -953,7 +1105,7 @@ mod tests { // Single contribution, no initiator inputs. do_test_interactive_tx_constructor(TestSession { inputs_a: vec![], - outputs_a: vec![generate_output(1_000_000)], + outputs_a: generate_outputs(&[1_000_000]), inputs_b: vec![], outputs_b: vec![], expect_error: Some(AbortReason::OutputsValueExceedsInputsValue), @@ -969,7 +1121,7 @@ mod tests { // Single contribution, insufficient fees. do_test_interactive_tx_constructor(TestSession { inputs_a: generate_inputs(&[1_000_000]), - outputs_a: vec![generate_output(1_000_000)], + outputs_a: generate_outputs(&[1_000_000]), inputs_b: vec![], outputs_b: vec![], expect_error: Some(AbortReason::InsufficientFees), @@ -979,17 +1131,47 @@ mod tests { inputs_a: generate_inputs(&[1_000_000]), outputs_a: vec![], inputs_b: generate_inputs(&[100_000]), - outputs_b: vec![generate_output(100_000)], + outputs_b: generate_outputs(&[100_000]), expect_error: Some(AbortReason::InsufficientFees), }); // 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: vec![generate_output(1_000_000), generate_output(200_000)], + outputs_a: generate_outputs(&[1_000_000, 200_000]), inputs_b: generate_inputs(&[1_000_000, 500_000]), - outputs_b: vec![generate_output(1_000_000), generate_output(400_000)], + outputs_b: generate_outputs(&[1_000_000, 400_000]), expect_error: None, }); + + // Prevout from initiator is not a witness program + let non_segwit_output_tx = { + let mut tx = generate_tx(&[1_000_000]); + tx.output.push(TxOut { + script_pubkey: Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .into_script() + .to_p2sh(), + ..Default::default() + }); + + TransactionU16LenLimited::new(tx).unwrap() + }; + let non_segwit_input = TxIn { + previous_output: OutPoint { + txid: non_segwit_output_tx.as_transaction().txid(), + vout: 1, + }, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![(non_segwit_input, non_segwit_output_tx)], + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::PrevTxOutInvalid), + }); + // Invalid input sequence from initiator. let tx = TransactionU16LenLimited::new(generate_tx(&[1_000_000])).unwrap(); let invalid_sequence_input = TxIn { @@ -998,7 +1180,7 @@ mod tests { }; do_test_interactive_tx_constructor(TestSession { inputs_a: vec![(invalid_sequence_input, tx.clone())], - outputs_a: vec![generate_output(1_000_000)], + outputs_a: generate_outputs(&[1_000_000]), inputs_b: vec![], outputs_b: vec![], expect_error: Some(AbortReason::IncorrectInputSequenceValue), @@ -1011,7 +1193,7 @@ mod tests { }; do_test_interactive_tx_constructor(TestSession { inputs_a: vec![(duplicate_input.clone(), tx.clone()), (duplicate_input, tx.clone())], - outputs_a: vec![generate_output(1_000_000)], + outputs_a: generate_outputs(&[1_000_000]), inputs_b: vec![], outputs_b: vec![], expect_error: Some(AbortReason::PrevTxOutInvalid), @@ -1024,10 +1206,109 @@ mod tests { }; do_test_interactive_tx_constructor(TestSession { inputs_a: vec![(duplicate_input.clone(), tx.clone())], - outputs_a: vec![generate_output(1_000_000)], + outputs_a: generate_outputs(&[1_000_000]), inputs_b: vec![(duplicate_input.clone(), tx.clone())], outputs_b: vec![], expect_error: Some(AbortReason::PrevTxOutInvalid), }); + // Initiator sends too many TxAddInputs + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_fixed_number_of_inputs(MAX_RECEIVED_TX_ADD_INPUT_COUNT + 1), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::ReceivedTooManyTxAddInputs), + }); + // Attempt to queue up two inputs with duplicate serial ids. We use a deliberately bad + // entropy source, `DuplicateEntropySource` to simulate this. + do_test_interactive_tx_constructor_with_entropy_source( + TestSession { + inputs_a: generate_fixed_number_of_inputs(2), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::DuplicateSerialId), + }, + &DuplicateEntropySource, + ); + // Initiator sends too many TxAddOutputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_fixed_number_of_outputs(MAX_RECEIVED_TX_ADD_OUTPUT_COUNT + 1), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::ReceivedTooManyTxAddOutputs), + }); + // Initiator sends an output below dust value. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_outputs(&[1]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::BelowDustLimit), + }); + // Initiator sends an output above maximum sats allowed. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: generate_outputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::ExceededMaximumSatsAllowed), + }); + // Initiator sends an output without a witness program. + do_test_interactive_tx_constructor(TestSession { + inputs_a: vec![], + outputs_a: vec![generate_non_witness_output(1_000_000)], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::InvalidOutputScript), + }); + // Attempt to queue up two outputs with duplicate serial ids. We use a deliberately bad + // entropy source, `DuplicateEntropySource` to simulate this. + do_test_interactive_tx_constructor_with_entropy_source( + TestSession { + inputs_a: vec![], + outputs_a: generate_fixed_number_of_outputs(2), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::DuplicateSerialId), + }, + &DuplicateEntropySource, + ); + + // Peer contributed more output value than inputs + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[100_000]), + outputs_a: generate_outputs(&[1_000_000]), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::OutputsValueExceedsInputsValue), + }); + + // Peer contributed more than allowed number of inputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_fixed_number_of_inputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1), + outputs_a: vec![], + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::ExceededNumberOfInputsOrOutputs), + }); + // Peer contributed more than allowed number of outputs. + do_test_interactive_tx_constructor(TestSession { + inputs_a: generate_inputs(&[TOTAL_BITCOIN_SUPPLY_SATOSHIS]), + outputs_a: generate_fixed_number_of_outputs(MAX_INPUTS_OUTPUTS_COUNT as u16 + 1), + inputs_b: vec![], + outputs_b: vec![], + expect_error: Some(AbortReason::ExceededNumberOfInputsOrOutputs), + }); + } + + #[test] + fn test_generate_local_serial_id() { + let entropy_source = TestEntropySource(AtomicCounter::new()); + + // Initiators should have even serial id, non-initiators should have odd serial id. + assert_eq!(generate_holder_serial_id(&&entropy_source, true) % 2, 0); + assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) } }