ln: add experimental accountable signal to update_add_htlc
What changed, and why it matters
This commit adds a new experimental flag called 'accountable' to the Lightning update_add_htlc message. The flag is purely informational right now: it is sent and received, but the code does not use it to make any forwarding or security decisions. It is marked experimental and is not treated as authoritative. There is no obvious security vulnerability in the change itself.
No immediate action required. Monitor future commits to ensure the experimental 'accountable' field is not used for security-critical forwarding or reputation decisions before the specification and threat model are finalized. Review the custom AccountableBool decoding if it is later relied upon, because any non-7 byte decodes as false, which could be surprising.
Security signals we found
New experimental protocol TLV added to update_add_htlc
Custom boolean wire encoding (true=7, false=0) with non-standard truthiness semantics
No production logic consumes the new field
Extensive test-only message construction updated to include the new field
Evidence from the diff
The patch extends UpdateAddHTLC with an optional TLV field ‘accountable’ (type 106823) using a custom AccountableBool wrapper that encodes true as 0x07 and false as 0x00. All existing construction sites set it to None, and no production logic reads or acts on it. The commit adds unit tests for encoding/decoding. The field is documented as experimental and not to be used for forwarding decisions.
Changed components
lightning/src/ln/msgs.rslightning/src/ln/channel.rslightning/src/ln/onion_payment.rslightning/src/ln/blinded_payment_tests.rslightning/src/ln/functional_tests.rslightning/src/ln/htlc_reserve_unit_tests.rslightning/src/ln/payment_tests.rsInspect captured patch +119 / −1
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index 7941a81..914f536 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 128091c..6a05f15 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -9747,6 +9747,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 e2963db..58ef44c 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 86c9572..4c4fbad 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<UserConfig>, 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 f237d73..dd9c8ff 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -768,6 +768,45 @@ 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: Option<bool>,
+}
+
+struct AccountableBool<T>(T);
+
+impl Writeable for AccountableBool<bool> {
+ #[inline]
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ let wire_value = if self.0 { 7u8 } else { 0u8 };
+ writer.write_all(&[wire_value])
+ }
+}
+
+impl Readable for AccountableBool<bool> {
+ #[inline]
+ fn read<R: Read>(reader: &mut R) -> Result<AccountableBool<bool>, DecodeError> {
+ let mut buf = [0u8; 1];
+ reader.read_exact(&mut buf)?;
+ let bool_value = buf[0] == 7;
+ Ok(AccountableBool(bool_value))
+ }
+}
+
+impl From<bool> for AccountableBool<bool> {
+ fn from(val: bool) -> Self {
+ Self(val)
+ }
+}
+
+impl From<AccountableBool<bool>> for bool {
+ fn from(val: AccountableBool<bool>) -> Self {
+ val.0
+ }
}
/// An [`onion message`] to be sent to or received from a peer.
@@ -3375,6 +3414,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, encoding: (bool, AccountableBool))),
});
impl LengthReadable for OnionMessage {
@@ -4374,7 +4414,7 @@ mod tests {
};
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use crate::util::ser::{BigSize, Hostname, LengthReadable, Readable, ReadableArgs, Writeable};
- use crate::util::test_utils;
+ use crate::util::test_utils::{self, pubkey};
use bitcoin::hex::DisplayHex;
use bitcoin::{Amount, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
@@ -5874,6 +5914,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 = <Vec<u8>>::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d32144668701144760101010101010101010101010101010101010101010101010101010101010101000c89d4ff031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap();
@@ -6761,4 +6802,71 @@ mod tests {
.to_socket_addrs()
.is_err());
}
+
+ fn test_update_add_htlc() -> msgs::UpdateAddHTLC {
+ msgs::UpdateAddHTLC {
+ channel_id: ChannelId::from_bytes([2; 32]),
+ htlc_id: 42,
+ amount_msat: 1000,
+ payment_hash: PaymentHash([1; 32]),
+ cltv_expiry: 500000,
+ skimmed_fee_msat: None,
+ onion_routing_packet: msgs::OnionPacket {
+ version: 0,
+ public_key: Ok(pubkey(42)),
+ hop_data: [1; 20 * 65],
+ hmac: [2; 32],
+ },
+ blinding_point: None,
+ hold_htlc: None,
+ accountable: None,
+ }
+ }
+
+ #[test]
+ fn test_update_add_htlc_accountable_encoding() {
+ // Tests that accountable boolean values are written to the wire with correct u8 values.
+ for (bool_signal, wire_value) in [(Some(false), 0u8), (Some(true), 7u8)] {
+ let mut base_msg = test_update_add_htlc();
+ base_msg.accountable = bool_signal;
+ let encoded = base_msg.encode();
+ assert_eq!(
+ *encoded.last().unwrap(),
+ wire_value,
+ "wrong wire value for accountable={:?}",
+ bool_signal
+ );
+ }
+ }
+
+ fn do_test_htlc_accountable_from_u8(accountable_override: Option<u8>, expected: Option<bool>) {
+ // Tests custom encoding conversion of u8 wire values to appropriate boolean, manually
+ // writing to support values that we wouldn't encode ourselves but should be able to read.
+ let base_msg = test_update_add_htlc();
+ let mut encoded = base_msg.encode();
+ if let Some(value) = accountable_override {
+ encoded.extend_from_slice(&[0xfe, 0x00, 0x01, 0xa1, 0x47]);
+ encoded.push(1);
+ encoded.push(value);
+ }
+
+ let decoded: msgs::UpdateAddHTLC =
+ LengthReadable::read_from_fixed_length_buffer(&mut &encoded[..]).unwrap();
+
+ assert_eq!(
+ decoded.accountable, expected,
+ "accountable={:?} with override={:?} not eq to expected={:?}",
+ decoded.accountable, accountable_override, expected
+ );
+ }
+
+ #[test]
+ fn update_add_htlc_accountable_from_u8() {
+ // Tests that accountable signals encoded as a u8 are properly translated to a bool.
+ do_test_htlc_accountable_from_u8(None, None);
+ do_test_htlc_accountable_from_u8(Some(8), Some(false)); // 8 is an invalid value
+ do_test_htlc_accountable_from_u8(Some(7), Some(true));
+ do_test_htlc_accountable_from_u8(Some(3), Some(false));
+ do_test_htlc_accountable_from_u8(Some(0), Some(false));
+ }
}
diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs
index 1abe433..2b1b3a1 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 f9894fa..8f209c8 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,
Why this scored 18/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.