Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions lightning/src/ln/blake2b_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,176 @@ fn test_pay_bolt11_invoice_between_blake2b_nodes() {
};
claim_payment(&nodes[0], &[&nodes[1]], preimage);
}

#[test]
fn test_fundee_fails_coinbase_funded_channel() {
// Under BLAKE2b proof of work a spend of a coinbase output does not relay for far longer than
// its maturity, so the fundee fails such a channel once the funding confirms (BOLT 2).
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let config = unified_config(true);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let node_a_id = nodes[0].node.get_our_node_id();
let node_b_id = nodes[1].node.get_our_node_id();

nodes[0].node.create_channel(node_b_id, 100000, 10001, 42, None, None).unwrap();
let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel);
let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
nodes[0].node.handle_accept_channel(node_b_id, &accept_channel);

let (channel_id, tx, _) =
create_coinbase_funding_transaction(&nodes[0], &node_b_id, 100000, 42);
// Our own checked funding path refuses a coinbase (see `test_no_coinbase_funding`), so fund
// through the manual path, as a funder that does not follow these rules could.
let outpoint = crate::chain::transaction::OutPoint { txid: tx.compute_txid(), index: 0 };
nodes[0]
.node
.unsafe_manual_funding_transaction_generated(channel_id, node_b_id, outpoint)
.unwrap();
let funding_created = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id);
nodes[1].node.handle_funding_created(node_a_id, &funding_created);
check_added_monitors(&nodes[1], 1);
expect_channel_pending_event(&nodes[1], &node_a_id);
let funding_signed = get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id);
nodes[0].node.handle_funding_signed(node_b_id, &funding_signed);
check_added_monitors(&nodes[0], 1);
// The manual funding path also tells us when the funding is safe to broadcast.
let events = nodes[0].node.get_and_clear_pending_events();
assert!(events.iter().any(|ev| matches!(ev, Event::ChannelPending { .. })));
assert!(events.iter().any(|ev| matches!(ev, Event::FundingTxBroadcastSafe { .. })));

confirm_transaction_at(&nodes[1], &tx, 1);
let err = "Funding transaction is a coinbase, which cannot be spent until it matures";
check_closed_event(
&nodes[1],
1,
ClosureReason::ProcessingError { err: err.to_owned() },
&[node_a_id],
100000,
);
check_closed_broadcast(&nodes[1], 1, true);
check_added_monitors(&nodes[1], 1);
assert!(nodes[1].node.list_channels().is_empty());

// The rule is the fundee's: the funder keeps the channel when the coinbase confirms.
confirm_transaction_at(&nodes[0], &tx, 1);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
}

#[test]
fn test_no_splice_of_channel_without_unified_sigs() {
// A node following BLAKE2b neither splices a channel without option_unified_sigs nor accepts
// such a splice, answering it with tx_abort (BOLT 2).
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let legacy = test_default_channel_config();
let node_chanmgrs =
create_node_chanmgrs(2, &node_cfgs, &[Some(legacy.clone()), Some(legacy.clone())]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let node_a_id = nodes[0].node.get_our_node_id();
let node_b_id = nodes[1].node.get_our_node_id();

let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1);
assert!(!nodes[1].node.list_channels()[0]
.channel_type
.as_ref()
.unwrap()
.supports_unified_sigs());
// A contribution prepared before following BLAKE2b.
provide_utxo_reserves(&nodes, 1, bitcoin::Amount::from_sat(100_000));
let template = nodes[1].node.splice_channel(&channel_id, &node_a_id).unwrap();
let feerate = bitcoin::FeeRate::from_sat_per_kwu(
crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW as u64,
);
let wallet = crate::util::wallet_utils::WalletSync::new(
std::sync::Arc::clone(&nodes[1].wallet_source),
nodes[1].logger,
);
let contribution = template
.splice_in_sync(bitcoin::Amount::from_sat(50_000), feerate, bitcoin::FeeRate::MAX, &wallet)
.unwrap();

let mut follows = legacy;
follows.follow_blake2b = true;
nodes[1].node.set_current_config(follows);

match nodes[1].node.splice_channel(&channel_id, &node_a_id) {
Err(APIError::APIMisuseError { err }) => assert!(err.contains("option_unified_sigs")),
res => panic!("Wrong result {:?}", res.err()),
}
match nodes[1].node.funding_contributed(&channel_id, &node_a_id, contribution, None) {
Err(crate::ln::channelmanager::SpliceContributionError::NegotiationFailed {
reason: crate::events::NegotiationFailureReason::ContributionInvalid,
}) => {},
res => panic!("Wrong result {:?}", res),
}
let events = nodes[1].node.get_and_clear_pending_events();
assert!(events.iter().any(|ev| matches!(ev, Event::DiscardFunding { .. })));
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());

