ln: add accountable signal to OutboundHTLCOutput
What changed, and why it matters
This commit adds a new boolean flag called 'accountable' to outbound payment records (HTLCs) in a Lightning Network implementation. It ensures the flag is saved to disk and restored correctly when the node restarts. The change appears to be a data-model fix to support tracking which payments should be held financially accountable, likely as part of a larger feature. There is no direct evidence in the commit that this fixes an active security vulnerability, but incomplete state serialization can lead to incorrect behavior after a restart.
Treat as a routine state-model update. Review related commits in the same release cycle to confirm how the 'accountable' flag is consumed in commitment/penalty logic, and verify that defaulting to false for legacy channels is safe. No immediate incident response is indicated by this commit alone.
Security signals we found
New serialization field added for security-relevant HTLC accountability signal
Deserialization validates vector length to prevent state mismatch
Default fallback to false on legacy data preserves backward compatibility
No explicit security bug or CVE referenced in commit message
Evidence from the diff
The patch introduces an accountable: bool field on OutboundHTLCOutput in lightning/src/ln/channel.rs, replacing a previous placeholder accountable: None in the conversion to OutboundHTLCOutputInfo. It threads the value through HTLC creation, serialization (TLV field 79), and deserialization, with validation that the stored vector length matches the pending outbound HTLC list. Default values are set to false for legacy deserialization and test fixtures. This is a state-consistency change rather than a logic change in payment handling.
Changed components
lightning/src/ln/channel.rsOutboundHTLCOutput structChannel persistence/serialization logicOutbound HTLC state restorationInspect captured patch +33 / −2
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 93b79a8..7a17c79 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -451,6 +451,7 @@ struct OutboundHTLCOutput {
skimmed_fee_msat: Option<u64>,
send_timestamp: Option<Duration>,
hold_htlc: Option<()>,
+ accountable: bool,
}
/// See AwaitingRemoteRevoke ChannelState for more info
@@ -9749,7 +9750,7 @@ where
skimmed_fee_msat: htlc.skimmed_fee_msat,
blinding_point: htlc.blinding_point,
hold_htlc: htlc.hold_htlc,
- accountable: None,
+ accountable: Some(htlc.accountable),
});
}
}
@@ -12750,6 +12751,7 @@ where
skimmed_fee_msat,
send_timestamp,
hold_htlc: hold_htlc.then(|| ()),
+ accountable,
});
self.context.next_holder_htlc_id += 1;
@@ -14632,6 +14634,7 @@ where
let mut pending_outbound_skimmed_fees: Vec<Option<u64>> = Vec::new();
let mut pending_outbound_blinding_points: Vec<Option<PublicKey>> = Vec::new();
let mut pending_outbound_held_htlc_flags: Vec<Option<()>> = Vec::new();
+ let mut pending_outbound_accountable: Vec<bool> = Vec::new();
(self.context.pending_outbound_htlcs.len() as u64).write(writer)?;
for htlc in self.context.pending_outbound_htlcs.iter() {
@@ -14675,6 +14678,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();
@@ -14977,6 +14981,7 @@ where
(73, holder_commitment_point_last_revoked, option), // Added in 0.3
(75, inbound_committed_update_adds, optional_vec),
(77, holding_cell_accountable_flags, optional_vec), // Added in 0.3
+ (79, pending_outbound_accountable, optional_vec), // Added in 0.3
});
Ok(())
@@ -15144,6 +15149,7 @@ where
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
});
}
@@ -15367,6 +15373,7 @@ where
let mut holding_cell_held_htlc_flags_opt: Option<Vec<Option<()>>> = None;
let mut inbound_committed_update_adds_opt: Option<Vec<Option<msgs::UpdateAddHTLC>>> = None;
let mut holding_cell_accountable: Option<Vec<bool>> = None;
+ let mut pending_outbound_accountable: Option<Vec<bool>> = None;
read_tlv_fields!(reader, {
(0, announcement_sigs, option),
@@ -15418,6 +15425,7 @@ where
(73, holder_commitment_point_last_revoked_opt, option), // Added in 0.3
(75, inbound_committed_update_adds_opt, optional_vec),
(77, holding_cell_accountable, optional_vec), // Added in 0.3
+ (79, pending_outbound_accountable, optional_vec), // Added in 0.3
});
let holder_signer = signer_provider.derive_channel_signer(channel_keys_id);
@@ -15565,7 +15573,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 {
@@ -16169,6 +16186,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
@@ -16624,6 +16642,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() {
@@ -17022,6 +17041,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
});
let payment_preimage_3 =
@@ -17037,6 +17057,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
});
let payment_preimage_4 =
@@ -17452,6 +17473,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
});
chan.context.pending_outbound_htlcs.push(OutboundHTLCOutput {
@@ -17465,6 +17487,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
});
test_commitment!("304402207d0870964530f97b62497b11153c551dca0a1e226815ef0a336651158da0f82402200f5378beee0e77759147b8a0a284decd11bfd2bc55c8fafa41c134fe996d43c8",
@@ -17706,6 +17729,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
}),
);
@@ -17769,6 +17793,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
}),
);
@@ -17851,6 +17876,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
}
}),
);
@@ -17907,6 +17933,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
}),
);
@@ -17943,6 +17970,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
},
),
);
@@ -17980,6 +18008,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
},
),
);
@@ -18017,6 +18046,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
},
),
);
@@ -18077,6 +18107,7 @@ mod tests {
blinding_point: None,
send_timestamp: None,
hold_htlc: None,
+ accountable: false,
}),
);
Why this scored 27/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.