Add a method to fetch all possible remote-closure `script_pubkey`s
What changed, and why it matters
This commit adds a new public helper method that lets users of the Lightning Dev Kit wallet generate a list of Bitcoin addresses (script_pubkeys) where their funds could end up if a channel counterparty force-closes a channel. It is a recovery/scanning feature, not a bug fix or vulnerability. The change also adds a test-only assertion to make sure the generated list actually contains the expected address when signing a counterparty payment.
No security action required. Treat as a normal feature/API addition. Reviewers may want to confirm the public method's documentation and capacity calculation are correct, and that the test-only assertion does not affect production builds.
Security signals we found
New public API exposes deterministic script_pubkeys for counterparty force-closure recovery
Test-only assertion added to validate derivation consistency during signing
No memory-safety issues, no input parsing, no network handling, no secret leakage
Evidence from the diff
The patch introduces KeysManager::possible_v2_counterparty_closed_balance_spks, which derives all possible static payment keys and produces the corresponding counterparty payment scripts for channels using the v2 remote-key derivation scheme. It supports both plain static-remote-key and anchors-zero-fee-HTLC channel types. A #[cfg(test)] assertion is added inside sign_counterparty_payment_input to verify that the descriptor output script is present in the generated set when v2_remote_key_derivation is enabled.
Changed components
lightning/src/sign/mod.rsKeysManagerv2 remote key derivation recovery pathInspect captured patch +43 / −0
diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 2417a8d..9a1db92 100644
--- a/lightning/src/sign/mod.rs
+++ b/lightning/src/sign/mod.rs
@@ -2074,6 +2074,40 @@ impl KeysManager {
self.node_secret
}
+ /// Gets the set of possible `script_pubkey`s which can appear on chain for our
+ /// non-HTLC-encumbered balance if our counterparty force-closes a channel.
+ ///
+ /// If you've lost all data except your seed, asking your peers nicely to force-close the
+ /// chanels they had with you (and hoping they don't broadcast a stale state and that there are
+ /// no pending HTLCs in the latest state) and scanning the chain for these `script_pubkey`s can
+ /// allow you to recover (some of) your funds.
+ ///
+ /// Only channels opened when using a [`KeysManager`] with the `v2_remote_key_derivation`
+ /// argument to [`KeysManager::new`] set, or any spliced channels will close to such scripts,
+ /// other channels will close to a randomly-generated `script_pubkey`.
+ pub fn possible_v2_counterparty_closed_balance_spks<C: Signing>(
+ &self, secp_ctx: &Secp256k1<C>,
+ ) -> Vec<ScriptBuf> {
+ let mut res = Vec::with_capacity(usize::from(STATIC_PAYMENT_KEY_COUNT) * 2);
+ let static_remote_key_features = ChannelTypeFeatures::only_static_remote_key();
+ let mut zero_fee_htlc_features = ChannelTypeFeatures::only_static_remote_key();
+ zero_fee_htlc_features.set_anchors_zero_fee_htlc_tx_required();
+ for idx in 0..STATIC_PAYMENT_KEY_COUNT {
+ let key = self
+ .static_payment_key
+ .derive_priv(
+ &self.secp_ctx,
+ &ChildNumber::from_hardened_idx(u32::from(idx)).expect("key space exhausted"),
+ )
+ .expect("Your RNG is busted")
+ .private_key;
+ let pubkey = PublicKey::from_secret_key(secp_ctx, &key);
+ res.push(get_counterparty_payment_script(&static_remote_key_features, &pubkey));
+ res.push(get_counterparty_payment_script(&zero_fee_htlc_features, &pubkey));
+ }
+ res
+ }
+
fn derive_payment_key_v2(&self, key_idx: u64) -> SecretKey {
let idx = key_idx % u64::from(STATIC_PAYMENT_KEY_COUNT);
self.static_payment_key
@@ -2176,6 +2210,15 @@ impl KeysManager {
let signer = self.derive_channel_keys(&descriptor.channel_keys_id);
keys_cache = Some((signer, descriptor.channel_keys_id));
}
+ #[cfg(test)]
+ if self.v2_remote_key_derivation {
+ // In tests, we don't have to deal with upgrades from V1 signers with
+ // `v2_remote_key_derivation` set, so use this opportunity to test
+ // `possible_v2_counterparty_closed_balance_spks`.
+ let possible_spks =
+ self.possible_v2_counterparty_closed_balance_spks(secp_ctx);
+ assert!(possible_spks.contains(&descriptor.output.script_pubkey));
+ }
let witness = keys_cache.as_ref().unwrap().0.sign_counterparty_payment_input(
&psbt.unsigned_tx,
input_idx,
Why this scored 19/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.