Skip to content

Commit e4b1c6b

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 2379686 commit e4b1c6b

File tree

2 files changed

+155
-67
lines changed

2 files changed

+155
-67
lines changed

lightning/src/ln/channel.rs

Lines changed: 65 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12686,9 +12686,7 @@ where
1268612686
}
1268712687

1268812688
/// Checks during handling splice_init
12689-
pub fn validate_splice_init(
12690-
&self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount,
12691-
) -> Result<FundingScope, ChannelError> {
12689+
pub fn validate_splice_init(&self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> {
1269212690
if self.holder_commitment_point.current_point().is_none() {
1269312691
return Err(ChannelError::WarnAndDisconnect(format!(
1269412692
"Channel {} commitment point needs to be advanced once before spliced",
@@ -12725,32 +12723,7 @@ where
1272512723
)));
1272612724
}
1272712725

12728-
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12729-
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
12730-
12731-
// Rotate the pubkeys using the prev_funding_txid as a tweak
12732-
let prev_funding_txid = self.funding.get_funding_txid();
12733-
let funding_pubkey = match prev_funding_txid {
12734-
None => {
12735-
debug_assert!(false);
12736-
self.funding.get_holder_pubkeys().funding_pubkey
12737-
},
12738-
Some(prev_funding_txid) => self
12739-
.context
12740-
.holder_signer
12741-
.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx),
12742-
};
12743-
let mut new_keys = self.funding.get_holder_pubkeys().clone();
12744-
new_keys.funding_pubkey = funding_pubkey;
12745-
12746-
Ok(FundingScope::for_splice(
12747-
&self.funding,
12748-
&self.context,
12749-
our_funding_contribution,
12750-
their_funding_contribution,
12751-
msg.funding_pubkey,
12752-
new_keys,
12753-
))
12726+
Ok(())
1275412727
}
1275512728

1275612729
fn validate_splice_contributions(
@@ -12891,18 +12864,45 @@ where
1289112864
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
1289212865
logger: &L,
1289312866
) -> Result<msgs::SpliceAck, InteractiveTxMsgError> {
12867+
self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?;
12868+
1289412869
let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64);
12895-
let (our_funding_contribution, holder_balance) = self
12870+
let (queued_net_value, holder_balance) = self
1289612871
.resolve_queued_contribution(feerate, logger)
1289712872
.map_err(|e| self.quiescent_negotiation_err(e))?;
1289812873

12899-
let splice_funding = self
12900-
.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))
12901-
.map_err(|e| self.quiescent_negotiation_err(e))?;
12874+
let our_funding_contribution = queued_net_value.unwrap_or(SignedAmount::ZERO);
12875+
let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis);
12876+
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
12877+
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
12878+
12879+
// Rotate the pubkeys using the prev_funding_txid as a tweak
12880+
let prev_funding_txid = self.funding.get_funding_txid();
12881+
let funding_pubkey = match prev_funding_txid {
12882+
None => {
12883+
debug_assert!(false);
12884+
self.funding.get_holder_pubkeys().funding_pubkey
12885+
},
12886+
Some(prev_funding_txid) => self
12887+
.context
12888+
.holder_signer
12889+
.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx),
12890+
};
12891+
let mut holder_pubkeys = self.funding.get_holder_pubkeys().clone();
12892+
holder_pubkeys.funding_pubkey = funding_pubkey;
12893+
12894+
let splice_funding = FundingScope::for_splice(
12895+
&self.funding,
12896+
&self.context,
12897+
our_funding_contribution,
12898+
their_funding_contribution,
12899+
msg.funding_pubkey,
12900+
holder_pubkeys,
12901+
);
1290212902

