Skip to content
Open
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
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ exclude = [
# worth it. Note that we only apply optimizations to dependencies, not workspace
# crates themselves.
# https://doc.rust-lang.org/cargo/reference/profiles.html#profile-selection
# Experimental `OP_TEMPLATEHASH` (BIP-446/448) support used by `option_htlcs_claim_tx` is not yet
# available in a released `bitcoin`; pull it from a fork that adds it. The BIP-446/448 commits are
# cherry-picked onto the same `bitcoin` patch release (0.32.101) that we otherwise depend on.
[patch.crates-io]
bitcoin = { git = "https://github.com/darosior/rust-bitcoin", branch = "bip448-0.32.4" }

[profile.dev.package."*"]
opt-level = 2

Expand Down
33 changes: 29 additions & 4 deletions lightning-types/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
//! (see [BOLT PR #1160](https://github.com/lightning/bolts/pull/1160) for more information).
//! - `HtlcHold` - requires/supports holding HTLCs and forwarding on receipt of an onion message
//! (see [BOLT-2](https://github.com/lightning/bolts/pull/989/files) for more information).
//! - `OptionHTLCsClaimTx` - requires/supports committing to a v3 claim transaction in the preimage
//! spend path of offered HTLC outputs, closing the last pinning gap (experimental).
//!
//! LDK knows about the following features, but does not support them:
//! - `AnchorsNonzeroFeeHtlcTx` - the initial version of anchor outputs, which was later found to be
Expand Down Expand Up @@ -167,8 +169,12 @@ mod sealed {
ZeroConf,
// Byte 7
Trampoline | SimpleClose | Splice,
// Byte 8 - 18
,,,,,,,,,,,
// Byte 8 - 12
,,,,,
// Byte 13
OptionHTLCsClaimTx,
// Byte 14 - 18
,,,,,
// Byte 19
HtlcHold,
]
Expand All @@ -192,8 +198,12 @@ mod sealed {
ZeroConf | Keysend,
// Byte 7
Trampoline | SimpleClose | Splice,
// Byte 8 - 18
,,,,,,,,,,,
// Byte 8 - 12
,,,,,
// Byte 13
OptionHTLCsClaimTx,
// Byte 14 - 18
,,,,,
// Byte 19
HtlcHold,
// Byte 20 - 31
Expand Down Expand Up @@ -259,6 +269,10 @@ mod sealed {
AnchorZeroFeeCommitments | SCIDPrivacy,
// Byte 6
ZeroConf,
// Byte 7 - 12
,,,,,,
// Byte 13
OptionHTLCsClaimTx,
]);

/// Defines a feature with the given bits for the specified [`Context`]s. The generated trait is
Expand Down Expand Up @@ -706,6 +720,17 @@ mod sealed {
// By default, allocate enough bytes to cover up to Splice. Update this as new features are
// added which we expect to appear commonly across contexts.
pub(super) const MIN_FEATURES_ALLOCATION_BYTES: usize = 63_usize.div_ceil(8);
define_feature!(
111,
OptionHTLCsClaimTx,
[InitContext, NodeContext, ChannelTypeContext],
"Feature flags for `option_htlcs_claim_tx`.",
set_htlcs_claim_tx_optional,
set_htlcs_claim_tx_required,
clear_htlcs_claim_tx,
supports_htlcs_claim_tx,
requires_htlcs_claim_tx
);
define_feature!(
153, // The BOLTs PR uses feature bit 52/53, so add +100 for the experimental bit
HtlcHold,
Expand Down
36 changes: 35 additions & 1 deletion lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4648,6 +4648,30 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
tx_lock_time,
}));
}
ClaimEvent::BumpHTLCsClaimTx {
target_feerate_sat_per_1000_weight, claim_tx, channel_parameters,
} => {
let channel_id = self.channel_id;
let counterparty_node_id = self.counterparty_node_id;
let channel_value_satoshis = channel_parameters.channel_value_satoshis;
// The claim transaction's single output (a P2WPKH to our payment point) is what
// the fee-paying child spends to bump the package fee.
let claim_output_descriptor = StaticPaymentOutputDescriptor {
outpoint: OutPoint { txid: claim_tx.compute_txid(), index: 0 },
output: claim_tx.output[0].clone(),
channel_keys_id: self.channel_keys_id,
channel_value_satoshis,
channel_transaction_parameters: Some(channel_parameters),
};
ret.push(Event::BumpTransaction(BumpTransactionEvent::HTLCsClaimTxResolution {
channel_id,
counterparty_node_id,
claim_id,
package_target_feerate_sat_per_1000_weight: target_feerate_sat_per_1000_weight,
claim_tx,
claim_output_descriptor,
}));
}
}
}
ret
Expand Down Expand Up @@ -6218,7 +6242,17 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {

let mut payment_preimage = PaymentPreimage([0; 32]);
if offered_preimage_claim || accepted_preimage_claim {
payment_preimage.0.copy_from_slice(input.witness.second_to_last().unwrap());
// For legacy P2WSH HTLC claims the 32-byte preimage is the second-to-last witness
// element. For `option_htlcs_claim_tx` templated (P2TR script-path) offered-HTLC
// claims the witness is `[preimage, htlc_success_script, control_block]`, so the
// preimage is instead the first element.
let second_to_last = input.witness.second_to_last().unwrap();
let preimage = if second_to_last.len() == 32 {
second_to_last
} else {
input.witness.nth(0).unwrap()
};
payment_preimage.0.copy_from_slice(preimage);
}

macro_rules! log_claim {
Expand Down
43 changes: 42 additions & 1 deletion lightning/src/chain/onchaintx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ pub(crate) enum ClaimEvent {
htlcs: Vec<HTLCDescriptor>,
tx_lock_time: LockTime,
},
/// Event yielded to signal that an `option_htlcs_claim_tx` offered HTLC must be resolved by
/// broadcasting the fixed, zero-fee, template-committed v3 claim transaction alongside a
/// fee-paying child (a TRUC 1-parent-1-child package).
BumpHTLCsClaimTx {
target_feerate_sat_per_1000_weight: u32,
/// The fully-signed, template-committed, zero-fee v3 HTLC claim transaction to be confirmed
/// via a fee-paying child.
claim_tx: Transaction,
channel_parameters: ChannelTransactionParameters,
},
}

/// Represents the different ways an output can be claimed (i.e., spent to an address under our
Expand Down Expand Up @@ -740,8 +750,35 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
None => Some((new_timer, 0, OnchainClaim::Tx(MaybeSignedTransaction(tx)))),
}
},
// `option_htlcs_claim_tx`: the offered HTLC is resolved by broadcasting the fixed,
// zero-fee, template-committed v3 claim transaction. Since it pays no fee, it can
// only be relayed and confirmed alongside a fee-paying child (TRUC 1P1C), which we
// request from the user through a `BumpHTLCsClaimTx` event.
PackageSolvingData::CounterpartyOfferedHTLCOutput(output) => {
debug_assert!(output.channel_type_features().supports_htlcs_claim_tx());
let claim_tx = cached_request.maybe_finalize_untractable_package(self, logger)?;
if !claim_tx.is_fully_signed() {
// We couldn't sign the claim transaction as the signer was unavailable, but
// we should still retry it later. We return the unsigned transaction anyway
// to register the claim.
return Some((new_timer, 0, OnchainClaim::Tx(claim_tx)));
}
let target_feerate_sat_per_1000_weight = cached_request
.compute_package_feerate(fee_estimator, conf_target, feerate_strategy);
let channel_parameters = output.channel_parameters()
.unwrap_or(self.channel_parameters()).clone();
Some((
new_timer,
target_feerate_sat_per_1000_weight as u64,
OnchainClaim::Event(ClaimEvent::BumpHTLCsClaimTx {
target_feerate_sat_per_1000_weight,
claim_tx: claim_tx.0,
channel_parameters,
}),
))
},
_ => {
debug_assert!(false, "Only HolderFundingOutput inputs should be untractable and require external funding");
debug_assert!(false, "Only HolderFundingOutput and option_htlcs_claim_tx offered HTLC inputs should be untractable and require external funding");
None
},
})
Expand Down Expand Up @@ -909,6 +946,10 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
// underlying set of HTLCs changes.
ClaimId::from_htlcs(htlcs)
},
ClaimEvent::BumpHTLCsClaimTx { ref claim_tx, .. } =>
// The template-committed claim transaction spends a single HTLC
// output, so its txid is unique per request.
ClaimId(claim_tx.compute_txid().to_byte_array()),
};
debug_assert!(self.pending_claim_requests.get(&claim_id).is_none());
debug_assert_eq!(self.pending_claim_events.iter().filter(|entry| entry.0 == claim_id).count(), 0);
Expand Down
82 changes: 80 additions & 2 deletions lightning/src/chain/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub(crate) fn verify_channel_type_features(channel_type_features: &Option<Channe
supported_feature_set.set_scid_privacy_required();
supported_feature_set.set_zero_conf_required();
supported_feature_set.set_anchor_zero_fee_commitments_required();
supported_feature_set.set_htlcs_claim_tx_required();

// allow the passing of an additional necessary permitted flag
if let Some(additional_permitted_features) = additional_permitted_features {
Expand Down Expand Up @@ -293,6 +294,62 @@ impl CounterpartyOfferedHTLCOutput {
outpoint_confirmation_height,
}
}

/// The channel type features of the channel this offered HTLC output belongs to.
pub(crate) fn channel_type_features(&self) -> &ChannelTypeFeatures {
&self.channel_type_features
}

/// The channel parameters of the channel this offered HTLC output belongs to, if known. May be
/// `None` for outputs deserialized from monitors written before LDK 0.2.
pub(crate) fn channel_parameters(&self) -> Option<&ChannelTransactionParameters> {
self.channel_parameters.as_ref()
}

/// Builds the templated v3 HTLC claim transaction spending this offered HTLC output via the
/// preimage (`htlc_success`) path, as required by `option_htlcs_claim_tx`.
///
/// Unlike the malleable sweep used for other offered HTLC outputs, this transaction is fixed
/// (a single input spending `outpoint`, a single zero-fee P2WPKH output for the full HTLC
/// value), broadcast verbatim, and committed to via `OP_TEMPLATEHASH`. It carries no
/// counterparty signature: satisfaction is the preimage plus the leaf script.
///
/// Because the claim transaction pays zero fees, it can only be relayed and confirmed alongside
/// a fee-paying child spending its P2WPKH output (a TRUC 1-parent-1-child package). The
/// `OnchainTxHandler` therefore yields it as a `ClaimEvent::BumpHTLCsClaimTx` (surfaced to the
/// user as a `BumpTransactionEvent::HTLCsClaimTxResolution`) rather than broadcasting it on its
/// own.
#[rustfmt::skip]
pub(crate) fn get_maybe_signed_htlcs_claim_tx<Signer: EcdsaChannelSigner>(
&self, onchain_handler: &mut OnchainTxHandler<Signer>, outpoint: &BitcoinOutPoint,
) -> Option<MaybeSignedTransaction> {
let channel_parameters = onchain_handler.channel_parameters();
let channel_parameters = self.channel_parameters.as_ref().unwrap_or(channel_parameters);
debug_assert!(channel_parameters.channel_type_features.supports_htlcs_claim_tx());
let directed_parameters = channel_parameters.as_counterparty_broadcastable();
let chan_keys = TxCreationKeys::from_channel_static_keys(
&self.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
);
let countersignatory_payment_point =
&directed_parameters.countersignatory_pubkeys().payment_point;

let mut claim_tx = chan_utils::build_htlc_claim_transaction(
*outpoint, &self.htlc, countersignatory_payment_point,
);
let spend_info = chan_utils::offered_htlc_taproot_spend_info(
&self.htlc, &chan_keys.revocation_key, &chan_keys.broadcaster_htlc_key,
&chan_keys.countersignatory_htlc_key, countersignatory_payment_point,
);
let (_htlc_timeout, htlc_success) = chan_utils::offered_htlc_tapscript_leaves(
&self.htlc, &chan_keys.broadcaster_htlc_key, &chan_keys.countersignatory_htlc_key,
countersignatory_payment_point,
);
claim_tx.input[0].witness = chan_utils::build_htlcs_claim_tx_witness(
&self.preimage, &spend_info, &htlc_success,
);
Some(MaybeSignedTransaction(claim_tx))
}
}

