Skip to content

Commit d275551

Browse files
jkczyzclaude
andcommitted
f - Defer resolve_queued_contribution until after validation
validate_splice_init and validate_tx_init_rbf check preconditions including quiescence. Move them before resolve_queued_contribution so that a misbehaving peer sending splice_init or tx_init_rbf before quiescence is rejected early. This also moves validate_splice_contributions and FundingScope construction into the callers since they depend on the resolved contribution. Have validate_tx_init_rbf return the last candidate's funding pubkeys so the caller can construct FundingScope without re-accessing pending_splice. Add a debug_assert in quiescent_negotiation_err that Abort errors only occur when the channel is quiescent, since exit_quiescence requires it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 69a1a1c commit d275551

File tree

2 files changed

+156
-68
lines changed

2 files changed

+156
-68
lines changed

lightning/src/ln/channel.rs

Lines changed: 66 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12397,9 +12397,7 @@ where
1239712397
}
1239812398

1239912399
/// Checks during handling splice_init
12400-
pub fn validate_splice_init(
12401-
&self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount,
12402-
) -> Result<FundingScope, ChannelError> {
12400+
pub fn validate_splice_init(&self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> {
1240312401
if self.holder_commitment_point.current_point().is_none() {
1240412402
return Err(ChannelError::WarnAndDisconnect(format!(
1240512403
"Channel {} commitment point needs to be advanced once before spliced",
@@ -12436,33 +12434,7 @@ where
1243612434
)));
1243712435
}
1243812436

12439-
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12440-
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
12441-
12442-
// Rotate the pubkeys using the prev_funding_txid as a tweak
12443-
let prev_funding_txid = self.funding.get_funding_txid();
12444-
let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) {
12445-
(None, _) => {
12446-
debug_assert!(false);
12447-
self.funding.get_holder_pubkeys().funding_pubkey
12448-
},
12449-
(Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => {
12450-
ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx)
12451-
},
12452-
#[cfg(taproot)]
12453-
_ => todo!(),
12454-
};
12455-
let mut new_keys = self.funding.get_holder_pubkeys().clone();
12456-
new_keys.funding_pubkey = funding_pubkey;
12457-
12458-
Ok(FundingScope::for_splice(
12459-
&self.funding,
12460-
&self.context,
12461-
our_funding_contribution,
12462-
their_funding_contribution,
12463-
msg.funding_pubkey,
12464-
new_keys,
12465-
))
12437+
Ok(())
1246612438
}
1246712439

1246812440
fn validate_splice_contributions(
@@ -12596,18 +12568,46 @@ where
1259612568
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
1259712569
logger: &L,
1259812570
) -> Result<msgs::SpliceAck, InteractiveTxMsgError> {
12571+
self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?;
12572+
1259912573
let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64);
12600-
let (our_funding_contribution, holder_balance) = self
12574+
let (queued_net_value, holder_balance) = self
1260112575
.resolve_queued_contribution(feerate, logger)
1260212576
.map_err(|e| self.quiescent_negotiation_err(e))?;
1260312577

12604-
let splice_funding = self
12605-
.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))
12606-
.map_err(|e| self.quiescent_negotiation_err(e))?;
12578+
let our_funding_contribution = queued_net_value.unwrap_or(SignedAmount::ZERO);
12579+
let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis);
12580+
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12581+
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
12582+
12583+
// Rotate the pubkeys using the prev_funding_txid as a tweak
12584+
let prev_funding_txid = self.funding.get_funding_txid();
12585+
let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) {
12586+
(None, _) => {
12587+
debug_assert!(false);
12588+
self.funding.get_holder_pubkeys().funding_pubkey
12589+
},
12590+
(Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => {
12591+
ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx)
12592+
},
12593+
#[cfg(taproot)]
12594+
_ => todo!(),
12595+
};
12596+
let mut holder_pubkeys = self.funding.get_holder_pubkeys().clone();
12597+
holder_pubkeys.funding_pubkey = funding_pubkey;
12598+
12599+
let splice_funding = FundingScope::for_splice(
12600+
&self.funding,
12601+
&self.context,
12602+
our_funding_contribution,
12603+
their_funding_contribution,
12604+
msg.funding_pubkey,
12605+
holder_pubkeys,
12606+
);
1260712607

