Skip to content

Commit fc3e1da

Browse files
jkczyzclaude
andcommitted
Split InteractiveTxConstructor::new into outbound/inbound variants
Replace the single public InteractiveTxConstructor::new() with separate new_for_outbound() and new_for_inbound() constructors. This moves the initiator's first message preparation out of the core constructor, making it infallible and removing is_initiator from the args struct. Callers no longer need to handle constructor errors, which avoids having to generate SpliceFailed/DiscardFunding events after the QuiescentAction has already been consumed during splice_init/splice_ack handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 085a42a commit fc3e1da

File tree

2 files changed

+96
-123
lines changed

2 files changed

+96
-123
lines changed

lightning/src/ln/channel.rs

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ use crate::ln::funding::{
5858
};
5959
use crate::ln::interactivetxs::{
6060
AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs,
61-
InteractiveTxMessageSend, InteractiveTxSigningSession, NegotiationError, SharedOwnedInput,
62-
SharedOwnedOutput,
61+
InteractiveTxMessageSend, InteractiveTxSigningSession, SharedOwnedInput, SharedOwnedOutput,
6362
};
6463
use crate::ln::msgs;
6564
use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket};
@@ -6586,7 +6585,7 @@ impl FundingNegotiationContext {
65866585
fn into_interactive_tx_constructor<SP: SignerProvider, ES: EntropySource>(
65876586
self, context: &ChannelContext<SP>, funding: &FundingScope, entropy_source: &ES,
65886587
holder_node_id: PublicKey,
6589-
) -> Result<InteractiveTxConstructor, NegotiationError> {
6588+
) -> InteractiveTxConstructor {
65906589
debug_assert_eq!(
65916590
self.shared_funding_input.is_some(),
65926591
funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(),
@@ -6609,7 +6608,6 @@ impl FundingNegotiationContext {
66096608
counterparty_node_id: context.counterparty_node_id,
66106609
channel_id: context.channel_id(),
66116610
feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight,
6612-
is_initiator: self.is_initiator,
66136611
funding_tx_locktime: self.funding_tx_locktime,
66146612
inputs_to_contribute: self.our_funding_inputs,
66156613
shared_funding_input: self.shared_funding_input,
@@ -6619,7 +6617,11 @@ impl FundingNegotiationContext {
66196617
),
66206618
outputs_to_contribute: self.our_funding_outputs,
66216619
};
6622-
InteractiveTxConstructor::new(constructor_args)
6620+
if self.is_initiator {
6621+
InteractiveTxConstructor::new_for_outbound(constructor_args)
6622+
} else {
6623+
InteractiveTxConstructor::new_for_inbound(constructor_args)
6624+
}
66236625
}
66246626

66256627
fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) {
@@ -12133,13 +12135,7 @@ where
1213312135
&splice_funding,
1213412136
entropy_source,
1213512137
holder_node_id.clone(),
12136-
)
12137-
.map_err(|err| {
12138-
ChannelError::WarnAndDisconnect(format!(
12139-
"Failed to start interactive transaction construction, {:?}",
12140-
err
12141-
))
12142-
})?;
12138+
);
1214312139
debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none());
1214412140

1214512141
let new_funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey;
@@ -12193,13 +12189,7 @@ where
1219312189
&splice_funding,
1219412190
entropy_source,
1219512191
holder_node_id.clone(),
12196-
)
12197-
.map_err(|err| {
12198-
ChannelError::WarnAndDisconnect(format!(
12199-
"Failed to start interactive transaction construction, {:?}",
12200-
err
12201-
))
12202-
})?;
12192+
);
1220312193
let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message();
1220412194

1220512195
debug_assert!(self.context.interactive_tx_signing_session.is_none());
@@ -14087,24 +14077,20 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
1408714077
script_pubkey: funding.get_funding_redeemscript().to_p2wsh(),
1408814078
};
1408914079

14090-
let interactive_tx_constructor = Some(InteractiveTxConstructor::new(
14080+
let interactive_tx_constructor = Some(InteractiveTxConstructor::new_for_inbound(
1409114081
InteractiveTxConstructorArgs {
1409214082
entropy_source,
1409314083
holder_node_id,
1409414084
counterparty_node_id,
1409514085
channel_id: context.channel_id,
1409614086
feerate_sat_per_kw: funding_negotiation_context.funding_feerate_sat_per_1000_weight,
1409714087
funding_tx_locktime: funding_negotiation_context.funding_tx_locktime,
14098-
is_initiator: false,
1409914088
inputs_to_contribute: our_funding_inputs,
1410014089
shared_funding_input: None,
1410114090
shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats),
1410214091
outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(),
1410314092
}
14104-
).map_err(|err| {
14105-
let reason = ClosureReason::ProcessingError { err: err.reason.to_string() };
14106-
ChannelError::Close((err.reason.to_string(), reason))
14107-
})?);
14093+
));
1410814094

