ln: add incoming_accountable to PendingHTLCInfo
What changed, and why it matters
This commit adds a new boolean flag called incoming_accountable to an internal data structure (PendingHTLCInfo) used when routing Lightning payments. The flag records whether the node is expected to be 'accountable' for resolving an incoming payment on time. It is read from an optional TLV field in the incoming message and defaults to false when not present. The change is a straightforward plumbing/data-model update; it does not by itself fix or introduce a security flaw, but it is part of work that could affect how reputation and payment-timeout penalties are tracked in the future.
No immediate action required. Monitor follow-up commits that consume incoming_accountable to ensure the flag is validated against the expected channel counterparty and that false/absent semantics do not create edge cases in reputation scoring or HTLC timeout handling. Review test coverage for the new field once behavior depends on it.
Security signals we found
New persisted field related to HTLC accountability/reputation
Loss of semantic distinction between absent TLV and explicitly false TLV
Experimental field per code comment
No validation or authorization checks added around the new field
Serialization uses default_value false, preserving backward compatibility
Evidence from the diff
The patch extends PendingHTLCInfo with incoming_accountable: bool, persisted as TLV field 11 with default_value false. It updates constructors create_recv_pending_htlc_info and create_fwd_pending_htlc_info to accept/populate the field from msg.accountable.unwrap_or(false), and propagates the value when re-creating HTLC info for phantom payments. The commit explicitly accepts that it can no longer distinguish ‘TLV absent’ from ‘TLV present with false’. No logic currently acts on the field beyond storage and forwarding.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/onion_payment.rsPendingHTLCInfo struct and serializationcreate_recv_pending_htlc_infocreate_fwd_pending_htlc_infoPhantom payment re-forward pathInspect captured patch +15 / −6
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index f3399ff..6284ded 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -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<u64>,
+ /// 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
@@ -5249,7 +5252,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)
+ msg.accountable.unwrap_or(false), current_height)
},
onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => {
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
@@ -7375,6 +7378,7 @@ where
payment_hash,
outgoing_amt_msat,
outgoing_cltv_value,
+ incoming_accountable,
..
},
} = payment;
@@ -7473,6 +7477,7 @@ where
Some(phantom_shared_secret),
false,
None,
+ incoming_accountable,
current_height,
);
match create_res {
@@ -16248,6 +16253,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 {
@@ -19837,7 +19843,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!(); }
@@ -19860,7 +19866,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]
@@ -19885,7 +19891,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 2b1b3a1..6c841f5 100644
--- a/lightning/src/ln/onion_payment.rs
+++ b/lightning/src/ln/onion_payment.rs
@@ -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: msg.accountable.unwrap_or(false),
})
}
@@ -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<u64>, current_height: u32
+ counterparty_skimmed_fee_msat: Option<u64>, incoming_accountable: bool, current_height: u32
) -> Result<PendingHTLCInfo, InboundHTLCErr> {
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,
+ msg.accountable.unwrap_or(false), cur_height,
)?
}
})
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.