Skip to content

Commit e298854

Browse files
committed
Fix panic when calling batch_funding_transaction_generated early
If a user calls `batch_funding_transaction_generated` before a channel is ready to fund its possible to hit an `unwrap` when the transaction-scanning logic attempts to fetch the channel's expected output `scriptPubKey`. While users shouldn't be doing this, we should also avoid the panic, so here check the channel state first. Found by the `full_stack_target` fuzzer.
1 parent 399e6e7 commit e298854

File tree

3 files changed

+89
-32
lines changed

3 files changed

+89
-32
lines changed

lightning/src/ln/channel.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,22 @@ where
15651565
)
15661566
}
15671567

1568+
/// Returns true if this channel is waiting on a (batch) funding transaction to be provided.
1569+
///
1570+
/// If this method returns true, [`Self::into_unfunded_outbound_v1`] will also succeed.
1571+
pub fn ready_to_fund(&self) -> bool {
1572+
if !self.funding().is_outbound() {
1573+
return false;
1574+
}
1575+
match self.context().channel_state {
1576+
ChannelState::NegotiatingFunding(flags) => {
1577+
debug_assert!(matches!(self.phase, ChannelPhase::UnfundedOutboundV1(_)));
1578+
flags.is_our_init_sent() && flags.is_their_init_sent()
1579+
},
1580+
_ => false,
1581+
}
1582+
}
1583+
15681584
pub fn into_unfunded_outbound_v1(self) -> Result<OutboundV1Channel<SP>, Self> {
15691585
if let ChannelPhase::UnfundedOutboundV1(channel) = self.phase {
15701586
Ok(channel)

lightning/src/ln/channel_open_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2509,3 +2509,38 @@ pub fn test_funding_signed_event() {
25092509
nodes[0].node.get_and_clear_pending_msg_events();
25102510
nodes[1].node.get_and_clear_pending_msg_events();
25112511
}
2512+
2513+
#[xtest(feature = "_externalize_tests")]
2514+
fn test_fund_pending_channel() {
2515+
// Previously, we would panic if a user called `batch_funding_transaction_generated` for a
2516+
// channel which wasn't ready to receive funding. While this isn't a major bug - users
2517+
// shouldn't do that - we shouldn't panic and should instead return an error.
2518+
let chanmon_cfgs = create_chanmon_cfgs(2);
2519+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2520+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2521+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2522+
2523+
let node_b_id = nodes[1].node.get_our_node_id();
2524+
2525+
nodes[0].node.create_channel(node_b_id, 100_000, 0, 42, None, None).unwrap();
2526+
let open_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
2527+
2528+
let tx = Transaction {
2529+
version: Version(2),
2530+
lock_time: LockTime::ZERO,
2531+
input: vec![],
2532+
output: vec![],
2533+
};
2534+
let pending_chan = [(&open_msg.common_fields.temporary_channel_id, &node_b_id)];
2535+
let res = nodes[0].node.batch_funding_transaction_generated(&pending_chan, tx);
2536+
if let Err(APIError::APIMisuseError { err }) = res {
2537+
assert_eq!(err, "Channel f7fee84016d554015f5166c0a0df6479942ef55fd70713883b0493493a38e13a with counterparty 0355f8d2238a322d16b602bd0ceaad5b01019fb055971eaadcc9b29226a4da6c23 is not an unfunded, outbound channel ready to fund");
2538+
} else {
2539+
panic!("Unexpected result {res:?}");
2540+
}
2541+
get_err_msg(&nodes[0], &node_b_id);
2542+
let reason = ClosureReason::ProcessingError {
2543+
err: "Error in transaction funding: Misuse error: Channel f7fee84016d554015f5166c0a0df6479942ef55fd70713883b0493493a38e13a with counterparty 0355f8d2238a322d16b602bd0ceaad5b01019fb055971eaadcc9b29226a4da6c23 is not an unfunded, outbound channel ready to fund".to_owned(),
2544+
};
2545+
check_closed_event!(nodes[0], 1, reason, [node_b_id], 100_000);
2546+
}

lightning/src/ln/channelmanager.rs

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5608,42 +5608,48 @@ where
56085608
Err($api_err)
56095609
} } }
56105610