crate::ln::splicing_tests::initiate_splice_in(
&nodes[0],
&nodes[1],
channel_id,
bitcoin::Amount::from_sat(50_000),
);
let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_b_id);
nodes[1].node.handle_stfu(node_a_id, &stfu);
let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_a_id);
nodes[0].node.handle_stfu(node_b_id, &stfu);
let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_b_id);
nodes[1].node.handle_splice_init(node_a_id, &splice_init);
let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_a_id);
assert!(String::from_utf8_lossy(&tx_abort.data).contains("option_unified_sigs"));
nodes[0].node.handle_tx_abort(node_b_id, &tx_abort);
if let Some(MessageSendEvent::SendTxAbort { msg, .. }) = nodes[0]
.node
.get_and_clear_pending_msg_events()
.into_iter()
.find(|ev| matches!(ev, MessageSendEvent::SendTxAbort { .. }))
{
nodes[1].node.handle_tx_abort(node_a_id, &msg);
}
// The initiator releases its wallet inputs and learns why the splice failed; nothing else is
// left in flight on either side.
let events = nodes[0].node.get_and_clear_pending_events();
assert!(events.iter().any(|ev| matches!(ev, Event::DiscardFunding { .. })));
assert!(events.iter().any(|ev| matches!(ev, Event::SpliceNegotiationFailed { .. })));
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());

// The channel is still usable in both directions.
send_payment(&nodes[0], &[&nodes[1]], 10_000_000);
send_payment(&nodes[1], &[&nodes[0]], 1_000_000);
}

#[test]
fn test_no_coinbase_funding() {
// A node following BLAKE2b does not fund a channel from a coinbase transaction.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let config = unified_config(true);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let node_a_id = nodes[0].node.get_our_node_id();
let node_b_id = nodes[1].node.get_our_node_id();

nodes[0].node.create_channel(node_b_id, 100000, 10001, 42, None, None).unwrap();
let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel);
let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
nodes[0].node.handle_accept_channel(node_b_id, &accept_channel);

let (channel_id, tx, _) =
create_coinbase_funding_transaction(&nodes[0], &node_b_id, 100000, 42);
match nodes[0].node.funding_transaction_generated(channel_id, node_b_id, tx) {
Err(APIError::APIMisuseError { err }) => assert!(err.contains("coinbase")),
res => panic!("Wrong result {:?}", res),
}
let _ = nodes[0].node.get_and_clear_pending_events();
let _ = nodes[0].node.get_and_clear_pending_msg_events();
}
40 changes: 37 additions & 3 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ use crate::util::config::{
};
use crate::util::errors::APIError;
use crate::util::logger::{Logger, Record, WithContext};
use crate::util::scid_utils::{block_from_scid, scid_from_parts};
use crate::util::scid_utils::{block_from_scid, scid_from_parts, scid_predates_blake2b};
use crate::util::ser::{Iterable, Readable, ReadableArgs, RequiredWrapper, Writeable, Writer};
use crate::util::wallet_utils::{ConfirmedUtxo, Input};
use crate::{impl_readable_for_vec, impl_writeable_for_vec};
Expand Down Expand Up @@ -12558,6 +12558,19 @@ where
)?;

