From: benthecarman Date: Tue, 9 May 2023 18:29:51 +0000 (-0500) Subject: Create and Sign PSBTs for spendable outputs X-Git-Tag: v0.0.116-alpha1~39^2 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=8c0479ac539ebcb41ad7cce8fda5ff15772c0cff;p=rust-lightning Create and Sign PSBTs for spendable outputs --- diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index bbff80b1..82f23fd8 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -4263,7 +4263,7 @@ macro_rules! check_spendable_outputs { match event { Event::SpendableOutputs { mut outputs } => { for outp in outputs.drain(..) { - txn.push($keysinterface.backing.spend_spendable_outputs(&[&outp], Vec::new(), Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &secp_ctx).unwrap()); + txn.push($keysinterface.backing.spend_spendable_outputs(&[&outp], Vec::new(), Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &secp_ctx).unwrap()); all_outputs.push(outp); } }, @@ -4271,7 +4271,7 @@ macro_rules! check_spendable_outputs { }; } if all_outputs.len() > 1 { - if let Ok(tx) = $keysinterface.backing.spend_spendable_outputs(&all_outputs.iter().map(|a| a).collect::>(), Vec::new(), Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &secp_ctx) { + if let Ok(tx) = $keysinterface.backing.spend_spendable_outputs(&all_outputs.iter().map(|a| a).collect::>(), Vec::new(), Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &secp_ctx) { txn.push(tx); } } diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index 1f9a2cf3..6856bbcd 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -114,7 +114,7 @@ fn test_spendable_output<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, spendable_t if let Event::SpendableOutputs { outputs } = spendable.pop().unwrap() { assert_eq!(outputs.len(), 1); let spend_tx = node.keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(), - Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap(); + Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &Secp256k1::new()).unwrap(); check_spends!(spend_tx, spendable_tx); } else { panic!(); } } @@ -2348,7 +2348,7 @@ fn test_anchors_aggregated_revoked_htlc_tx() { if let Event::SpendableOutputs { outputs } = event { assert_eq!(outputs.len(), 1); let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs( - &[&outputs[0]], Vec::new(), Script::new_op_return(&[]), 253, &Secp256k1::new(), + &[&outputs[0]], Vec::new(), Script::new_op_return(&[]), 253, None, &Secp256k1::new(), ).unwrap(); check_spends!(spend_tx, revoked_claims[idx]); } else { diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs index e8f0c125..79443b3e 100644 --- a/lightning/src/ln/reorg_tests.rs +++ b/lightning/src/ln/reorg_tests.rs @@ -587,7 +587,7 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { if let Event::SpendableOutputs { outputs } = node_a_spendable.pop().unwrap() { assert_eq!(outputs.len(), 1); let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(), - Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap(); + Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &Secp256k1::new()).unwrap(); check_spends!(spend_tx, remote_txn_b[0]); } @@ -607,7 +607,7 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { if let Event::SpendableOutputs { outputs } = node_b_spendable.pop().unwrap() { assert_eq!(outputs.len(), 1); let spend_tx = nodes[1].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(), - Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap(); + Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &Secp256k1::new()).unwrap(); check_spends!(spend_tx, remote_txn_a[0]); } } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 9ba3ba55..cd898f12 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -16,6 +16,7 @@ use bitcoin::blockdata::transaction::{Transaction, TxOut, TxIn, EcdsaSighashType use bitcoin::blockdata::script::{Script, Builder}; use bitcoin::blockdata::opcodes; use bitcoin::network::constants::Network; +use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::util::bip32::{ExtendedPrivKey, ExtendedPubKey, ChildNumber}; use bitcoin::util::sighash; @@ -218,6 +219,126 @@ impl_writeable_tlv_based_enum!(SpendableOutputDescriptor, (2, StaticPaymentOutput), ); +impl SpendableOutputDescriptor { + /// Turns this into a [`bitcoin::psbt::Input`] which can be used to create a + /// [`PartiallySignedTransaction`] which spends the given descriptor. + /// + /// Note that this does not include any signatures, just the information required to + /// construct the transaction and sign it. + pub fn to_psbt_input(&self) -> bitcoin::psbt::Input { + match self { + SpendableOutputDescriptor::StaticOutput { output, .. } => { + // Is a standard P2WPKH, no need for witness script + bitcoin::psbt::Input { + witness_utxo: Some(output.clone()), + ..Default::default() + } + }, + SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { + // TODO we could add the witness script as well + bitcoin::psbt::Input { + witness_utxo: Some(descriptor.output.clone()), + ..Default::default() + } + }, + SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { + // TODO we could add the witness script as well + bitcoin::psbt::Input { + witness_utxo: Some(descriptor.output.clone()), + ..Default::default() + } + }, + } + } + + /// Creates an unsigned [`PartiallySignedTransaction`] which spends the given descriptors to + /// the given outputs, plus an output to the given change destination (if sufficient + /// change value remains). The PSBT will have a feerate, at least, of the given value. + /// + /// The `locktime` argument is used to set the transaction's locktime. If `None`, the + /// transaction will have a locktime of 0. It it recommended to set this to the current block + /// height to avoid fee sniping, unless you have some specific reason to use a different + /// locktime. + /// + /// Returns the PSBT and expected max transaction weight. + /// + /// Returns `Err(())` if the output value is greater than the input value minus required fee, + /// if a descriptor was duplicated, or if an output descriptor `script_pubkey` + /// does not match the one we can spend. + /// + /// We do not enforce that outputs meet the dust limit or that any output scripts are standard. + pub fn create_spendable_outputs_psbt(descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: Script, feerate_sat_per_1000_weight: u32, locktime: Option) -> Result<(PartiallySignedTransaction, usize), ()> { + let mut input = Vec::with_capacity(descriptors.len()); + let mut input_value = 0; + let mut witness_weight = 0; + let mut output_set = HashSet::with_capacity(descriptors.len()); + for outp in descriptors { + match outp { + SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { + if !output_set.insert(descriptor.outpoint) { return Err(()); } + input.push(TxIn { + previous_output: descriptor.outpoint.into_bitcoin_outpoint(), + script_sig: Script::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }); + witness_weight += StaticPaymentOutputDescriptor::MAX_WITNESS_LENGTH; + #[cfg(feature = "grind_signatures")] + { witness_weight -= 1; } // Guarantees a low R signature + input_value += descriptor.output.value; + }, + SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { + if !output_set.insert(descriptor.outpoint) { return Err(()); } + input.push(TxIn { + previous_output: descriptor.outpoint.into_bitcoin_outpoint(), + script_sig: Script::new(), + sequence: Sequence(descriptor.to_self_delay as u32), + witness: Witness::new(), + }); + witness_weight += DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH; + #[cfg(feature = "grind_signatures")] + { witness_weight -= 1; } // Guarantees a low R signature + input_value += descriptor.output.value; + }, + SpendableOutputDescriptor::StaticOutput { ref outpoint, ref output } => { + if !output_set.insert(*outpoint) { return Err(()); } + input.push(TxIn { + previous_output: outpoint.into_bitcoin_outpoint(), + script_sig: Script::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }); + witness_weight += 1 + 73 + 34; + #[cfg(feature = "grind_signatures")] + { witness_weight -= 1; } // Guarantees a low R signature + input_value += output.value; + } + } + if input_value > MAX_VALUE_MSAT / 1000 { return Err(()); } + } + let mut tx = Transaction { + version: 2, + lock_time: locktime.unwrap_or(PackedLockTime::ZERO), + input, + output: outputs, + }; + let expected_max_weight = + transaction_utils::maybe_add_change_output(&mut tx, input_value, witness_weight, feerate_sat_per_1000_weight, change_destination_script)?; + + let psbt_inputs = descriptors.iter().map(|d| d.to_psbt_input()).collect::>(); + let psbt = PartiallySignedTransaction { + inputs: psbt_inputs, + outputs: vec![Default::default(); tx.output.len()], + unsigned_tx: tx, + xpub: Default::default(), + version: 0, + proprietary: Default::default(), + unknown: Default::default(), + }; + Ok((psbt, expected_max_weight)) + } +} + /// A trait to handle Lightning channel key material without concretizing the channel type or /// the signature mechanism. pub trait ChannelSigner { @@ -1171,97 +1292,40 @@ impl KeysManager { ) } - /// Creates a [`Transaction`] which spends the given descriptors to the given outputs, plus an - /// output to the given change destination (if sufficient change value remains). The - /// transaction will have a feerate, at least, of the given value. + /// Signs the given [`PartiallySignedTransaction`] which spends the given [`SpendableOutputDescriptor`]s. + /// The resulting inputs will be finalized and the PSBT will be ready for broadcast if there + /// are no other inputs that need signing. /// - /// Returns `Err(())` if the output value is greater than the input value minus required fee, - /// if a descriptor was duplicated, or if an output descriptor `script_pubkey` - /// does not match the one we can spend. - /// - /// We do not enforce that outputs meet the dust limit or that any output scripts are standard. + /// Returns `Err(())` if the PSBT is missing a descriptor or if we fail to sign. /// /// May panic if the [`SpendableOutputDescriptor`]s were not generated by channels which used /// this [`KeysManager`] or one of the [`InMemorySigner`] created by this [`KeysManager`]. - pub fn spend_spendable_outputs(&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: Script, feerate_sat_per_1000_weight: u32, secp_ctx: &Secp256k1) -> Result { - let mut input = Vec::new(); - let mut input_value = 0; - let mut witness_weight = 0; - let mut output_set = HashSet::with_capacity(descriptors.len()); - for outp in descriptors { - match outp { - SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { - input.push(TxIn { - previous_output: descriptor.outpoint.into_bitcoin_outpoint(), - script_sig: Script::new(), - sequence: Sequence::ZERO, - witness: Witness::new(), - }); - witness_weight += StaticPaymentOutputDescriptor::MAX_WITNESS_LENGTH; - #[cfg(feature = "grind_signatures")] - { witness_weight -= 1; } // Guarantees a low R signature - input_value += descriptor.output.value; - if !output_set.insert(descriptor.outpoint) { return Err(()); } - }, - SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { - input.push(TxIn { - previous_output: descriptor.outpoint.into_bitcoin_outpoint(), - script_sig: Script::new(), - sequence: Sequence(descriptor.to_self_delay as u32), - witness: Witness::new(), - }); - witness_weight += DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH; - #[cfg(feature = "grind_signatures")] - { witness_weight -= 1; } // Guarantees a low R signature - input_value += descriptor.output.value; - if !output_set.insert(descriptor.outpoint) { return Err(()); } - }, - SpendableOutputDescriptor::StaticOutput { ref outpoint, ref output } => { - input.push(TxIn { - previous_output: outpoint.into_bitcoin_outpoint(), - script_sig: Script::new(), - sequence: Sequence::ZERO, - witness: Witness::new(), - }); - witness_weight += 1 + 73 + 34; - #[cfg(feature = "grind_signatures")] - { witness_weight -= 1; } // Guarantees a low R signature - input_value += output.value; - if !output_set.insert(*outpoint) { return Err(()); } - } - } - if input_value > MAX_VALUE_MSAT / 1000 { return Err(()); } - } - let mut spend_tx = Transaction { - version: 2, - lock_time: PackedLockTime(0), - input, - output: outputs, - }; - let expected_max_weight = - transaction_utils::maybe_add_change_output(&mut spend_tx, input_value, witness_weight, feerate_sat_per_1000_weight, change_destination_script)?; - + pub fn sign_spendable_outputs_psbt(&self, descriptors: &[&SpendableOutputDescriptor], psbt: &mut PartiallySignedTransaction, secp_ctx: &Secp256k1) -> Result<(), ()> { let mut keys_cache: Option<(InMemorySigner, [u8; 32])> = None; - let mut input_idx = 0; for outp in descriptors { match outp { SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { + let input_idx = psbt.unsigned_tx.input.iter().position(|i| i.previous_output == descriptor.outpoint.into_bitcoin_outpoint()).ok_or(())?; if keys_cache.is_none() || keys_cache.as_ref().unwrap().1 != descriptor.channel_keys_id { keys_cache = Some(( self.derive_channel_keys(descriptor.channel_value_satoshis, &descriptor.channel_keys_id), descriptor.channel_keys_id)); } - spend_tx.input[input_idx].witness = Witness::from_vec(keys_cache.as_ref().unwrap().0.sign_counterparty_payment_input(&spend_tx, input_idx, &descriptor, &secp_ctx)?); + let witness = Witness::from_vec(keys_cache.as_ref().unwrap().0.sign_counterparty_payment_input(&psbt.unsigned_tx, input_idx, &descriptor, &secp_ctx)?); + psbt.inputs[input_idx].final_script_witness = Some(witness); }, SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { + let input_idx = psbt.unsigned_tx.input.iter().position(|i| i.previous_output == descriptor.outpoint.into_bitcoin_outpoint()).ok_or(())?; if keys_cache.is_none() || keys_cache.as_ref().unwrap().1 != descriptor.channel_keys_id { keys_cache = Some(( self.derive_channel_keys(descriptor.channel_value_satoshis, &descriptor.channel_keys_id), descriptor.channel_keys_id)); } - spend_tx.input[input_idx].witness = Witness::from_vec(keys_cache.as_ref().unwrap().0.sign_dynamic_p2wsh_input(&spend_tx, input_idx, &descriptor, &secp_ctx)?); + let witness = Witness::from_vec(keys_cache.as_ref().unwrap().0.sign_dynamic_p2wsh_input(&psbt.unsigned_tx, input_idx, &descriptor, &secp_ctx)?); + psbt.inputs[input_idx].final_script_witness = Some(witness); }, - SpendableOutputDescriptor::StaticOutput { ref output, .. } => { + SpendableOutputDescriptor::StaticOutput { ref outpoint, ref output } => { + let input_idx = psbt.unsigned_tx.input.iter().position(|i| i.previous_output == outpoint.into_bitcoin_outpoint()).ok_or(())?; let derivation_idx = if output.script_pubkey == self.destination_script { 1 } else { @@ -1288,17 +1352,42 @@ impl KeysManager { if payment_script != output.script_pubkey { return Err(()); }; - let sighash = hash_to_message!(&sighash::SighashCache::new(&spend_tx).segwit_signature_hash(input_idx, &witness_script, output.value, EcdsaSighashType::All).unwrap()[..]); + let sighash = hash_to_message!(&sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(input_idx, &witness_script, output.value, EcdsaSighashType::All).unwrap()[..]); let sig = sign_with_aux_rand(secp_ctx, &sighash, &secret.private_key, &self); let mut sig_ser = sig.serialize_der().to_vec(); sig_ser.push(EcdsaSighashType::All as u8); - spend_tx.input[input_idx].witness.push(sig_ser); - spend_tx.input[input_idx].witness.push(pubkey.inner.serialize().to_vec()); + let witness = Witness::from_vec(vec![sig_ser, pubkey.inner.serialize().to_vec()]); + psbt.inputs[input_idx].final_script_witness = Some(witness); }, } - input_idx += 1; } + Ok(()) + } + + /// Creates a [`Transaction`] which spends the given descriptors to the given outputs, plus an + /// output to the given change destination (if sufficient change value remains). The + /// transaction will have a feerate, at least, of the given value. + /// + /// The `locktime` argument is used to set the transaction's locktime. If `None`, the + /// transaction will have a locktime of 0. It it recommended to set this to the current block + /// height to avoid fee sniping, unless you have some specific reason to use a different + /// locktime. + /// + /// Returns `Err(())` if the output value is greater than the input value minus required fee, + /// if a descriptor was duplicated, or if an output descriptor `script_pubkey` + /// does not match the one we can spend. + /// + /// We do not enforce that outputs meet the dust limit or that any output scripts are standard. + /// + /// May panic if the [`SpendableOutputDescriptor`]s were not generated by channels which used + /// this [`KeysManager`] or one of the [`InMemorySigner`] created by this [`KeysManager`]. + pub fn spend_spendable_outputs(&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: Script, feerate_sat_per_1000_weight: u32, locktime: Option, secp_ctx: &Secp256k1) -> Result { + let (mut psbt, expected_max_weight) = SpendableOutputDescriptor::create_spendable_outputs_psbt(descriptors, outputs, change_destination_script, feerate_sat_per_1000_weight, locktime)?; + self.sign_spendable_outputs_psbt(descriptors, &mut psbt, secp_ctx)?; + + let spend_tx = psbt.extract_tx(); + debug_assert!(expected_max_weight >= spend_tx.weight()); // Note that witnesses with a signature vary somewhat in size, so allow // `expected_max_weight` to overshoot by up to 3 bytes per input. @@ -1512,8 +1601,8 @@ impl PhantomKeysManager { } /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. - pub fn spend_spendable_outputs(&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: Script, feerate_sat_per_1000_weight: u32, secp_ctx: &Secp256k1) -> Result { - self.inner.spend_spendable_outputs(descriptors, outputs, change_destination_script, feerate_sat_per_1000_weight, secp_ctx) + pub fn spend_spendable_outputs(&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: Script, feerate_sat_per_1000_weight: u32, locktime: Option, secp_ctx: &Secp256k1) -> Result { + self.inner.spend_spendable_outputs(descriptors, outputs, change_destination_script, feerate_sat_per_1000_weight, locktime, secp_ctx) } /// See [`KeysManager::derive_channel_keys`] for documentation on this method.