1290312903
// Adjust for the feerate and clone so we can store it for future RBF re-use.
1290412904
let (adjusted_contribution, our_funding_inputs, our_funding_outputs) =
12905-
if our_funding_contribution.is_some() {
12905+
if queued_net_value.is_some() {
1290612906
let adjusted_contribution = self
1290712907
.take_queued_funding_contribution()
1290812908
.expect("queued_funding_contribution was Some")
@@ -12913,7 +12913,6 @@ where
1291312913
} else {
1291412914
(None, Default::default(), Default::default())
1291512915
};
12916-
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1291712916

1291812917
log_info!(
1291912918
logger,
@@ -12956,9 +12955,8 @@ where
1295612955

1295712956
/// Checks during handling tx_init_rbf for an existing splice
1295812957
fn validate_tx_init_rbf<F: FeeEstimator>(
12959-
&self, msg: &msgs::TxInitRbf, our_funding_contribution: SignedAmount,
12960-
fee_estimator: &LowerBoundedFeeEstimator<F>,
12961-
) -> Result<FundingScope, ChannelError> {
12958+
&self, msg: &msgs::TxInitRbf, fee_estimator: &LowerBoundedFeeEstimator<F>,
12959+
) -> Result<(ChannelPublicKeys, PublicKey), ChannelError> {
1296212960
if self.holder_commitment_point.current_point().is_none() {
1296312961
return Err(ChannelError::WarnAndDisconnect(format!(
1296412962
"Channel {} commitment point needs to be advanced once before RBF",
@@ -13024,33 +13022,22 @@ where
1302413022
return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate));
1302513023
}
1302613024

13027-
let their_funding_contribution = match msg.funding_output_contribution {
13028-
Some(value) => SignedAmount::from_sat(value),
13029-
None => SignedAmount::ZERO,
13030-
};
13031-
13032-
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
13033-
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
13034-
1303513025
// Reuse funding pubkeys from the last negotiated candidate since all RBF candidates
1303613026
// for the same splice share the same funding output script.
13037-
let holder_pubkeys = last_candidate.get_holder_pubkeys().clone();
13038-
let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey();
13039-
13040-
Ok(FundingScope::for_splice(
13041-
&self.funding,
13042-
&self.context,
13043-
our_funding_contribution,
13044-
their_funding_contribution,
13045-
counterparty_funding_pubkey,
13046-
holder_pubkeys,
13027+
Ok((
13028+
last_candidate.get_holder_pubkeys().clone(),
13029+
*last_candidate.counterparty_funding_pubkey(),
1304713030
))
1304813031
}
1304913032

1305013033
pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>(
1305113034
&mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey,
1305213035
fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
1305313036
) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> {
13037+
let (holder_pubkeys, counterparty_funding_pubkey) = self
13038+
.validate_tx_init_rbf(msg, fee_estimator)
13039+
.map_err(|e| self.quiescent_negotiation_err(e))?;
13040+
1305413041
let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64);
1305513042
let (queued_net_value, holder_balance) = self
1305613043
.resolve_queued_contribution(feerate, logger)
@@ -13079,14 +13066,23 @@ where
1307913066
};
1308013067

1308113068
let our_funding_contribution = queued_net_value.or(prior_net_value);
13069+
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
1308213070

13083-
let rbf_funding = self
13084-
.validate_tx_init_rbf(
13085-
msg,
13086-
our_funding_contribution.unwrap_or(SignedAmount::ZERO),
13087-
fee_estimator,
13088-
)
13089-
.map_err(|e| self.quiescent_negotiation_err(e))?;
13071+
let their_funding_contribution = match msg.funding_output_contribution {
13072+
Some(value) => SignedAmount::from_sat(value),
13073+
None => SignedAmount::ZERO,
13074+
};
13075+
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
13076+
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
13077+
13078+
let rbf_funding = FundingScope::for_splice(
13079+
&self.funding,
13080+
&self.context,
13081+
our_funding_contribution,
13082+
their_funding_contribution,
13083+
counterparty_funding_pubkey,
13084+
holder_pubkeys,
13085+
);
1309013086

1309113087
// Consume the appropriate contribution source.
1309213088
let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() {
@@ -13123,8 +13119,6 @@ where
1312313119
Default::default()
1312413120
};
1312513121

13126-
let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO);
13127-
1312813122
log_info!(
1312913123
logger,
1313013124
"Starting RBF funding negotiation for channel {} after receiving tx_init_rbf; channel value: {} sats",
@@ -14256,8 +14250,12 @@ where
1425614250
}
1425714251

1425814252
fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError {
14259-
let exited_quiescence =
14260-
if matches!(err, ChannelError::Abort(_)) { self.exit_quiescence() } else { false };
14253+
let exited_quiescence = if matches!(err, ChannelError::Abort(_)) {
14254+
debug_assert!(self.context.channel_state.is_quiescent());
14255+
self.exit_quiescence()
14256+
} else {
14257+
false
14258+
};
1426114259
InteractiveTxMsgError { err, splice_funding_failed: None, exited_quiescence }
1426214260
}
1426314261

lightning/src/ln/splicing_tests.rs

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

6586+
#[test]
6587+
fn test_splice_init_before_quiescence_sends_warning() {
6588+
// A misbehaving peer sends splice_init before quiescence is established. The receiver
6589+
// should send a warning and disconnect.
6590+
let chanmon_cfgs = create_chanmon_cfgs(2);
6591+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
6592+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
6593+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
6594+
6595+
let node_id_1 = nodes[1].node.get_our_node_id();
6596+
6597+
let initial_channel_value_sat = 100_000;
6598+
let (_, _, channel_id, _) =
6599+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
6600+
6601+
// Node 0 initiates quiescence.
6602+
nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
6603+
let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
6604+
6605+
// Misbehaving node 1 sends splice_init before completing the STFU handshake.
6606+
let funding_pubkey =
6607+
PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
6608+
let splice_init = msgs::SpliceInit {
6609+
channel_id,
6610+
funding_contribution_satoshis: 50_000,
6611+
funding_feerate_per_kw: FEERATE_FLOOR_SATS_PER_KW,
6612+
locktime: 0,
6613+
funding_pubkey,
6614+
require_confirmed_inputs: None,
6615+
};
6616+
nodes[0].node.handle_splice_init(node_id_1, &splice_init);
6617+
6618+
// Node 0 should send a warning and disconnect.
6619+
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
6620+
assert_eq!(msg_events.len(), 1);
6621+
match &msg_events[0] {
6622+
MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
6623+
other => panic!("Expected HandleError, got {:?}", other),
6624+
}
6625+
}
6626+
6627+
#[test]
6628+
fn test_tx_init_rbf_before_quiescence_sends_warning() {
6629+
// A misbehaving peer sends tx_init_rbf before quiescence is established. The receiver
6630+
// should send a warning and disconnect.
6631+
let chanmon_cfgs = create_chanmon_cfgs(2);
6632+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
6633+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
6634+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
6635+
6636+
let node_id_1 = nodes[1].node.get_our_node_id();
6637+
6638+
let initial_channel_value_sat = 100_000;
6639+
let (_, _, channel_id, _) =
6640+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
6641+
6642+
let added_value = Amount::from_sat(50_000);
6643+
provide_utxo_reserves(&nodes, 2, added_value * 2);
6644+
6645+
// Complete a splice-in so there's a pending splice to RBF.
6646+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
6647+
let (_splice_tx, _new_funding_script) =
6648+
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
6649+
6650+
// Node 0 initiates quiescence.
6651+
nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap();
6652+
let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
6653+
6654+
// Misbehaving node 1 sends tx_init_rbf before completing the STFU handshake.
6655+
let tx_init_rbf = msgs::TxInitRbf {
6656+
channel_id,
6657+
locktime: 0,
6658+
feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW + 25,
6659+
funding_output_contribution: Some(added_value.to_sat() as i64),
6660+
};
6661+
nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf);
6662+
6663+
// Node 0 should send a warning and disconnect.
6664+
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
6665+
assert_eq!(msg_events.len(), 1);
6666+
match &msg_events[0] {
6667+
MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1),
6668+
other => panic!("Expected HandleError, got {:?}", other),
6669+
}
6670+
6671+
// Clean up events from the splice setup.
6672+
nodes[0].node.get_and_clear_pending_events();
6673+
nodes[1].node.get_and_clear_pending_events();
6674+
}
6675+
65866676
#[test]
65876677
fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
65886678
// After several RBF attempts, the counterparty's RBF feerate must be high enough to

0 commit comments

Comments
 (0)