if is_funding_tx_confirmed {
// A spend of a coinbase output does not relay for far longer than the coinbase
// maturity under BLAKE2b proof of work, so no commitment transaction of a
// coinbase-funded channel could be broadcast while its HTLCs expire, so the fundee
// fails it (BOLT 2). A 0-conf channel, already in use on trust, is exempt.
if tx.is_coinbase()
&& user_config.follow_blake2b
&& !self.funding.is_outbound()
&& !self.funding.get_channel_type().supports_zero_conf()
&& self.context.minimum_depth.unwrap_or(0) > 0
{
let err = "Funding transaction is a coinbase, which cannot be spent until it matures";
return Err(ClosureReason::ProcessingError { err: err.to_owned() });
}
// If this is a coinbase transaction and not a 0-conf channel
// we should update our min_depth to 100 to handle coinbase maturity
if tx.is_coinbase() &&
Expand Down Expand Up @@ -12938,6 +12951,10 @@ where

let short_channel_id = self.funding.get_short_channel_id()
.ok_or(ChannelError::Ignore("Cannot get a ChannelAnnouncement if the channel has not been confirmed yet".to_owned()))?;
// A channel funded before the BLAKE2b activation is not announced (BOLT 7).
if scid_predates_blake2b(chain_hash, short_channel_id) {
return Err(ChannelError::Ignore("Channel was funded before the BLAKE2b activation".to_owned()));
}
let node_id = NodeId::from_pubkey(&node_signer.get_node_id(Recipient::Node)
.map_err(|_| ChannelError::Ignore("Failed to retrieve own public key".to_owned()))?);
let counterparty_node_id = NodeId::from_pubkey(&self.context.get_counterparty_node_id());
Expand Down Expand Up @@ -13964,9 +13981,10 @@ where

pub(crate) fn splice_init<ES: EntropySource, L: Logger>(
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
min_funding_satoshis: u64, logger: &L,
min_funding_satoshis: u64, follow_blake2b: bool, logger: &L,
) -> Result<msgs::SpliceAck, InteractiveTxMsgError> {
self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?;
self.refuse_splice_without_unified_sigs(follow_blake2b)?;

let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64);
let (queued_net_value, holder_balance) = self
Expand Down Expand Up @@ -14139,11 +14157,13 @@ where

pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>(
&mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey,
fee_estimator: &LowerBoundedFeeEstimator<F>, min_funding_satoshis: u64, logger: &L,
fee_estimator: &LowerBoundedFeeEstimator<F>, min_funding_satoshis: u64,
follow_blake2b: bool, logger: &L,
) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> {
let (holder_pubkeys, counterparty_funding_pubkey) = self
.validate_tx_init_rbf(msg, fee_estimator)
.map_err(|e| self.quiescent_negotiation_err(e))?;
self.refuse_splice_without_unified_sigs(follow_blake2b)?;

let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64);
let (queued_net_value, holder_balance) = self
Expand Down Expand Up @@ -15480,6 +15500,20 @@ where
was_quiescent
}

/// Following BLAKE2b, a splice of a channel without option_unified_sigs is answered with
/// `tx_abort` (BOLT 2). Called once the splice has been validated, so we are quiescent.
fn refuse_splice_without_unified_sigs(
&mut self, follow_blake2b: bool,
) -> Result<(), InteractiveTxMsgError> {
if follow_blake2b && !self.funding.get_channel_type().supports_unified_sigs() {
let err = ChannelError::Abort(AbortReason::InternalError(
"Splicing a channel without option_unified_sigs is not allowed",
));
return Err(self.quiescent_negotiation_err(err));
}
Ok(())
}

fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError {
if matches!(err, ChannelError::Abort(_)) {
debug_assert!(self.context.channel_state.is_quiescent());
Expand Down
45 changes: 43 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5013,7 +5013,7 @@ impl<
///
/// Initiating a splice requires that the channel counterparty supports splicing. Any
/// channel (no matter the type) can be spliced, as long as the counterparty is currently
/// connected.
/// connected, except that with [`UserConfig::follow_blake2b`] no channel can be spliced yet.
///
/// # Return Value
///
Expand Down Expand Up @@ -5059,6 +5059,18 @@ impl<
match peer_state.channel_by_id.entry(*channel_id) {
hash_map::Entry::Occupied(chan_phase_entry) => {
if let Some(chan) = chan_phase_entry.get().as_funded() {
// Following BLAKE2b, a channel without option_unified_sigs is not spliced: the
// splice would sign the previous funding output without the opt-in (BOLT 2).
if self.config.read().unwrap().follow_blake2b
&& !chan.funding.get_channel_type().supports_unified_sigs()
{
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} does not use option_unified_sigs, so it cannot be spliced",
channel_id
),
});
}
chan.splice_channel()
} else {
Err(APIError::ChannelUnavailable {
Expand Down Expand Up @@ -6673,6 +6685,14 @@ impl<
}
}

// A spend of a coinbase output does not relay until long after it matures under
// BLAKE2b proof of work, so no commitment transaction of the channel could be broadcast.
if self.config.read().unwrap().follow_blake2b && funding_transaction.is_coinbase() {
result = result.and(Err(APIError::APIMisuseError {
err: "A coinbase transaction cannot fund a channel".to_owned(),
}));
}

if funding_transaction.output.len() > u16::max_value() as usize {
result = result.and(Err(APIError::APIMisuseError {
err: "Transaction had more than 2^16 outputs, which is not supported"
Expand Down Expand Up @@ -7004,6 +7024,15 @@ impl<
match peer_state.channel_by_id.get_mut(channel_id) {
Some(channel) => match channel.as_funded_mut() {
Some(chan) => {
// See `splice_channel`: no splice of a channel without option_unified_sigs.
if self.config.read().unwrap().follow_blake2b
&& !chan.funding.get_channel_type().supports_unified_sigs()
{
result = Err(SpliceContributionError::NegotiationFailed {
reason: events::NegotiationFailureReason::ContributionInvalid,
});
return push_discard_funding(contribution);
}
let locktime = bitcoin::absolute::LockTime::from_consensus(
locktime.unwrap_or_else(|| self.current_best_block().height),
);
Expand Down Expand Up @@ -13710,7 +13739,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
))
},
hash_map::Entry::Occupied(mut chan_entry) => {
if self.config.read().unwrap().reject_inbound_splices {
// Following BLAKE2b, a splice of a channel without option_unified_sigs is answered with
// `tx_abort` by `splice_init` (BOLT 2), even when inbound splices are not accepted.
let follow_blake2b = self.config.read().unwrap().follow_blake2b;
let refuse_without_unified_sigs = follow_blake2b
&& chan_entry.get().as_funded().is_some_and(|chan| {
!chan.funding.get_channel_type().supports_unified_sigs()
});
if self.config.read().unwrap().reject_inbound_splices
&& !refuse_without_unified_sigs
{
let err = ChannelError::WarnAndDisconnect(
"Inbound channel splices are currently not allowed".to_owned(),
);
Expand All @@ -13724,6 +13762,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
&self.entropy_source,
&self.get_our_node_id(),
self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
follow_blake2b,
&self.logger,
) {
Ok(splice_ack_msg) => {
Expand Down Expand Up @@ -13776,12 +13815,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
hash_map::Entry::Occupied(mut chan_entry) => {
if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() {
let user_channel_id = funded_channel.context.get_user_id();
let follow_blake2b = self.config.read().unwrap().follow_blake2b;
match funded_channel.tx_init_rbf(
msg,
&self.entropy_source,
&self.get_our_node_id(),
&self.fee_estimator,
self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
follow_blake2b,
&self.logger,
) {
Ok(tx_ack_rbf_msg) => {
Expand Down
12 changes: 4 additions & 8 deletions lightning/src/routing/gossip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ use crate::util::indexed_map::{
Entry as IndexedMapEntry, IndexedMap, OccupiedEntry as IndexedMapOccupiedEntry,
};
use crate::util::logger::{Level, Logger};
use crate::util::scid_utils::{block_from_scid, scid_from_parts, MAX_SCID_BLOCK};
use crate::util::scid_utils::{
block_from_scid, scid_from_parts, scid_predates_blake2b, MAX_SCID_BLOCK,
};
use crate::util::ser::{MaybeReadable, Readable, ReadableArgs, RequiredWrapper, Writeable, Writer};
use crate::util::wakers::Future;

Expand Down Expand Up @@ -2159,13 +2161,7 @@ impl<L: Logger> NetworkGraph<L> {
/// A funding output from before the BLAKE2b activation may have been spent where we cannot see
/// it, so a channel funded there is not added to the graph from P2P gossip.
fn predates_blake2b(&self, short_channel_id: u64) -> bool {
// The fuzz seeds replay mainnet gossip for scids in the first blocks.
if cfg!(fuzzing) {
return false;
}
Network::from_chain_hash(self.chain_hash)
.and_then(|network| bitcoin::consensus::Params::new(network).blake2b_height)
.map_or(false, |height| block_from_scid(short_channel_id) < height)
scid_predates_blake2b(self.chain_hash, short_channel_id)
}

/// Update channel information from a received announcement.
Expand Down
Loading
Loading