diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index bf99b216b1b..a5c728a012a 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)] @@ -389,10 +390,13 @@ impl NegotiationContext { /// Channel 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 LocalState: 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 RemoteState: State { fn into_negotiation_context(self) -> NegotiationContext; } @@ -496,12 +500,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!(LOCAL_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!(REMOTE_STATE, [ DATA &msgs::TxAddInput, TRANSITION sent_tx_add_input, DATA &msgs::TxRemoveInput, TRANSITION sent_tx_remove_input, @@ -703,6 +711,8 @@ impl InteractiveTxConstructor { } fn do_local_state_transition(&mut self) -> Result { + // We first attempt to send inputs we want to add, then outputs. Once we are done sending + // them both, then we always send tx_complete. if let Some((serial_id, input, prevtx)) = self.inputs_to_contribute.pop() { let msg = msgs::TxAddInput { channel_id: self.channel_id, @@ -789,8 +799,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_local_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; @@ -801,17 +814,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, @@ -822,11 +856,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, @@ -836,7 +886,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, @@ -899,9 +949,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() @@ -934,10 +988,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(), } } @@ -954,7 +1065,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), @@ -970,7 +1081,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), @@ -980,17 +1091,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 { @@ -999,7 +1140,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), @@ -1012,7 +1153,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), @@ -1025,10 +1166,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_local_serial_id(&&entropy_source, true) % 2, 0); + assert_eq!(generate_local_serial_id(&&entropy_source, false) % 2, 1) } }