From 152ec421bef94e59c23ecfa103d420e28a8bc104 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 23 Oct 2025 10:15:14 -0400 Subject: [PATCH 1/5] ln: add experimental accountable signal to update_add_htlc --- lightning/src/ln/blinded_payment_tests.rs | 1 + lightning/src/ln/channel.rs | 1 + lightning/src/ln/functional_tests.rs | 1 + lightning/src/ln/htlc_reserve_unit_tests.rs | 5 +++++ lightning/src/ln/msgs.rs | 24 +++++++++++++++++++++ lightning/src/ln/onion_payment.rs | 1 + lightning/src/ln/payment_tests.rs | 1 + 7 files changed, 34 insertions(+) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index a902cfebd12..ddabf8e5d95 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1526,6 +1526,7 @@ fn update_add_msg( skimmed_fee_msat: None, blinding_point, hold_htlc: None, + accountable: None, } } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5b4ac4c0aa5..b32c3be7ec1 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9719,6 +9719,7 @@ where skimmed_fee_msat: htlc.skimmed_fee_msat, blinding_point: htlc.blinding_point, hold_htlc: htlc.hold_htlc, + accountable: None, }); } } diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index e2963dbeb09..58ef44c4939 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2270,6 +2270,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; nodes[0].node.handle_update_add_htlc(node_b_id, &update_add_htlc); } diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 86c95721d47..4c4fbada7dd 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -839,6 +839,7 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); @@ -1082,6 +1083,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; nodes[0].node.handle_update_add_htlc(node_b_id, &msg); @@ -1266,6 +1268,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); @@ -1650,6 +1653,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; for i in 0..50 { @@ -2256,6 +2260,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; nodes[1].node.handle_update_add_htlc(node_a_id, &msg); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8e230fab1d9..6dc5a2d38c3 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -768,6 +768,28 @@ pub struct UpdateAddHTLC { /// /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc pub hold_htlc: Option<()>, + /// An experimental field indicating whether the receiving node's reputation would be held + /// accountable for the timely resolution of the HTLC. + /// + /// Note that this field is [`experimental`] so should not be used for forwarding decisions. + /// + /// [`experimental`]: https://github.com/lightning/blips/blob/master/blip-0004.md + pub accountable: ExperimentalAccountable, +} + +/// Represents the value sent on the wire to signal experimental accountability. For historical +/// reasons the least significant three bits are used to represent the accountable signal's value, +/// though it is now only interpreted as a binary value. +pub type ExperimentalAccountable = Option; + +/// Converts a boolean accountable signal to its wire representation. +pub fn accountable_from_bool(value: bool) -> ExperimentalAccountable { + Some(if value { 7 } else { 0 }) +} + +/// Converts the accountable signal on the wire to a boolean signal. +pub fn accountable_into_bool(accountable: ExperimentalAccountable) -> bool { + accountable.is_some_and(|v| v == 7) } /// An [`onion message`] to be sent to or received from a peer. @@ -3373,6 +3395,7 @@ impl_writeable_msg!(UpdateAddHTLC, { // TODO: currently we may fail to read the `ChannelManager` if we write a new even TLV in this message // and then downgrade. Once this is fixed, update the type here to match BOLTs PR 989. (75537, hold_htlc, option), + (106823, accountable, option), }); impl LengthReadable for OnionMessage { @@ -5872,6 +5895,7 @@ mod tests { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, }; let encoded_value = update_add_htlc.encode(); let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d32144668701144760101010101010101010101010101010101010101010101010101010101010101000c89d4ff031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap(); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 1abe4330a25..2b1b3a1876c 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -814,6 +814,7 @@ mod tests { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: None, } } diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 6c982738a52..040bae22952 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5103,6 +5103,7 @@ fn peel_payment_onion_custom_tlvs() { onion_routing_packet, blinding_point: None, hold_htlc: None, + accountable: None, }; let peeled_onion = crate::ln::onion_payment::peel_payment_onion( &update_add, From 1da4e9e0d21f2ac5b102cfdf64dc29f260a82082 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 23 Oct 2025 14:08:03 -0400 Subject: [PATCH 2/5] ln: add incoming_accountable to PendingHTLCInfo Persist as a bool so that we don't need to use Option when we will just inevitably unwrap_or(false) the field. This means that we won't be able to distinguish between an incoming htlc that has no TLV set, and one that has the TLV set with a false value in it. We accept this loss of information for the sake of simplicity in the codebase. --- lightning/src/ln/channelmanager.rs | 18 ++++++++++++------ lightning/src/ln/onion_payment.rs | 9 ++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 72585d69f80..76309bd10bd 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -68,8 +68,8 @@ use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; use crate::ln::msgs::{ - BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError, - MessageSendEvent, + accountable_into_bool, BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, + DecodeError, LightningError, MessageSendEvent, }; use crate::ln::onion_payment::{ check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_recv_pending_htlc_info, @@ -427,6 +427,9 @@ pub struct PendingHTLCInfo { /// This is used to allow LSPs to take fees as a part of payments, without the sender having to /// shoulder them. pub skimmed_fee_msat: Option, + /// An experimental field indicating whether our node's reputation would be held accountable + /// for the timely resolution of the received HTLC. + pub incoming_accountable: bool, } #[derive(Clone, Debug)] // See FundedChannel::revoke_and_ack for why, tl;dr: Rust bug @@ -5100,7 +5103,7 @@ where let current_height: u32 = self.best_block.read().unwrap().height; create_recv_pending_htlc_info(decoded_hop, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, None, allow_underpay, msg.skimmed_fee_msat, - current_height) + accountable_into_bool(msg.accountable), current_height) }, onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => { create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) @@ -7222,6 +7225,7 @@ where payment_hash, outgoing_amt_msat, outgoing_cltv_value, + incoming_accountable, .. }, } = payment; @@ -7320,6 +7324,7 @@ where Some(phantom_shared_secret), false, None, + incoming_accountable, current_height, ); match create_res { @@ -15963,6 +15968,7 @@ impl_writeable_tlv_based!(PendingHTLCInfo, { (8, outgoing_cltv_value, required), (9, incoming_amt_msat, option), (10, skimmed_fee_msat, option), + (11, incoming_accountable, (default_value, false)), }); impl Writeable for HTLCFailureMsg { @@ -19398,7 +19404,7 @@ mod tests { if let Err(crate::ln::channelmanager::InboundHTLCErr { reason, .. }) = create_recv_pending_htlc_info(hop_data, [0; 32], PaymentHash([0; 32]), sender_intended_amt_msat - extra_fee_msat - 1, 42, None, true, Some(extra_fee_msat), - current_height) + false, current_height) { assert_eq!(reason, LocalHTLCFailureReason::FinalIncorrectHTLCAmount); } else { panic!(); } @@ -19421,7 +19427,7 @@ mod tests { let current_height: u32 = node[0].node.best_block.read().unwrap().height; assert!(create_recv_pending_htlc_info(hop_data, [0; 32], PaymentHash([0; 32]), sender_intended_amt_msat - extra_fee_msat, 42, None, true, Some(extra_fee_msat), - current_height).is_ok()); + false, current_height).is_ok()); } #[test] @@ -19446,7 +19452,7 @@ mod tests { custom_tlvs: Vec::new(), }, shared_secret: SharedSecret::from_bytes([0; 32]), - }, [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, current_height); + }, [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, false, current_height); // Should not return an error as this condition: // https://github.com/lightning/bolts/blob/4dcc377209509b13cf89a4b91fde7d478f5b46d8/04-onion-routing.md?plain=1#L334 diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 2b1b3a1876c..261ee9e589e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -15,7 +15,7 @@ use crate::ln::channelmanager::{ BlindedFailure, BlindedForward, HTLCFailureMsg, PendingHTLCInfo, PendingHTLCRouting, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::msgs; +use crate::ln::msgs::{self, accountable_into_bool}; use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason, ONION_DATA_LEN}; use crate::sign::{NodeSigner, Recipient}; @@ -267,6 +267,7 @@ pub(super) fn create_fwd_pending_htlc_info( outgoing_amt_msat: amt_to_forward, outgoing_cltv_value, skimmed_fee_msat: None, + incoming_accountable: accountable_into_bool(msg.accountable), }) } @@ -274,7 +275,7 @@ pub(super) fn create_fwd_pending_htlc_info( pub(super) fn create_recv_pending_htlc_info( hop_data: onion_utils::Hop, shared_secret: [u8; 32], payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>, allow_underpay: bool, - counterparty_skimmed_fee_msat: Option, current_height: u32 + counterparty_skimmed_fee_msat: Option, incoming_accountable: bool, current_height: u32 ) -> Result { let ( payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, onion_cltv_expiry, @@ -456,6 +457,7 @@ pub(super) fn create_recv_pending_htlc_info( outgoing_amt_msat: onion_amt_msat, outgoing_cltv_value: onion_cltv_expiry, skimmed_fee_msat: counterparty_skimmed_fee_msat, + incoming_accountable, }) } @@ -520,7 +522,8 @@ where let shared_secret = hop.shared_secret().secret_bytes(); create_recv_pending_htlc_info( hop, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, - None, allow_skimmed_fees, msg.skimmed_fee_msat, cur_height, + None, allow_skimmed_fees, msg.skimmed_fee_msat, + accountable_into_bool(msg.accountable), cur_height, )? } }) From 4a64e98751adffbd6fa20c7031a3fd1450818b94 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 23 Oct 2025 10:48:24 -0400 Subject: [PATCH 3/5] ln: add accountable signal to HTLCUpdateAwaitingACK::AddHTLC --- lightning/src/ln/channel.rs | 37 ++++++++++++++++++++++++++---- lightning/src/ln/channelmanager.rs | 3 +++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b32c3be7ec1..1880646ff62 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -462,6 +462,7 @@ enum HTLCUpdateAwaitingACK { skimmed_fee_msat: Option, blinding_point: Option, hold_htlc: Option<()>, + accountable: bool, }, ClaimHTLC { payment_preimage: PaymentPreimage, @@ -8384,7 +8385,7 @@ where skimmed_fee_msat, blinding_point, hold_htlc, - .. + accountable, } => { match self.send_htlc( amount_msat, @@ -8396,6 +8397,7 @@ where skimmed_fee_msat, blinding_point, hold_htlc.is_some(), + accountable, fee_estimator, logger, ) { @@ -12546,7 +12548,8 @@ where pub fn queue_add_htlc( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, - blinding_point: Option, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + blinding_point: Option, accountable: bool, + fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result<(), (LocalHTLCFailureReason, String)> where F::Target: FeeEstimator, @@ -12563,6 +12566,7 @@ where blinding_point, // This method is only called for forwarded HTLCs, which are never held at the next hop false, + accountable, fee_estimator, logger, ) @@ -12594,7 +12598,7 @@ where &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, mut force_holding_cell: bool, skimmed_fee_msat: Option, blinding_point: Option, hold_htlc: bool, - fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result where F::Target: FeeEstimator, @@ -12676,6 +12680,7 @@ where skimmed_fee_msat, blinding_point, hold_htlc: hold_htlc.then(|| ()), + accountable, }); return Ok(false); } @@ -12947,7 +12952,8 @@ where pub fn send_htlc_and_commit( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, - hold_htlc: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + hold_htlc: bool, accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, + logger: &L, ) -> Result, ChannelError> where F::Target: FeeEstimator, @@ -12963,6 +12969,7 @@ where skimmed_fee_msat, None, hold_htlc, + accountable, fee_estimator, logger, ); @@ -14630,6 +14637,8 @@ where Vec::with_capacity(holding_cell_htlc_update_count); let mut holding_cell_held_htlc_flags: Vec> = Vec::with_capacity(holding_cell_htlc_update_count); + let mut holding_cell_accountable_flags: Vec = + Vec::with_capacity(holding_cell_htlc_update_count); // Vec of (htlc_id, failure_code, sha256_of_onion) let mut malformed_htlcs: Vec<(u64, u16, [u8; 32])> = Vec::new(); (holding_cell_htlc_update_count as u64).write(writer)?; @@ -14644,6 +14653,7 @@ where blinding_point, skimmed_fee_msat, hold_htlc, + accountable, } => { 0u8.write(writer)?; amount_msat.write(writer)?; @@ -14655,6 +14665,7 @@ where holding_cell_skimmed_fees.push(skimmed_fee_msat); holding_cell_blinding_points.push(blinding_point); holding_cell_held_htlc_flags.push(hold_htlc); + holding_cell_accountable_flags.push(accountable); }, &HTLCUpdateAwaitingACK::ClaimHTLC { ref payment_preimage, @@ -14915,6 +14926,7 @@ where (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 (71, holder_commitment_point_previous_revoked, option), // Added in 0.3 (73, holder_commitment_point_last_revoked, option), // Added in 0.3 + (75, holding_cell_accountable_flags, optional_vec), // Added in 0.3 }); Ok(()) @@ -15101,6 +15113,7 @@ where skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: false, }, 1 => HTLCUpdateAwaitingACK::ClaimHTLC { payment_preimage: Readable::read(reader)?, @@ -15302,6 +15315,7 @@ where let mut pending_outbound_held_htlc_flags_opt: Option>> = None; let mut holding_cell_held_htlc_flags_opt: Option>> = None; + let mut holding_cell_accountable: Option> = None; read_tlv_fields!(reader, { (0, announcement_sigs, option), @@ -15351,6 +15365,7 @@ where (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 (71, holder_commitment_point_previous_revoked_opt, option), // Added in 0.3 (73, holder_commitment_point_last_revoked_opt, option), // Added in 0.3 + (75, holding_cell_accountable, optional_vec), // Added in 0.3 }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -15475,6 +15490,19 @@ where } } + if let Some(accountable_htlcs) = holding_cell_accountable { + let mut iter = accountable_htlcs.into_iter(); + for htlc in holding_cell_htlc_updates.iter_mut() { + if let HTLCUpdateAwaitingACK::AddHTLC { ref mut accountable, .. } = htlc { + *accountable = iter.next().ok_or(DecodeError::InvalidValue)?; + } + } + // We expect all accountable HTLC signals to be consumed above + if iter.next().is_some() { + return Err(DecodeError::InvalidValue); + } + } + if let Some(attribution_data_list) = removed_htlc_attribution_data { let mut removed_htlcs = pending_inbound_htlcs.iter_mut().filter_map(|status| { if let InboundHTLCState::LocalRemoved(reason) = &mut status.state { @@ -16559,6 +16587,7 @@ mod tests { skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, + accountable: false, }; let dummy_holding_cell_claim_htlc = |attribution_data| HTLCUpdateAwaitingACK::ClaimHTLC { payment_preimage: PaymentPreimage([42; 32]), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 76309bd10bd..44a1d73309c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5330,6 +5330,7 @@ where onion_packet, None, hold_htlc_at_next_hop, + false, // Not accountable by default for sender. &self.fee_estimator, &&logger, ); @@ -7434,6 +7435,7 @@ where outgoing_cltv_value, routing, skimmed_fee_msat, + incoming_accountable, .. }, .. @@ -7534,6 +7536,7 @@ where onion_packet.clone(), *skimmed_fee_msat, next_blinding_point, + *incoming_accountable, &self.fee_estimator, &&logger, ) { From d34f3b9f7cc6bb4b97504ea03ad3d4ca3b5d65e2 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 2 Dec 2025 09:21:59 -0500 Subject: [PATCH 4/5] ln: add accountable signal to OutboundHTLCOutput --- lightning/src/ln/channel.rs | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 1880646ff62..73505e6bdf1 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -62,7 +62,7 @@ use crate::ln::interactivetxs::{ InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT, }; -use crate::ln::msgs; +use crate::ln::msgs::{self, accountable_from_bool}; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; use crate::ln::onion_utils::{ AttributionData, HTLCFailReason, LocalHTLCFailureReason, HOLD_TIME_UNIT_MILLIS, @@ -444,6 +444,7 @@ struct OutboundHTLCOutput { skimmed_fee_msat: Option, send_timestamp: Option, hold_htlc: Option<()>, + accountable: bool, } /// See AwaitingRemoteRevoke ChannelState for more info @@ -9721,7 +9722,7 @@ where skimmed_fee_msat: htlc.skimmed_fee_msat, blinding_point: htlc.blinding_point, hold_htlc: htlc.hold_htlc, - accountable: None, + accountable: accountable_from_bool(htlc.accountable), }); } } @@ -12703,6 +12704,7 @@ where skimmed_fee_msat, send_timestamp, hold_htlc: hold_htlc.then(|| ()), + accountable, }); self.context.next_holder_htlc_id += 1; @@ -14583,6 +14585,7 @@ where let mut pending_outbound_skimmed_fees: Vec> = Vec::new(); let mut pending_outbound_blinding_points: Vec> = Vec::new(); let mut pending_outbound_held_htlc_flags: Vec> = Vec::new(); + let mut pending_outbound_accountable: Vec = Vec::new(); (self.context.pending_outbound_htlcs.len() as u64).write(writer)?; for htlc in self.context.pending_outbound_htlcs.iter() { @@ -14626,6 +14629,7 @@ where pending_outbound_skimmed_fees.push(htlc.skimmed_fee_msat); pending_outbound_blinding_points.push(htlc.blinding_point); pending_outbound_held_htlc_flags.push(htlc.hold_htlc); + pending_outbound_accountable.push(htlc.accountable); } let holding_cell_htlc_update_count = self.context.holding_cell_htlc_updates.len(); @@ -14927,6 +14931,7 @@ where (71, holder_commitment_point_previous_revoked, option), // Added in 0.3 (73, holder_commitment_point_last_revoked, option), // Added in 0.3 (75, holding_cell_accountable_flags, optional_vec), // Added in 0.3 + (77, pending_outbound_accountable, optional_vec), // Added in 0.3 }); Ok(()) @@ -15094,6 +15099,7 @@ where blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); } @@ -15316,6 +15322,7 @@ where let mut pending_outbound_held_htlc_flags_opt: Option>> = None; let mut holding_cell_held_htlc_flags_opt: Option>> = None; let mut holding_cell_accountable: Option> = None; + let mut pending_outbound_accountable: Option> = None; read_tlv_fields!(reader, { (0, announcement_sigs, option), @@ -15366,6 +15373,7 @@ where (71, holder_commitment_point_previous_revoked_opt, option), // Added in 0.3 (73, holder_commitment_point_last_revoked_opt, option), // Added in 0.3 (75, holding_cell_accountable, optional_vec), // Added in 0.3 + (77, pending_outbound_accountable, optional_vec), // Added in 0.3 }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -15502,7 +15510,16 @@ where return Err(DecodeError::InvalidValue); } } - + if let Some(accountable_htlcs) = pending_outbound_accountable { + let mut iter = accountable_htlcs.into_iter(); + for htlc in pending_outbound_htlcs.iter_mut() { + htlc.accountable = iter.next().ok_or(DecodeError::InvalidValue)?; + } + // We expect all accountable HTLC signals to be consumed above + if iter.next().is_some() { + return Err(DecodeError::InvalidValue); + } + } if let Some(attribution_data_list) = removed_htlc_attribution_data { let mut removed_htlcs = pending_inbound_htlcs.iter_mut().filter_map(|status| { if let InboundHTLCState::LocalRemoved(reason) = &mut status.state { @@ -16106,6 +16123,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); // Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass @@ -16561,6 +16579,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }; let mut pending_outbound_htlcs = vec![dummy_outbound_output.clone(); 10]; for (idx, htlc) in pending_outbound_htlcs.iter_mut().enumerate() { @@ -16959,6 +16978,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); let payment_preimage_3 = @@ -16974,6 +16994,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); let payment_preimage_4 = @@ -17389,6 +17410,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); chan.context.pending_outbound_htlcs.push(OutboundHTLCOutput { @@ -17402,6 +17424,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }); test_commitment!("304402207d0870964530f97b62497b11153c551dca0a1e226815ef0a336651158da0f82402200f5378beee0e77759147b8a0a284decd11bfd2bc55c8fafa41c134fe996d43c8", @@ -17643,6 +17666,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }), ); @@ -17706,6 +17730,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }), ); @@ -17788,6 +17813,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, } }), ); @@ -17844,6 +17870,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }), ); @@ -17880,6 +17907,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }, ), ); @@ -17917,6 +17945,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }, ), ); @@ -17954,6 +17983,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }, ), ); @@ -18014,6 +18044,7 @@ mod tests { blinding_point: None, send_timestamp: None, hold_htlc: None, + accountable: false, }), ); From 72c479cdec095ef734749b2870a5491daba219da Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 12 Nov 2025 16:31:57 -0500 Subject: [PATCH 5/5] ln/test: add test coverage for accountable signal propagation --- lightning/src/ln/accountable_tests.rs | 91 +++++++++++++++++++++++++++ lightning/src/ln/mod.rs | 2 + 2 files changed, 93 insertions(+) create mode 100644 lightning/src/ln/accountable_tests.rs diff --git a/lightning/src/ln/accountable_tests.rs b/lightning/src/ln/accountable_tests.rs new file mode 100644 index 00000000000..57b96b28bd1 --- /dev/null +++ b/lightning/src/ln/accountable_tests.rs @@ -0,0 +1,91 @@ +use crate::ln::channelmanager::{ + HTLCForwardInfo, PaymentId, PendingAddHTLCInfo, PendingHTLCInfo, RecipientOnionFields, Retry, +}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{accountable_from_bool, ChannelMessageHandler, ExperimentalAccountable}; +use crate::routing::router::{PaymentParameters, RouteParameters}; + +fn test_accountable_forwarding_with_override( + override_accountable: ExperimentalAccountable, expected_forwarded: bool, +) { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let _chan_ab = create_announced_chan_between_nodes(&nodes, 0, 1); + let _chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); + + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV), + 100_000, + ); + let onion_fields = RecipientOnionFields::secret_only(payment_secret); + let payment_id = PaymentId(payment_hash.0); + nodes[0] + .node + .send_payment(payment_hash, onion_fields, payment_id, route_params, Retry::Attempts(0)) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let updates_ab = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); + assert_eq!(updates_ab.update_add_htlcs.len(), 1); + let mut htlc_ab = updates_ab.update_add_htlcs[0].clone(); + assert_eq!(htlc_ab.accountable, accountable_from_bool(false)); + + // Override accountable value if requested + if let Some(override_value) = override_accountable { + htlc_ab.accountable = Some(override_value); + } + + nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &htlc_ab); + do_commitment_signed_dance(&nodes[1], &nodes[0], &updates_ab.commitment_signed, false, false); + expect_and_process_pending_htlcs(&nodes[1], false); + check_added_monitors(&nodes[1], 1); + + let updates_bc = get_htlc_update_msgs(&nodes[1], &nodes[2].node.get_our_node_id()); + assert_eq!(updates_bc.update_add_htlcs.len(), 1); + let htlc_bc = &updates_bc.update_add_htlcs[0]; + let expected_acountable_signal = accountable_from_bool(expected_forwarded); + assert_eq!( + htlc_bc.accountable, expected_acountable_signal, + "B -> C should have accountable = {:?}", + expected_acountable_signal + ); + + nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), htlc_bc); + do_commitment_signed_dance(&nodes[2], &nodes[1], &updates_bc.commitment_signed, false, false); + + // Accountable signal is not surfaced in PaymentClaimable, so we do our next-best and check + // that the received htlcs that will be processed has the signal set as we expect. We manually + // process pending update adds so that we can access the htlc in forward_htlcs. + nodes[2].node.test_process_pending_update_add_htlcs(); + { + let fwds_lock = nodes[2].node.forward_htlcs.lock().unwrap(); + let recvs = fwds_lock.get(&0).unwrap(); + assert_eq!(recvs.len(), 1); + match recvs[0] { + HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { + forward_info: PendingHTLCInfo { incoming_accountable, .. }, + .. + }) => { + assert_eq!(incoming_accountable, expected_forwarded) + }, + _ => panic!("Unexpected forward"), + } + } + + expect_and_process_pending_htlcs(&nodes[2], false); + check_added_monitors(&nodes[2], 0); + expect_payment_claimable!(nodes[2], payment_hash, payment_secret, 100_000); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); +} + +#[test] +fn test_accountable_signal() { + // Tests forwarding of accountable signal for various incoming signal values. + test_accountable_forwarding_with_override(None, false); + test_accountable_forwarding_with_override(Some(7), true); + test_accountable_forwarding_with_override(Some(3), false); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 9473142cfed..6ab11b968dc 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -61,6 +61,8 @@ pub use onion_utils::process_onion_failure; #[cfg(fuzzing)] pub use onion_utils::AttributionData; +#[cfg(test)] +mod accountable_tests; #[cfg(test)] #[allow(unused_mut)] mod async_payments_tests;