Skip to content

Commit 0b47b8b

Browse files
jkczyzclaude
andcommitted
Reject splicing a channel with a splice inherited from a prior LDK version
A pending splice negotiated before an upgrade from a prior LDK version (e.g. 0.2) returns without its feerate or our contribution: 0.2 persists neither and drops the odd TLVs that carry them. splice_channel derives the RBF feerate floor from the prior splice's feerate and assumed it was always present via a debug assertion, so attempting to splice such a channel tripped that assertion. Return a clean error instead, refusing to splice a channel whose pending splice lacks the metadata to reconstruct the prior request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 63953ce commit 0b47b8b

2 files changed

Lines changed: 100 additions & 5 deletions

File tree

lightning-tests/src/upgrade_downgrade_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ use lightning::ln::splicing_tests::*;
6161
use lightning::ln::types::ChannelId;
6262
use lightning::onion_message::packet::Packet;
6363
use lightning::sign::OutputSpender;
64+
use lightning::util::errors::APIError;
6465
use lightning::util::ser::{MaybeReadable, Writeable};
6566
use lightning::util::wallet_utils::WalletSourceSync;
6667

@@ -1016,4 +1017,88 @@ fn upgrade_single_splice_from_0_2() {
10161017
assert_eq!(splice.candidates.len(), 1);
10171018
assert_eq!(splice.candidates[0].contribution, None);
10181019
}
1020+
1021+
// The splice cannot be RBF'd: 0.2 persisted no feerate, so splice_channel refuses it.
1022+
let node_id_1 = nodes[1].node.get_our_node_id();
1023+
let err = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap_err();
1024+
let expected = format!(
1025+
"Channel {} has a pending splice from a prior LDK version and cannot be spliced again",
1026+
channel_id,
1027+
);
1028+
match err {
1029+
APIError::APIMisuseError { err } => assert_eq!(err, expected),
1030+
_ => panic!("unexpected error: {:?}", err),
1031+
}
1032+
}
1033+
1034+
#[test]
1035+
fn splice_inherited_across_0_2_cannot_be_rbfed() {
1036+
// Negotiate a contributory splice on current, downgrade to LDK 0.2, then upgrade back. LDK 0.2
1037+
// persists neither our contribution nor the splice feerate and does not retain the odd TLVs that
1038+
// carry them, so the splice returns to current without either. `splice_channel` needs the prior
1039+
// feerate to derive the RBF floor, so it refuses such a channel with a clean error rather than
1040+
// operating on incomplete state (which would otherwise trip a debug assertion that a pending
1041+
// splice always has a known feerate).
1042+
let chan_id_bytes;
1043+
let (v3_mgr, v3_mon);
1044+
{
1045+
let chanmon_cfgs = create_chanmon_cfgs(2);
1046+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1047+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1048+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1049+
let (_, _, channel_id, _) =
1050+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
1051+
chan_id_bytes = channel_id.0;
1052+
1053+
let added_value = Amount::from_sat(50_000);
1054+
provide_utxo_reserves(&nodes, 2, added_value * 2);
1055+
let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
1056+
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution);
1057+
mine_transaction(&nodes[0], &splice_tx);
1058+
mine_transaction(&nodes[1], &splice_tx);
1059+
1060+
v3_mgr = nodes[0].node.encode();
1061+
v3_mon = get_monitor!(nodes[0], channel_id).encode();
1062+
}
1063+
1064+
// Downgrade node 0 to LDK 0.2 and re-serialize there, stripping the contribution and feerate.
1065+
let (v2_mgr, v2_mon);
1066+
{
1067+
let mut chanmon_cfgs = lightning_0_2_utils::create_chanmon_cfgs(2);
1068+
chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true;
1069+
chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true;
1070+
let node_cfgs = lightning_0_2_utils::create_node_cfgs(2, &chanmon_cfgs);
1071+
let node_chanmgrs = lightning_0_2_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1072+
let nodes = lightning_0_2_utils::create_network(2, &node_cfgs, &node_chanmgrs);
1073+
let mut config = lightning_0_2_utils::test_default_channel_config();
1074+
config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
1075+
let mgr = lightning_0_2_utils::_reload_node(&nodes[0], config, &v3_mgr, &[&v3_mon[..]]);
1076+
assert_eq!(mgr.list_channels().len(), 1);
1077+
let v2_channel_id = lightning_0_2::ln::types::ChannelId(chan_id_bytes);
1078+
v2_mgr = mgr.encode();
1079+
v2_mon = get_monitor_0_2!(nodes[0], v2_channel_id).encode();
1080+
}
1081+
1082+
// Upgrade back to current and attempt to RBF the inherited splice.
1083+
let mut chanmon_cfgs = create_chanmon_cfgs(2);
1084+
chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true;
1085+
chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true;
1086+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1087+
let (persister, chain_mon, new_node);
1088+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1089+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1090+
let config = test_default_channel_config();
1091+
reload_node!(nodes[0], config, &v2_mgr, &[&v2_mon[..]], persister, chain_mon, new_node);
1092+
1093+
let channel_id = ChannelId(chan_id_bytes);
1094+
let node_id_1 = nodes[1].node.get_our_node_id();
1095+
let err = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap_err();
1096+
let expected = format!(
1097+
"Channel {} has a pending splice from a prior LDK version and cannot be spliced again",
1098+
channel_id,
1099+
);
1100+
match err {
1101+
APIError::APIMisuseError { err } => assert_eq!(err, expected),
1102+
_ => panic!("unexpected error: {:?}", err),
1103+
}
10191104
}

lightning/src/ln/channel.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12928,11 +12928,21 @@ where
1292812928
.as_ref()
1292912929
.map(|n| n.funding_feerate_sat_per_1000_weight())
1293012930
});
12931-
debug_assert!(
12932-
prev_feerate.is_some(),
12933-
"pending_splice should have last_funding_feerate or funding_negotiation",
12934-
);
12935-
let min_rbf_feerate = prev_feerate.map(min_rbf_feerate);
12931+
let prev_feerate = match prev_feerate {
12932+
Some(prev_feerate) => prev_feerate,
12933+
None => {
12934+
// The feerate and our contribution are only persisted by LDK 0.3+, so their
12935+
// absence means this splice was last written by an older version (negotiated
12936+
// there, or round-tripped 0.3 -> 0.2 -> 0.3) and cannot be RBF'd.
12937+
return Err(APIError::APIMisuseError {
12938+
err: format!(
12939+
"Channel {} has a pending splice from a prior LDK version and cannot be spliced again",
12940+
self.context.channel_id(),
12941+
),
12942+
});
12943+
},
12944+
};
12945+
let min_rbf_feerate = Some(min_rbf_feerate(prev_feerate));
1293612946
let prior = if pending_splice.last_funding_feerate_sat_per_1000_weight.is_some() {
1293712947
pending_splice.latest_contribution().cloned()
1293812948
} else {

0 commit comments

Comments
 (0)