impl Writeable for CounterpartyOfferedHTLCOutput {
Expand Down Expand Up @@ -941,6 +998,9 @@ impl PackageSolvingData {
directed_parameters.broadcaster_pubkeys().htlc_basepoint,
outp.counterparty_htlc_base_key,
);
// `option_htlcs_claim_tx` offered HTLC outputs are untractable (resolved by
// broadcasting the fixed HTLC claim transaction) and so are never finalized here.
debug_assert!(!channel_parameters.channel_type_features.supports_htlcs_claim_tx());
let chan_keys = TxCreationKeys::from_channel_static_keys(
&outp.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
Expand Down Expand Up @@ -1000,6 +1060,11 @@ impl PackageSolvingData {
PackageSolvingData::HolderFundingOutput(ref outp) => {
Some(outp.get_maybe_signed_commitment_tx(onchain_handler))
}
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => {
// Only `option_htlcs_claim_tx` offered HTLC outputs are untractable and reach here.
debug_assert!(outp.channel_type_features.supports_htlcs_claim_tx());
outp.get_maybe_signed_htlcs_claim_tx(onchain_handler, outpoint)
}
_ => { panic!("API Error!"); }
}
}
Expand Down Expand Up @@ -1043,8 +1108,16 @@ impl PackageSolvingData {
PackageMalleability::Malleable(AggregationCluster::Pinnable)
}
},
PackageSolvingData::CounterpartyOfferedHTLCOutput(..) =>
PackageMalleability::Malleable(AggregationCluster::Unpinnable),
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => {
if outp.channel_type_features.supports_htlcs_claim_tx() {
// `option_htlcs_claim_tx`: the output is resolved by broadcasting a fixed,
// templated zero-fee claim transaction (fee-bumped by a child). It is committed
// to via `OP_TEMPLATEHASH` and so cannot be aggregated or RBF-bumped.
PackageMalleability::Untractable
} else {
PackageMalleability::Malleable(AggregationCluster::Unpinnable)
}
},
PackageSolvingData::CounterpartyReceivedHTLCOutput(..) =>
PackageMalleability::Malleable(AggregationCluster::Pinnable),
PackageSolvingData::HolderHTLCOutput(ref outp) => {
Expand Down Expand Up @@ -1581,6 +1654,11 @@ impl PackageTemplate {
outp.channel_type_features.supports_anchors_zero_fee_htlc_tx()
|| outp.channel_type_features.supports_anchor_zero_fee_commitments()
},
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => {
// `option_htlcs_claim_tx` offered HTLC outputs are resolved by broadcasting a
// fixed, zero-fee claim transaction that must be fee-bumped via a child (CPFP).
outp.channel_type_features.supports_htlcs_claim_tx()
},
_ => false,
}).is_some()
}
Expand Down
Loading