Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dual funding extension: begin_interactive_funding_tx_construction #3443

Merged
merged 2 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 108 additions & 10 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use bitcoin::amount::Amount;
use bitcoin::constants::ChainHash;
use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash};
use bitcoin::transaction::{Transaction, TxIn};
use bitcoin::transaction::{Transaction, TxIn, TxOut};
use bitcoin::sighash::EcdsaSighashType;
use bitcoin::consensus::encode;
use bitcoin::absolute::LockTime;
Expand All @@ -30,9 +30,9 @@ use crate::ln::types::ChannelId;
use crate::types::payment::{PaymentPreimage, PaymentHash};
use crate::types::features::{ChannelTypeFeatures, InitFeatures};
use crate::ln::interactivetxs::{
get_output_weight, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor,
InteractiveTxConstructorArgs, InteractiveTxSigningSession, InteractiveTxMessageSendResult,
TX_COMMON_FIELDS_WEIGHT,
calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor,
InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult,
OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT,
};
use crate::ln::msgs;
use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket};
Expand Down Expand Up @@ -2220,6 +2220,95 @@ impl<SP: Deref> InitialRemoteCommitmentReceiver<SP> for FundedChannel<SP> where
}

impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
/// Prepare and start interactive transaction negotiation.
/// `change_destination_opt` - Optional destination for optional change; if None,
/// default destination address is used.
/// If error occurs, it is caused by our side, not the counterparty.
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled
fn begin_interactive_funding_tx_construction<ES: Deref>(
&mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey,
change_destination_opt: Option<ScriptBuf>,
) -> Result<Option<InteractiveTxMessageSend>, AbortReason>
where ES::Target: EntropySource
{
debug_assert!(matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_)));
debug_assert!(self.interactive_tx_constructor.is_none());

let mut funding_inputs = Vec::new();
mem::swap(&mut self.dual_funding_context.our_funding_inputs, &mut funding_inputs);

// TODO(splicing): Add prev funding tx as input, must be provided as a parameter

// Add output for funding tx
// Note: For the error case when the inputs are insufficient, it will be handled after
// the `calculate_change_output_value` call below
let mut funding_outputs = Vec::new();
let mut expected_remote_shared_funding_output = None;

let shared_funding_output = TxOut {
value: Amount::from_sat(self.funding.get_value_satoshis()),
script_pubkey: self.funding.get_funding_redeemscript().to_p2wsh(),
};

if self.funding.is_outbound() {
funding_outputs.push(
OutputOwned::Shared(SharedOwnedOutput::new(
shared_funding_output, self.dual_funding_context.our_funding_satoshis,
))
);
} else {
let TxOut { value, script_pubkey } = shared_funding_output;
expected_remote_shared_funding_output = Some((script_pubkey, value.to_sat()));
}

// Optionally add change output
let change_script = if let Some(script) = change_destination_opt {
script
} else {
signer_provider.get_destination_script(self.context.channel_keys_id)
.map_err(|_err| AbortReason::InternalError("Error getting destination script"))?
};
let change_value_opt = calculate_change_output_value(
self.funding.is_outbound(), self.dual_funding_context.our_funding_satoshis,
&funding_inputs, &funding_outputs,
self.dual_funding_context.funding_feerate_sat_per_1000_weight,
change_script.minimal_non_dust().to_sat(),
)?;
if let Some(change_value) = change_value_opt {
let mut change_output = TxOut {
value: Amount::from_sat(change_value),
script_pubkey: change_script,
};
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
let change_output_fee = fee_for_weight(self.dual_funding_context.funding_feerate_sat_per_1000_weight, change_output_weight);
let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee);
// Check dust limit again
if change_value_decreased_with_fee > self.context.holder_dust_limit_satoshis {
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
funding_outputs.push(OutputOwned::Single(change_output));
}
}

let constructor_args = InteractiveTxConstructorArgs {
entropy_source,
holder_node_id,
counterparty_node_id: self.context.counterparty_node_id,
channel_id: self.context.channel_id(),
feerate_sat_per_kw: self.dual_funding_context.funding_feerate_sat_per_1000_weight,
is_initiator: self.funding.is_outbound(),
funding_tx_locktime: self.dual_funding_context.funding_tx_locktime,
inputs_to_contribute: funding_inputs,
outputs_to_contribute: funding_outputs,
expected_remote_shared_funding_output,
};
let mut tx_constructor = InteractiveTxConstructor::new(constructor_args)?;
let msg = tx_constructor.take_initiator_first_message();

self.interactive_tx_constructor = Some(tx_constructor);

Ok(msg)
}

pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult {
InteractiveTxMessageSendResult(match &mut self.interactive_tx_constructor {
Some(ref mut tx_constructor) => tx_constructor.handle_tx_add_input(msg).map_err(
Expand Down Expand Up @@ -4849,6 +4938,9 @@ fn check_v2_funding_inputs_sufficient(
pub(super) struct DualFundingChannelContext {
/// The amount in satoshis we will be contributing to the channel.
pub our_funding_satoshis: u64,
/// The amount in satoshis our counterparty will be contributing to the channel.
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
pub their_funding_satoshis: Option<u64>,
/// The funding transaction locktime suggested by the initiator. If set by us, it is always set
/// to the current block height to align incentives against fee-sniping.
pub funding_tx_locktime: LockTime,
Expand All @@ -4860,6 +4952,8 @@ pub(super) struct DualFundingChannelContext {
/// Note that the `our_funding_satoshis` field is equal to the total value of `our_funding_inputs`
/// minus any fees paid for our contributed weight. This means that change will never be generated
/// and the maximum value possible will go towards funding the channel.
///
/// Note that this field may be emptied once the interactive negotiation has been started.
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
pub our_funding_inputs: Vec<(TxIn, TransactionU16LenLimited)>,
}
Expand Down Expand Up @@ -9829,16 +9923,19 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
unfunded_channel_age_ticks: 0,
holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx),
};
let dual_funding_context = DualFundingChannelContext {
our_funding_satoshis: funding_satoshis,
// TODO(dual_funding) TODO(splicing) Include counterparty contribution, once that's enabled
their_funding_satoshis: None,
funding_tx_locktime,
funding_feerate_sat_per_1000_weight,
our_funding_inputs: funding_inputs,
};
let chan = Self {
funding,
context,
unfunded_context,
dual_funding_context: DualFundingChannelContext {
our_funding_satoshis: funding_satoshis,
funding_tx_locktime,
funding_feerate_sat_per_1000_weight,
our_funding_inputs: funding_inputs,
},
dual_funding_context,
interactive_tx_constructor: None,
interactive_tx_signing_session: None,
};
Expand Down Expand Up @@ -9980,6 +10077,7 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {

let dual_funding_context = DualFundingChannelContext {
our_funding_satoshis: our_funding_satoshis,
their_funding_satoshis: Some(msg.common_fields.funding_satoshis),
funding_tx_locktime: LockTime::from_consensus(msg.locktime),
funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight,
our_funding_inputs: our_funding_inputs.clone(),
Expand Down
Loading
Loading