Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f99d519
ln/events: multiple htlcs in/out for trampoline PaymentForwarded
carlaKC Dec 16, 2025
68669ba
f Write legacy fields for backwards compatibility
carlaKC Jan 14, 2026
6bd32c6
f typos in PaymentForwarded doc and comment
carlaKC Jan 16, 2026
83ef21a
ln: make event optional in EmitEventAndFreeOtherChannel
carlaKC Dec 16, 2025
9a30965
f Move changelog downgrade note to pending_changelog
carlaKC Jan 12, 2026
2ec14f1
ln/refactor: rename EmitEventAndFreeOtherChannel to note optional event
carlaKC Jan 7, 2026
10bc106
ln+events: allow multiple prev_channel_id in HTLCHandlingFailed
carlaKC Jan 7, 2026
5e078bd
f write legacy channel_id in HTLCHandlingFailed for backwards compat
carlaKC Jan 14, 2026
875a757
events: add TrampolineForward variant to HTLCHandlingFailureType
carlaKC Jan 6, 2026
b0372d8
f typo in TrampolineForward doc
carlaKC Jan 16, 2026
5edc5f8
f use odd field for TrampolineForward to be forwards compatible
carlaKC Jan 16, 2026
f1d5bc4
ln: add TrampolineForward SendHTLCId variant
carlaKC Dec 2, 2025
5fce390
ln: add TrampolineForward variant to HTLCSource enum
a-mpch Aug 22, 2025
561b5d9
f store full path and use macros for htlc source persistence
carlaKC Jan 14, 2026
e6cf8bd
ln/refactor: add claim funds for htlc forward helper
carlaKC Dec 16, 2025
8dcf534
ln/refactor: pass closure to create PaymentForwarded event
carlaKC Jan 6, 2026
1aa4c7e
ln: add trampoline routing payment claiming
carlaKC Jan 6, 2026
4420fd8
ln/refactor: add blinded forwarding failure helper function
carlaKC Nov 20, 2025
606330e
ln: add trampoline routing failure handling
carlaKC Dec 1, 2025
c351239
ln/refactor: extract channelmonitor recovery to external helper
a-mpch Aug 25, 2025
17f432a
ln: add channel monitor recovery for trampoline forwards
a-mpch Aug 25, 2025
6a2421f
ln/refactor: move outgoing payment replay code into helper function
carlaKC Jan 15, 2026
2c52690
ln: handle trampoline claims on restart
carlaKC Jan 16, 2026
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
8 changes: 4 additions & 4 deletions lightning-liquidity/tests/lsps2_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1338,14 +1338,14 @@ fn client_trusts_lsp_end_to_end_test() {

let total_fee_msat = match service_events[0].clone() {
Event::PaymentForwarded {
prev_node_id,
next_node_id,
ref prev_htlcs,
ref next_htlcs,
skimmed_fee_msat,
total_fee_earned_msat,
..
} => {
assert_eq!(prev_node_id, Some(payer_node_id));
assert_eq!(next_node_id, Some(client_node_id));
assert_eq!(prev_htlcs[0].node_id, Some(payer_node_id));
assert_eq!(next_htlcs[0].node_id, Some(client_node_id));
service_handler.payment_forwarded(channel_id, skimmed_fee_msat.unwrap_or(0)).unwrap();
Some(total_fee_earned_msat.unwrap() - skimmed_fee_msat.unwrap())
},
Expand Down
2 changes: 2 additions & 0 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2868,6 +2868,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
let outbound_payment = match source {
None => panic!("Outbound HTLCs should have a source"),
Some(&HTLCSource::PreviousHopData(_)) => false,
Some(&HTLCSource::TrampolineForward { .. }) => false,
Some(&HTLCSource::OutboundRoute { .. }) => true,
};
return Some(Balance::MaybeTimeoutClaimableHTLC {
Expand Down Expand Up @@ -3080,6 +3081,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
let outbound_payment = match source {
None => panic!("Outbound HTLCs should have a source"),
Some(HTLCSource::PreviousHopData(_)) => false,
Some(HTLCSource::TrampolineForward { .. }) => false,
Some(HTLCSource::OutboundRoute { .. }) => true,
};
if outbound_payment {
Expand Down
179 changes: 113 additions & 66 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::util::ser::{
WithoutLength, Writeable, Writer,
};

use crate::io;
use crate::io::{self, ErrorKind::InvalidData as IOInvalidData};
use crate::sync::Arc;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
Expand Down Expand Up @@ -575,6 +575,14 @@ pub enum HTLCHandlingFailureType {
/// The payment hash of the payment we attempted to process.
payment_hash: PaymentHash,
},
/// We were responsible for pathfinding and forwarding of a trampoline payment, but failed to
/// do so. An example of such an instance is when we can't find a route to the specified
/// trampoline destination.
TrampolineForward {
/// The set of HTLCs dispatched by our node in an attempt to complete the trampoline forward
/// which have failed.
attempted_htlcs: Vec<HTLCLocator>,
},
}

impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType,
Expand All @@ -592,6 +600,9 @@ impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType,
(4, Receive) => {
(0, payment_hash, required),
},
(5, TrampolineForward) => {
(1, attempted_htlcs, required_vec),
},
);

/// The reason for HTLC failures in [`Event::HTLCHandlingFailed`].
Expand Down Expand Up @@ -729,6 +740,25 @@ pub enum InboundChannelFunds {
DualFunded,
}

/// Identifies the channel and peer committed to a HTLC, used for both incoming and outgoing HTLCs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HTLCLocator {
/// The channel that the HTLC was sent or received on.
pub channel_id: ChannelId,

/// The `user_channel_id` for `channel_id`.
pub user_channel_id: Option<u128>,

/// The public key identity of the node that the HTLC was sent to or received from.
pub node_id: Option<PublicKey>,
}

impl_writeable_tlv_based!(HTLCLocator, {
(1, channel_id, required),
(3, user_channel_id, option),
(5, node_id, option),
});

/// An Event which you should probably take some action in response to.
///
/// Note that while Writeable and Readable are implemented for Event, you probably shouldn't use
Expand Down Expand Up @@ -1312,38 +1342,22 @@ pub enum Event {
/// This event is generated when a payment has been successfully forwarded through us and a
/// forwarding fee earned.
///
/// Note that downgrading from 0.3 with pending trampoline forwards that use multipart payments
/// will produce an event that only provides information about the first htlc that was
/// received/dispatched.
///
/// # Failure Behavior and Persistence
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
/// returning `Err(ReplayEvent ())`) and will be persisted across restarts.
PaymentForwarded {
/// The channel id of the incoming channel between the previous node and us.
///
/// This is only `None` for events generated or serialized by versions prior to 0.0.107.
prev_channel_id: Option<ChannelId>,
/// The channel id of the outgoing channel between the next node and us.
///
/// This is only `None` for events generated or serialized by versions prior to 0.0.107.
next_channel_id: Option<ChannelId>,
/// The `user_channel_id` of the incoming channel between the previous node and us.
///
/// This is only `None` for events generated or serialized by versions prior to 0.0.122.
prev_user_channel_id: Option<u128>,
/// The `user_channel_id` of the outgoing channel between the next node and us.
///
/// This will be `None` if the payment was settled via an on-chain transaction. See the
/// caveat described for the `total_fee_earned_msat` field. Moreover it will be `None` for
/// events generated or serialized by versions prior to 0.0.122.
next_user_channel_id: Option<u128>,
/// The node id of the previous node.
///
/// This is only `None` for HTLCs received prior to 0.1 or for events serialized by
/// versions prior to 0.1
prev_node_id: Option<PublicKey>,
/// The node id of the next node.
///
/// This is only `None` for HTLCs received prior to 0.1 or for events serialized by
/// versions prior to 0.1
next_node_id: Option<PublicKey>,
/// The set of HTLCs forwarded to our node that will be claimed by this forward. Contains a
/// single HTLC for source-routed payments, and may contain multiple HTLCs when we acted as
/// a trampoline router, responsible for pathfinding within the route.
prev_htlcs: Vec<HTLCLocator>,
/// The set of HTLCs forwarded by our node that have been claimed by this forward. Contains
/// a single HTLC for regular source-routed payments, and may contain multiple HTLCs when
/// we acted as a trampoline router, responsible for pathfinding within the route.
next_htlcs: Vec<HTLCLocator>,
/// The total fee, in milli-satoshis, which was earned as a result of the payment.
///
/// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC
Expand Down Expand Up @@ -1660,12 +1674,17 @@ pub enum Event {
/// Indicates that the HTLC was accepted, but could not be processed when or after attempting to
/// forward it.
///
/// Note that downgrading from 0.3 with pending trampoline forwards that have incoming multipart
/// payments will produce an event that only provides information about the first htlc that was
/// received/dispatched.
///
/// # Failure Behavior and Persistence
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
/// returning `Err(ReplayEvent ())`) and will be persisted across restarts.
HTLCHandlingFailed {
/// The channel over which the HTLC was received.
prev_channel_id: ChannelId,
/// The channel(s) over which the HTLC(s) was received. May contain multiple entries for
/// trampoline forwards.
prev_channel_ids: Vec<ChannelId>,
/// The type of HTLC handling that failed.
failure_type: HTLCHandlingFailureType,
/// The reason that the HTLC failed.
Expand Down Expand Up @@ -2027,29 +2046,33 @@ impl Writeable for Event {
});
},
&Event::PaymentForwarded {
prev_channel_id,
next_channel_id,
prev_user_channel_id,
next_user_channel_id,
prev_node_id,
next_node_id,
ref prev_htlcs,
ref next_htlcs,
total_fee_earned_msat,
skimmed_fee_msat,
claim_from_onchain_tx,
outbound_amount_forwarded_msat,
} => {
7u8.write(writer)?;
// Fields 1, 3, 9, 11, 13 and 15 are written for backwards compatibility.
let legacy_prev = prev_htlcs.first().ok_or(io::Error::from(IOInvalidData))?;
let legacy_next = next_htlcs.first().ok_or(io::Error::from(IOInvalidData))?;
write_tlv_fields!(writer, {
(0, total_fee_earned_msat, option),
(1, prev_channel_id, option),
(1, Some(legacy_prev.channel_id), option),
(2, claim_from_onchain_tx, required),
(3, next_channel_id, option),
(3, Some(legacy_next.channel_id), option),
(5, outbound_amount_forwarded_msat, option),
(7, skimmed_fee_msat, option),
(9, prev_user_channel_id, option),
(11, next_user_channel_id, option),
(13, prev_node_id, option),
(15, next_node_id, option),
(9, legacy_prev.user_channel_id, option),
(11, legacy_next.user_channel_id, option),
(13, legacy_prev.node_id, option),
(15, legacy_next.node_id, option),
// HTLCs are written as required, rather than required_vec, so that they can be
// deserialized using default_value to fill in legacy fields which expects
// LengthReadable (required_vec is WithoutLength).
(17, *prev_htlcs, required),
(19, *next_htlcs, required),
});
},
&Event::ChannelClosed {
Expand Down Expand Up @@ -2197,15 +2220,19 @@ impl Writeable for Event {
})
},
&Event::HTLCHandlingFailed {
ref prev_channel_id,
ref prev_channel_ids,
ref failure_type,
ref failure_reason,
} => {
25u8.write(writer)?;
let legacy_chan_id =
prev_channel_ids.first().ok_or(io::Error::from(IOInvalidData))?;
write_tlv_fields!(writer, {
(0, prev_channel_id, required),
// Write legacy field to remain backwards compatible.
(0, legacy_chan_id, required),
(1, failure_reason, option),
(2, failure_type, required),
(3, *prev_channel_ids, required),
})
},
&Event::BumpTransaction(ref event) => {
Expand Down Expand Up @@ -2544,35 +2571,51 @@ impl MaybeReadable for Event {
},
7u8 => {
let mut f = || {
let mut prev_channel_id = None;
let mut next_channel_id = None;
let mut prev_user_channel_id = None;
let mut next_user_channel_id = None;
let mut prev_node_id = None;
let mut next_node_id = None;
// Legacy values that have been replaced by prev_htlcs and next_htlcs.
let mut prev_channel_id_legacy = None;
let mut next_channel_id_legacy = None;
let mut prev_user_channel_id_legacy = None;
let mut next_user_channel_id_legacy = None;
let mut prev_node_id_legacy = None;
let mut next_node_id_legacy = None;

let mut total_fee_earned_msat = None;
let mut skimmed_fee_msat = None;
let mut claim_from_onchain_tx = false;
let mut outbound_amount_forwarded_msat = None;
let mut prev_htlcs = vec![];
let mut next_htlcs = vec![];
read_tlv_fields!(reader, {
(0, total_fee_earned_msat, option),
(1, prev_channel_id, option),
(1, prev_channel_id_legacy, option),
(2, claim_from_onchain_tx, required),
(3, next_channel_id, option),
(3, next_channel_id_legacy, option),
(5, outbound_amount_forwarded_msat, option),
(7, skimmed_fee_msat, option),
(9, prev_user_channel_id, option),
(11, next_user_channel_id, option),
(13, prev_node_id, option),
(15, next_node_id, option),
(9, prev_user_channel_id_legacy, option),
(11, next_user_channel_id_legacy, option),
(13, prev_node_id_legacy, option),
(15, next_node_id_legacy, option),
// We can unwrap in the eagerly-evaluated default_value code because we
// always write legacy fields to be backwards compatible, and expect
// this field to be set because the legacy field was only None for versions
// before 0.0.107 and we do not allow upgrades with pending forwards to 0.1
// for any version before 0.0.123.
(17, prev_htlcs, (default_value, vec![HTLCLocator{
channel_id: prev_channel_id_legacy.unwrap(),
user_channel_id: prev_user_channel_id_legacy,
node_id: prev_node_id_legacy,
}])),
(19, next_htlcs, (default_value, vec![HTLCLocator{
channel_id: next_channel_id_legacy.unwrap(),
user_channel_id: next_user_channel_id_legacy,
node_id: next_node_id_legacy,
}])),
});

Ok(Some(Event::PaymentForwarded {
prev_channel_id,
next_channel_id,
prev_user_channel_id,
next_user_channel_id,
prev_node_id,
next_node_id,
prev_htlcs,
next_htlcs,
total_fee_earned_msat,
skimmed_fee_msat,
claim_from_onchain_tx,
Expand Down Expand Up @@ -2761,13 +2804,17 @@ impl MaybeReadable for Event {
},
25u8 => {
let mut f = || {
let mut prev_channel_id = ChannelId::new_zero();
let mut prev_channel_id_legacy = ChannelId::new_zero();
let mut failure_reason = None;
let mut failure_type_opt = UpgradableRequired(None);
let mut prev_channel_ids = vec![];
read_tlv_fields!(reader, {
(0, prev_channel_id, required),
(0, prev_channel_id_legacy, required),
(1, failure_reason, option),
(2, failure_type_opt, upgradable_required),
(3, prev_channel_ids, (default_value, vec![
prev_channel_id_legacy,
])),
});

// If a legacy HTLCHandlingFailureType::UnknownNextHop was written, upgrade
Expand All @@ -2782,7 +2829,7 @@ impl MaybeReadable for Event {
failure_reason = Some(LocalHTLCFailureReason::UnknownNextPeer.into());
}
Ok(Some(Event::HTLCHandlingFailed {
prev_channel_id,
prev_channel_ids,
failure_type: _init_tlv_based_struct_field!(
failure_type_opt,
upgradable_required
Expand Down
Loading