1410914095
let unfunded_context = UnfundedChannelContext {
1411014096
unfunded_channel_age_ticks: 0,

lightning/src/ln/interactivetxs.rs

Lines changed: 85 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,7 +2012,6 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> {
20122012
pub counterparty_node_id: PublicKey,
20132013
pub channel_id: ChannelId,
20142014
pub feerate_sat_per_kw: u32,
2015-
pub is_initiator: bool,
20162015
pub funding_tx_locktime: AbsoluteLockTime,
20172016
pub inputs_to_contribute: Vec<FundingTxInput>,
20182017
pub shared_funding_input: Option<SharedOwnedInput>,
@@ -2023,18 +2022,15 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> {
20232022
impl InteractiveTxConstructor {
20242023
/// Instantiates a new `InteractiveTxConstructor`.
20252024
///
2026-
/// If the holder is the initiator, they need to send the first message which is a `TxAddInput`
2027-
/// message.
2028-
pub fn new<ES: EntropySource>(
2029-
args: InteractiveTxConstructorArgs<ES>,
2030-
) -> Result<Self, NegotiationError> {
2025+
/// Use [`Self::new_for_outbound`] or [`Self::new_for_inbound`] instead to also prepare the
2026+
/// first message for the initiator.
2027+
fn new<ES: EntropySource>(args: InteractiveTxConstructorArgs<ES>, is_initiator: bool) -> Self {
20312028
let InteractiveTxConstructorArgs {
20322029
entropy_source,
20332030
holder_node_id,
20342031
counterparty_node_id,
20352032
channel_id,
20362033
feerate_sat_per_kw,
2037-
is_initiator,
20382034
funding_tx_locktime,
20392035
inputs_to_contribute,
20402036
shared_funding_input,
@@ -2104,7 +2100,7 @@ impl InteractiveTxConstructor {
21042100
let next_input_index = (!inputs_to_contribute.is_empty()).then_some(0);
21052101
let next_output_index = (!outputs_to_contribute.is_empty()).then_some(0);
21062102

2107-
let mut constructor = Self {
2103+
Self {
21082104
state_machine,
21092105
is_initiator,
21102106
initiator_first_message: None,
@@ -2113,19 +2109,32 @@ impl InteractiveTxConstructor {
21132109
outputs_to_contribute,
21142110
next_input_index,
21152111
next_output_index,
2116-
};
2117-
// We'll store the first message for the initiator.
2118-
if is_initiator {
2119-
match constructor.maybe_send_message() {
2120-
Ok(message) => {
2121-
constructor.initiator_first_message = Some(message);
2122-
},
2123-
Err(reason) => {
2124-
return Err(constructor.into_negotiation_error(reason));
2125-
},
2126-
}
21272112
}
2128-
Ok(constructor)
2113+
}
2114+
2115+
/// Instantiates a new `InteractiveTxConstructor` for the initiator (outbound splice).
2116+
///
2117+
/// The initiator always has the shared funding output added internally, so preparing the
2118+
/// first message should never fail. Debug asserts verify this invariant.
2119+
pub fn new_for_outbound<ES: EntropySource>(args: InteractiveTxConstructorArgs<ES>) -> Self {
2120+
let mut constructor = Self::new(args, true);
2121+
match constructor.maybe_send_message() {
2122+
Ok(message) => constructor.initiator_first_message = Some(message),
2123+
Err(reason) => {
2124+
debug_assert!(
2125+
false,
2126+
"Outbound constructor should always have inputs: {:?}",
2127+
reason
2128+
);
2129+
},
2130+
}
2131+
constructor
2132+
}
2133+
2134+
/// Instantiates a new `InteractiveTxConstructor` for the non-initiator (inbound splice or
2135+
/// dual-funded channel acceptor).
2136+
pub fn new_for_inbound<ES: EntropySource>(args: InteractiveTxConstructorArgs<ES>) -> Self {
2137+
Self::new(args, false)
21292138
}
21302139

21312140
fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
@@ -2428,84 +2437,62 @@ mod tests {
24282437
&SecretKey::from_slice(&[43; 32]).unwrap(),
24292438
);
24302439

2431-
let mut constructor_a = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs {
2432-
entropy_source,
2433-
channel_id,
2434-
feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW,
2435-
holder_node_id,
2436-
counterparty_node_id,
2437-
is_initiator: true,
2438-
funding_tx_locktime,
2439-
inputs_to_contribute: session.inputs_a,
2440-
shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| {
2441-
SharedOwnedInput::new(
2442-
TxIn {
2443-
previous_output: op,
2444-
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2445-
..Default::default()
2446-
},
2447-
prev_output,
2448-
lo,
2449-
true, // holder_sig_first
2450-
generate_funding_script_pubkey(), // witness_script for test
2451-
)
2452-
}),
2453-
shared_funding_output: SharedOwnedOutput::new(
2454-
session.shared_output_a.0,
2455-
session.shared_output_a.1,
2456-
),
2457-
outputs_to_contribute: session.outputs_a,
2458-
}) {
2459-
Ok(r) => Some(r),
2460-
Err(e) => {
2461-
assert_eq!(
2462-
Some((e.reason, ErrorCulprit::NodeA)),
2463-
session.expect_error,
2464-
"Test: {}",
2465-
session.description
2466-
);
2467-
return;
2468-
},
2469-
};
2470-
let mut constructor_b = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs {
2471-
entropy_source,
2472-
holder_node_id,
2473-
counterparty_node_id,
2474-
channel_id,
2475-
feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW,
2476-
is_initiator: false,
2477-
funding_tx_locktime,
2478-
inputs_to_contribute: session.inputs_b,
2479-
shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| {
2480-
SharedOwnedInput::new(
2481-
TxIn {
2482-
previous_output: op,
2483-
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2484-
..Default::default()
2485-
},
2486-
prev_output,
2487-
lo,
2488-
false, // holder_sig_first
2489-
generate_funding_script_pubkey(), // witness_script for test
2490-
)
2491-
}),
2492-
shared_funding_output: SharedOwnedOutput::new(
2493-
session.shared_output_b.0,
2494-
session.shared_output_b.1,
2495-
),
2496-
outputs_to_contribute: session.outputs_b,
2497-
}) {
2498-
Ok(r) => Some(r),
2499-
Err(e) => {
2500-
assert_eq!(
2501-
Some((e.reason, ErrorCulprit::NodeB)),
2502-
session.expect_error,
2503-
"Test: {}",
2504-
session.description
2505-
);
2506-
return;
2507-
},
2508-
};
2440+
let mut constructor_a =
2441+
Some(InteractiveTxConstructor::new_for_outbound(InteractiveTxConstructorArgs {
2442+
entropy_source,
2443+
channel_id,
2444+
feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW,
2445+
holder_node_id,
2446+
counterparty_node_id,
2447+
funding_tx_locktime,
2448+
inputs_to_contribute: session.inputs_a,
2449+
shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| {
2450+
SharedOwnedInput::new(
2451+
TxIn {
2452+
previous_output: op,
2453+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2454+
..Default::default()
2455+
},
2456+
prev_output,
2457+
lo,
2458+
true, // holder_sig_first
2459+
generate_funding_script_pubkey(), // witness_script for test
2460+
)
2461+
}),
2462+
shared_funding_output: SharedOwnedOutput::new(
2463+
session.shared_output_a.0,
2464+
session.shared_output_a.1,
2465+
),
2466+
outputs_to_contribute: session.outputs_a,
2467+
}));
2468+
let mut constructor_b =
2469+
Some(InteractiveTxConstructor::new_for_inbound(InteractiveTxConstructorArgs {
2470+
entropy_source,
2471+
holder_node_id,
2472+
counterparty_node_id,
2473+
channel_id,
2474+
feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW,
2475+
funding_tx_locktime,
2476+
inputs_to_contribute: session.inputs_b,
2477+
shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| {
2478+
SharedOwnedInput::new(
2479+
TxIn {
2480+
previous_output: op,
2481+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2482+
..Default::default()
2483+
},
2484+
prev_output,
2485+
lo,
2486+
false, // holder_sig_first
2487+
generate_funding_script_pubkey(), // witness_script for test
2488+
)
2489+
}),
2490+
shared_funding_output: SharedOwnedOutput::new(
2491+
session.shared_output_b.0,
2492+
session.shared_output_b.1,
2493+
),
2494+
outputs_to_contribute: session.outputs_b,
2495+
}));
25092496

25102497
let handle_message_send =
25112498
|msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| {

0 commit comments

Comments
 (0)