5611-
let funding_txo;
5612-
let (mut chan, msg_opt) = match peer_state.channel_by_id.remove(&temporary_channel_id)
5613-
.map(Channel::into_unfunded_outbound_v1)
5614-
{
5615-
Some(Ok(mut chan)) => {
5616-
match find_funding_output(&chan) {
5617-
Ok(found_funding_txo) => funding_txo = found_funding_txo,
5618-
Err(err) => {
5619-
let chan_err = ChannelError::close(err.to_owned());
5620-
let api_err = APIError::APIMisuseError { err: err.to_owned() };
5621-
return abandon_chan!(chan_err, api_err, chan);
5622-
},
5611+
let mut chan = match peer_state.channel_by_id.entry(temporary_channel_id) {
5612+
hash_map::Entry::Occupied(chan) => {
5613+
if !chan.get().ready_to_fund() {
5614+
return Err(APIError::APIMisuseError {
5615+
err: format!("Channel {temporary_channel_id} with counterparty {counterparty_node_id} is not an unfunded, outbound channel ready to fund"),
5616+
});
56235617
}
5624-
5625-
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
5626-
let funding_res = chan.get_funding_created(funding_transaction, funding_txo, is_batch_funding, &&logger);
5627-
match funding_res {
5628-
Ok(funding_msg) => (chan, funding_msg),
5629-
Err((mut chan, chan_err)) => {
5630-
let api_err = APIError::ChannelUnavailable { err: "Signer refused to sign the initial commitment transaction".to_owned() };
5631-
return abandon_chan!(chan_err, api_err, chan);
5632-
}
5618+
match chan.remove().into_unfunded_outbound_v1() {
5619+
Ok(chan) => chan,
5620+
Err(chan) => {
5621+
debug_assert!(false, "ready_to_fund guarantees into_unfunded_outbound_v1 will succeed");
5622+
peer_state.channel_by_id.insert(temporary_channel_id, chan);
5623+
return Err(APIError::APIMisuseError {
5624+
err: "Invalid state, please report this bug".to_owned(),
5625+
});
5626+
},
56335627
}
56345628
},
5635-
Some(Err(chan)) => {
5636-
peer_state.channel_by_id.insert(temporary_channel_id, chan);
5637-
return Err(APIError::APIMisuseError {
5638-
err: format!(
5639-
"Channel with id {} for the passed counterparty node_id {} is not an unfunded, outbound V1 channel",
5640-
temporary_channel_id, counterparty_node_id),
5641-
})
5629+
hash_map::Entry::Vacant(_) => {
5630+
return Err(APIError::ChannelUnavailable {
5631+
err: format!("Channel {temporary_channel_id} with counterparty {counterparty_node_id} not found"),
5632+
});
56425633
},
5643-
None => return Err(APIError::ChannelUnavailable {err: format!(
5644-
"Channel with id {} not found for the passed counterparty node_id {}",
5645-
temporary_channel_id, counterparty_node_id),
5646-
}),
5634+
};
5635+
5636+
let funding_txo = match find_funding_output(&chan) {
5637+
Ok(found_funding_txo) => found_funding_txo,
5638+
Err(err) => {
5639+
let chan_err = ChannelError::close(err.to_owned());
5640+
let api_err = APIError::APIMisuseError { err: err.to_owned() };
5641+
return abandon_chan!(chan_err, api_err, chan);
5642+
},
5643+
};
5644+
5645+
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
5646+
let funding_res = chan.get_funding_created(funding_transaction, funding_txo, is_batch_funding, &&logger);
5647+
let (mut chan, msg_opt) = match funding_res {
5648+
Ok(funding_msg) => (chan, funding_msg),
5649+
Err((mut chan, chan_err)) => {
5650+
let api_err = APIError::ChannelUnavailable { err: "Signer refused to sign the initial commitment transaction".to_owned() };
5651+
return abandon_chan!(chan_err, api_err, chan);
5652+
}
56475653
};
56485654

56495655
match peer_state.channel_by_id.entry(chan.context.channel_id()) {

0 commit comments

Comments
 (0)