1260812608
// Adjust for the feerate and clone so we can store it for future RBF re-use.
1260912609
let (adjusted_contribution, our_funding_inputs, our_funding_outputs) =
12610-
if our_funding_contribution.is_some() {
12610+
if queued_net_value.is_some() {
1261112611
let adjusted_contribution = self
1261212612
.take_queued_funding_contribution()
1261312613
.expect("queued_funding_contribution was Some")
@@ -12618,7 +12618,6 @@ where
1261812618
} else {
1261912619
(None, Default::default(), Default::default())
1262012620
};
12621-
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1262212621

1262312622
log_info!(
1262412623
logger,
@@ -12661,9 +12660,8 @@ where
1266112660

1266212661
/// Checks during handling tx_init_rbf for an existing splice
1266312662
fn validate_tx_init_rbf<F: FeeEstimator>(
12664-
&self, msg: &msgs::TxInitRbf, our_funding_contribution: SignedAmount,
12665-
fee_estimator: &LowerBoundedFeeEstimator<F>,
12666-
) -> Result<FundingScope, ChannelError> {
12663+
&self, msg: &msgs::TxInitRbf, fee_estimator: &LowerBoundedFeeEstimator<F>,
12664+
) -> Result<(ChannelPublicKeys, PublicKey), ChannelError> {
1266712665
if self.holder_commitment_point.current_point().is_none() {
1266812666
return Err(ChannelError::WarnAndDisconnect(format!(
1266912667
"Channel {} commitment point needs to be advanced once before RBF",
@@ -12729,33 +12727,22 @@ where
1272912727
return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate));
1273012728
}
1273112729

12732-
let their_funding_contribution = match msg.funding_output_contribution {
12733-
Some(value) => SignedAmount::from_sat(value),
12734-
None => SignedAmount::ZERO,
12735-
};
12736-
12737-
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12738-
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
12739-
1274012730
// Reuse funding pubkeys from the last negotiated candidate since all RBF candidates
1274112731
// for the same splice share the same funding output script.
12742-
let holder_pubkeys = last_candidate.get_holder_pubkeys().clone();
12743-
let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey();
12744-
12745-
Ok(FundingScope::for_splice(
12746-
&self.funding,
12747-
&self.context,
12748-
our_funding_contribution,
12749-
their_funding_contribution,
12750-
counterparty_funding_pubkey,
12751-
holder_pubkeys,
12732+
Ok((
12733+
last_candidate.get_holder_pubkeys().clone(),
12734+
*last_candidate.counterparty_funding_pubkey(),
1275212735
))
1275312736
}
1275412737

1275512738
pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>(
1275612739
&mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey,
1275712740
fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
1275812741
) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> {
12742+
let (holder_pubkeys, counterparty_funding_pubkey) = self
12743+
.validate_tx_init_rbf(msg, fee_estimator)
12744+
.map_err(|e| self.quiescent_negotiation_err(e))?;
12745+
1275912746
let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64);
1276012747
let (queued_net_value, holder_balance) = self
1276112748
.resolve_queued_contribution(feerate, logger)
@@ -12784,14 +12771,23 @@ where
1278412771
};
1278512772

1278612773
let our_funding_contribution = queued_net_value.or(prior_net_value);
12774+
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1278712775

12788-
let rbf_funding = self
12789-
.validate_tx_init_rbf(
12790-
msg,
12791-
our_funding_contribution.unwrap_or(SignedAmount::ZERO),
12792-
fee_estimator,
12793-
)
12794-
.map_err(|e| self.quiescent_negotiation_err(e))?;
12776+
let their_funding_contribution = match msg.funding_output_contribution {
12777+
Some(value) => SignedAmount::from_sat(value),
12778+
None => SignedAmount::ZERO,
12779+
};
12780+
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12781+
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
12782+
12783+
let rbf_funding = FundingScope::for_splice(
12784+
&self.funding,
12785+
&self.context,
12786+
our_funding_contribution,
12787+
their_funding_contribution,
12788+
counterparty_funding_pubkey,
12789+
holder_pubkeys,
12790+
);
1279512791

1279612792
// Consume the appropriate contribution source.
1279712793
let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() {
@@ -12828,8 +12824,6 @@ where
1282812824
Default::default()
1282912825
};
1283012826

12831-
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
12832-
1283312827
log_info!(
1283412828
logger,
1283512829
"Starting RBF funding negotiation for channel {} after receiving tx_init_rbf; channel value: {} sats",
@@ -13968,8 +13962,12 @@ where
1396813962
}
1396913963

1397013964
fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError {
13971-
let exited_quiescence =
13972-
if matches!(err, ChannelError::Abort(_)) { self.exit_quiescence() } else { false };
13965+
let exited_quiescence = if matches!(err, ChannelError::Abort(_)) {
13966+
debug_assert!(self.context.channel_state.is_quiescent());
13967+
self.exit_quiescence()
13968+
} else {
13969+
false
13970+
};
1397313971
InteractiveTxMsgError { err, splice_funding_failed: None, exited_quiescence }
1397413972
}
1397513973

lightning/src/ln/splicing_tests.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6432,6 +6432,96 @@ fn test_splice_revalidation_at_quiescence() {
64326432
expect_splice_failed_events(&nodes[0], &channel_id, contribution);
64336433
}
64346434

6435+
#[test]
6436+
fn test_splice_init_before_quiescence_sends_warning() {
6437+
// A misbehaving peer sends splice_init before quiescence is established. The receiver
6438+
// should send a warning and disconnect.
6439+
let chanmon_cfgs = create_chanmon_cfgs(2);
6440+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
6441+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
6442+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
6443+
6444+
let node_id_1 = nodes[1].node.get_our_node_id();
6445+
6446+
let initial_channel_value_sat = 100_000;
6447+
let (_, _, channel_id, _) =
6448+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
6449+
6450+
// Node 0 initiates quiescence.
6451+
nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
6452+
let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
6453+
6454+
// Misbehaving node 1 sends splice_init before completing the STFU handshake.
6455+
let funding_pubkey =
6456+
PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
6457+
let splice_init = msgs::SpliceInit {
6458+
channel_id,
6459+
funding_contribution_satoshis: 50_000,
6460+
funding_feerate_per_kw: FEERATE_FLOOR_SATS_PER_KW,
6461+
locktime: 0,
6462+
funding_pubkey,
6463+
require_confirmed_inputs: None,
6464+
};
6465+
nodes[0].node.handle_splice_init(node_id_1, &splice_init);
6466+
6467+
// Node 0 should send a warning and disconnect.
6468+
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
6469+
assert_eq!(msg_events.len(), 1);
6470+
match &msg_events[0] {
6471+
MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
6472+
other => panic!("Expected HandleError, got {:?}", other),
6473+
}
6474+
}
6475+
6476+
#[test]
6477+
fn test_tx_init_rbf_before_quiescence_sends_warning() {
6478+
// A misbehaving peer sends tx_init_rbf before quiescence is established. The receiver
6479+
// should send a warning and disconnect.
6480+
let chanmon_cfgs = create_chanmon_cfgs(2);
6481+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
6482+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
6483+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
6484+
6485+
let node_id_1 = nodes[1].node.get_our_node_id();
6486+
6487+
let initial_channel_value_sat = 100_000;
6488+
let (_, _, channel_id, _) =
6489+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
6490+
6491+
let added_value = Amount::from_sat(50_000);
6492+
provide_utxo_reserves(&nodes, 2, added_value * 2);
6493+
6494+
// Complete a splice-in so there's a pending splice to RBF.
6495+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
6496+
let (_splice_tx, _new_funding_script) =
6497+
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
6498+
6499+
// Node 0 initiates quiescence.
6500+
nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
6501+
let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
6502+
6503+
// Misbehaving node 1 sends tx_init_rbf before completing the STFU handshake.
6504+
let tx_init_rbf = msgs::TxInitRbf {
6505+
channel_id,
6506+
locktime: 0,
6507+
feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW + 25,
6508+
funding_output_contribution: Some(added_value.to_sat() as i64),
6509+
};
6510+
nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf);
6511+
6512+
// Node 0 should send a warning and disconnect.
6513+
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
6514+
assert_eq!(msg_events.len(), 1);
6515+
match &msg_events[0] {
6516+
MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
6517+
other => panic!("Expected HandleError, got {:?}", other),
6518+
}
6519+
6520+
// Clean up events from the splice setup.
6521+
nodes[0].node.get_and_clear_pending_events();
6522+
nodes[1].node.get_and_clear_pending_events();
6523+
}
6524+
64356525
#[test]
64366526
fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
64376527
// After several RBF attempts, the counterparty's RBF feerate must be high enough to

0 commit comments

Comments
 (0)