From c9d169ef227fa5f8294b0a6b4c8a3e5887e33853 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 24 Sep 2025 12:21:28 +0000 Subject: [PATCH 1/5] Add support for deriving and signing a new static remote key The `remote_key` derived by default in `KeysManager` depends on the chanel's `channel_keys_id`, which generally has sufficient entropy that without it the `remote_key` cannot be re-derived. In disaster case where there is no remaining state except the `KeysManager`'s `seed`, this results in lost funds, even if the counterparty force-closes the channel. Luckily, because of the `static_remote_key` feature, there's no need for this. If the `remote_key` we derive is one of a countable set, we can simply scan the chain for outputs to our `remote_key`s. Here we set up such new derivation, adding logic to derive one of 1000 possible `remote_key`s (which translates to 2000 potential `script_pubkey`s on chain). We also update the spending code to check which of the two derivation formats where used and sign with the correct key. --- fuzz/src/chanmon_consistency.rs | 1 + fuzz/src/full_stack.rs | 2 +- lightning/src/chain/channelmonitor.rs | 2 + lightning/src/chain/onchaintx.rs | 1 + lightning/src/ln/channel.rs | 1 + lightning/src/sign/mod.rs | 133 +++++++++++++++++++++----- 6 files changed, 115 insertions(+), 25 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 40a840eb164..5e15cf6817e 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -390,6 +390,7 @@ impl SignerProvider for KeyProvider { SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, self.node_secret[31]]).unwrap(), + SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, self.node_secret[31]]).unwrap(), [id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, self.node_secret[31]], diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index ee5f4572eb2..e036a73e01a 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -477,7 +477,7 @@ impl SignerProvider for KeyProvider { e = SecretKey::from_slice(&key).unwrap(); key[30] = 6 + if inbound { 0 } else { 6 }; f = key; - let signer = InMemorySigner::new(&secp_ctx, a, b, c, d, e, f, keys_id, keys_id); + let signer = InMemorySigner::new(&secp_ctx, a, b, c, c, d, e, f, keys_id, keys_id); TestChannelSigner::new_with_revoked(DynSigner::new(signer), state, false, false) } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 0f36cf14e60..4dca0a72bc1 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -6878,6 +6878,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], [0; 32], [0; 32], @@ -7140,6 +7141,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], [0; 32], [0; 32], diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 0d70f9d1201..02c1f6b92be 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1307,6 +1307,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], [0; 32], [0; 32], diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 1ca067f43f4..4128dc30199 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -15987,6 +15987,7 @@ mod tests { SecretKey::from_slice(&>::from_hex("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), + SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("3333333333333333333333333333333333333333333333333333333333333333").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index f9db5ff4672..42bf07dbd72 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -41,8 +41,8 @@ use crate::chain::transaction::OutPoint; use crate::crypto::utils::{hkdf_extract_expand_twice, sign, sign_with_aux_rand}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ - get_revokeable_redeemscript, make_funding_redeemscript, ChannelPublicKeys, - ChannelTransactionParameters, ClosingTransaction, CommitmentTransaction, + get_counterparty_payment_script, get_revokeable_redeemscript, make_funding_redeemscript, + ChannelPublicKeys, ChannelTransactionParameters, ClosingTransaction, CommitmentTransaction, HTLCOutputInCommitment, HolderCommitmentTransaction, }; use crate::ln::channel::ANCHOR_OUTPUT_VALUE_SATOSHI; @@ -56,6 +56,7 @@ use crate::ln::msgs::PartialSignatureWithNonce; use crate::ln::msgs::{UnsignedChannelAnnouncement, UnsignedGossipMessage}; use crate::ln::script::ShutdownScript; use crate::offers::invoice::UnsignedBolt12Invoice; +use crate::types::features::ChannelTypeFeatures; use crate::types::payment::PaymentPreimage; use crate::util::async_poll::AsyncResult; use crate::util::ser::{ReadableArgs, Writeable}; @@ -140,6 +141,16 @@ pub(crate) const P2WPKH_WITNESS_WEIGHT: u64 = 1 /* num stack items */ + pub(crate) const P2TR_KEY_PATH_WITNESS_WEIGHT: u64 = 1 /* witness items */ + 1 /* schnorr sig len */ + 64 /* schnorr sig */; +/// If a [`KeysManager`] is built with [`KeysManager::new`] with `v2_remote_key_derivation` set, +/// the script which we receive funds to on-chain when our counterparty force-closes a channel is +/// one of this many possible derivation paths. +/// +/// Keping this limited allows for scanning the chain to find lost funds if our state is destroyed, +/// while this being more than a handful provides some privacy by not constantly reusing the same +/// scripts on-chain across channels. +// Note that this MUST remain below the maximum BIP 32 derivation paths (2^31) +pub const STATIC_PAYMENT_KEY_COUNT: u16 = 1000; + /// Information about a spendable output to our "payment key". /// /// See [`SpendableOutputDescriptor::StaticPaymentOutput`] for more details on how to spend this. @@ -371,7 +382,7 @@ impl SpendableOutputDescriptor { if let Some(basepoint) = delayed_payment_basepoint.as_ref() { // Required to derive signing key: privkey = basepoint_secret + SHA256(per_commitment_point || basepoint) let add_tweak = basepoint.derive_add_tweak(&per_commitment_point); - let payment_key = DelayedPaymentKey(add_public_key_tweak( + let delayed_payment_key = DelayedPaymentKey(add_public_key_tweak( secp_ctx, &basepoint.to_public_key(), &add_tweak, @@ -381,7 +392,7 @@ impl SpendableOutputDescriptor { Some(get_revokeable_redeemscript( &revocation_pubkey, *to_self_delay, - &payment_key, + &delayed_payment_key, )), Some(add_tweak), ) @@ -1140,8 +1151,12 @@ pub struct InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey, /// Holder secret key for blinded revocation pubkey. pub revocation_base_key: SecretKey, - /// Holder secret key used for our balance in counterparty-broadcasted commitment transactions. - pub payment_key: SecretKey, + /// Holder secret key used for our balance in counterparty-broadcasted commitment transactions, + /// old-style derivation. + payment_key_v1: SecretKey, + /// Holder secret key used for our balance in counterparty-broadcasted commitment transactions, + /// new-style derivation. + payment_key_v2: SecretKey, /// Holder secret key used in an HTLC transaction. pub delayed_payment_base_key: SecretKey, /// Holder HTLC secret key used in commitment transaction HTLC outputs. @@ -1160,7 +1175,8 @@ impl PartialEq for InMemorySigner { fn eq(&self, other: &Self) -> bool { self.funding_key == other.funding_key && self.revocation_base_key == other.revocation_base_key - && self.payment_key == other.payment_key + && self.payment_key_v1 == other.payment_key_v1 + && self.payment_key_v2 == other.payment_key_v2 && self.delayed_payment_base_key == other.delayed_payment_base_key && self.htlc_base_key == other.htlc_base_key && self.commitment_seed == other.commitment_seed @@ -1174,7 +1190,8 @@ impl Clone for InMemorySigner { Self { funding_key: self.funding_key.clone(), revocation_base_key: self.revocation_base_key.clone(), - payment_key: self.payment_key.clone(), + payment_key_v1: self.payment_key_v1.clone(), + payment_key_v2: self.payment_key_v2.clone(), delayed_payment_base_key: self.delayed_payment_base_key.clone(), htlc_base_key: self.htlc_base_key.clone(), commitment_seed: self.commitment_seed.clone(), @@ -1186,24 +1203,57 @@ impl Clone for InMemorySigner { } impl InMemorySigner { - /// Creates a new [`InMemorySigner`]. + #[cfg(any(feature = "_test_utils", test))] pub fn new( secp_ctx: &Secp256k1, funding_key: SecretKey, revocation_base_key: SecretKey, - payment_key: SecretKey, delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, - commitment_seed: [u8; 32], channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], + payment_key_v1: SecretKey, payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, + htlc_base_key: SecretKey, commitment_seed: [u8; 32], channel_keys_id: [u8; 32], + rand_bytes_unique_start: [u8; 32], + ) -> InMemorySigner { + // TODO: Make the key used dynamic + let holder_channel_pubkeys = InMemorySigner::make_holder_keys( + secp_ctx, + &funding_key, + &revocation_base_key, + &payment_key_v1, + &delayed_payment_base_key, + &htlc_base_key, + ); + InMemorySigner { + funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), + revocation_base_key, + payment_key_v1, + payment_key_v2, + delayed_payment_base_key, + htlc_base_key, + commitment_seed, + holder_channel_pubkeys, + channel_keys_id, + entropy_source: RandomBytes::new(rand_bytes_unique_start), + } + } + + #[cfg(not(any(feature = "_test_utils", test)))] + fn new( + secp_ctx: &Secp256k1, funding_key: SecretKey, revocation_base_key: SecretKey, + payment_key_v1: SecretKey, payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, + htlc_base_key: SecretKey, commitment_seed: [u8; 32], channel_keys_id: [u8; 32], + rand_bytes_unique_start: [u8; 32], ) -> InMemorySigner { + // TODO: Make the key used dynamic let holder_channel_pubkeys = InMemorySigner::make_holder_keys( secp_ctx, &funding_key, &revocation_base_key, - &payment_key, + &payment_key_v1, &delayed_payment_base_key, &htlc_base_key, ); InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, - payment_key, + payment_key_v1, + payment_key_v2, delayed_payment_base_key, htlc_base_key, commitment_seed, @@ -1264,14 +1314,28 @@ impl InMemorySigner { return Err(()); } - let remotepubkey = bitcoin::PublicKey::new(self.holder_channel_pubkeys.payment_point); - let supports_anchors_zero_fee_htlc_tx = descriptor + let legacy_default_channel_type = ChannelTypeFeatures::only_static_remote_key(); + let channel_type_features = descriptor .channel_transaction_parameters .as_ref() - .map(|params| params.channel_type_features.supports_anchors_zero_fee_htlc_tx()) - .unwrap_or(false); + .map(|params| ¶ms.channel_type_features) + .unwrap_or(&legacy_default_channel_type); + + let payment_point_v1 = PublicKey::from_secret_key(secp_ctx, &self.payment_key_v1); + let payment_point_v2 = PublicKey::from_secret_key(secp_ctx, &self.payment_key_v2); + let spk_v1 = get_counterparty_payment_script(channel_type_features, &payment_point_v1); + let spk_v2 = get_counterparty_payment_script(channel_type_features, &payment_point_v2); + + let (remotepubkey, payment_key) = if spk_v1 == descriptor.output.script_pubkey { + (bitcoin::PublicKey::new(payment_point_v1), &self.payment_key_v1) + } else { + if spk_v2 != descriptor.output.script_pubkey { + return Err(()); + } + (bitcoin::PublicKey::new(payment_point_v2), &self.payment_key_v2) + }; - let witness_script = if supports_anchors_zero_fee_htlc_tx { + let witness_script = if channel_type_features.supports_anchors_zero_fee_htlc_tx() { chan_utils::get_to_countersigner_keyed_anchor_redeemscript(&remotepubkey.inner) } else { ScriptBuf::new_p2pkh(&remotepubkey.pubkey_hash()) @@ -1286,8 +1350,8 @@ impl InMemorySigner { ) .unwrap()[..] ); - let remotesig = sign_with_aux_rand(secp_ctx, &sighash, &self.payment_key, &self); - let payment_script = if supports_anchors_zero_fee_htlc_tx { + let remotesig = sign_with_aux_rand(secp_ctx, &sighash, payment_key, &self); + let payment_script = if channel_type_features.supports_anchors_zero_fee_htlc_tx() { witness_script.to_p2wsh() } else { ScriptBuf::new_p2wpkh(&remotepubkey.wpubkey_hash().unwrap()) @@ -1300,7 +1364,7 @@ impl InMemorySigner { let mut witness = Vec::with_capacity(2); witness.push(remotesig.serialize_der().to_vec()); witness[0].push(EcdsaSighashType::All as u8); - if supports_anchors_zero_fee_htlc_tx { + if channel_type_features.supports_anchors_zero_fee_htlc_tx() { witness.push(witness_script.to_bytes()); } else { witness.push(remotepubkey.to_bytes()); @@ -1874,6 +1938,7 @@ pub struct KeysManager { destination_script: ScriptBuf, shutdown_pubkey: PublicKey, channel_master_key: Xpriv, + static_payment_key: Xpriv, channel_child_index: AtomicUsize, peer_storage_key: PeerStorageKey, receive_auth_key: ReceiveAuthKey, @@ -1915,6 +1980,7 @@ impl KeysManager { const INBOUND_PAYMENT_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 5 }; const PEER_STORAGE_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 6 }; const RECEIVE_AUTH_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 7 }; + const STATIC_PAYMENT_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 8 }; let secp_ctx = Secp256k1::new(); // Note that when we aren't serializing the key, network doesn't matter @@ -1962,6 +2028,10 @@ impl KeysManager { .expect("Your RNG is busted") .private_key; + let static_payment_key = master_key + .derive_priv(&secp_ctx, &STATIC_PAYMENT_KEY_INDEX) + .expect("Your RNG is busted"); + let mut rand_bytes_engine = Sha256::engine(); rand_bytes_engine.input(&starting_time_secs.to_be_bytes()); rand_bytes_engine.input(&starting_time_nanos.to_be_bytes()); @@ -1984,6 +2054,7 @@ impl KeysManager { channel_master_key, channel_child_index: AtomicUsize::new(0), + static_payment_key, entropy_source: RandomBytes::new(rand_bytes_unique_start), @@ -2004,6 +2075,19 @@ impl KeysManager { self.node_secret } + fn derive_payment_key_v2(&self, params: &[u8; 32]) -> SecretKey { + let mut eight_bytes = [0; 8]; + eight_bytes.copy_from_slice(¶ms[0..8]); + let idx = u64::from_le_bytes(eight_bytes) % u64::from(STATIC_PAYMENT_KEY_COUNT); + self.static_payment_key + .derive_priv( + &self.secp_ctx, + &ChildNumber::from_hardened_idx(idx as u32).expect("key space exhausted"), + ) + .expect("Your RNG is busted") + .private_key + } + /// Derive an old [`EcdsaChannelSigner`] containing per-channel secrets based on a key derivation parameters. pub fn derive_channel_keys(&self, params: &[u8; 32]) -> InMemorySigner { let chan_id = u64::from_be_bytes(params[0..8].try_into().unwrap()); @@ -2044,8 +2128,8 @@ impl KeysManager { } let funding_key = key_step!(b"funding key", commitment_seed); let revocation_base_key = key_step!(b"revocation base key", funding_key); - let payment_key = key_step!(b"payment key", revocation_base_key); - let delayed_payment_base_key = key_step!(b"delayed payment base key", payment_key); + let payment_key_v1 = key_step!(b"payment key", revocation_base_key); + let delayed_payment_base_key = key_step!(b"delayed payment base key", payment_key_v1); let htlc_base_key = key_step!(b"HTLC base key", delayed_payment_base_key); let prng_seed = self.get_secure_random_bytes(); @@ -2053,7 +2137,8 @@ impl KeysManager { &self.secp_ctx, funding_key, revocation_base_key, - payment_key, + payment_key_v1, + self.derive_payment_key_v2(&commitment_seed), delayed_payment_base_key, htlc_base_key, commitment_seed, From c1cd42377721daac7dfbd5669e90c7a873173748 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 24 Sep 2025 12:35:45 +0000 Subject: [PATCH 2/5] Convert `ChannelSigner::pubkeys` to only fetch *new* pubkeys The `remote_key` derived by default in `KeysManager` depends on the chanel's `channel_keys_id`, which generally has sufficient entropy that without it the `remote_key` cannot be re-derived. In disaster case where there is no remaining state except the `KeysManager`'s `seed`, this results in lost funds, even if the counterparty force-closes the channel. Luckily, because of the `static_remote_key` feature, there's no need for this. If the `remote_key` we derive is one of a countable set, we can simply scan the chain for outputs to our `remote_key`s. In the next commit, we'll start using different `remote_key`s based on a config knob the user sets, but with the current `ChannelSigner::pubkeys` API this would be invalid - we can't return a different set of keys for a re-derived `ChannelSigner`. Luckily, this isn't actually how LDK uses `ChannelSigner::pubkeys`, it actually only calls it when it wants a new set of pubkeys, either for a new channel or a splice. Thus, here, we rename `ChannelSigner::pubkeys` to `ChannelSigner::new_pubkeys` and update documentation to match. --- fuzz/src/chanmon_consistency.rs | 2 - fuzz/src/full_stack.rs | 3 +- lightning/src/chain/channelmonitor.rs | 8 +- lightning/src/chain/onchaintx.rs | 5 +- lightning/src/ln/chan_utils.rs | 8 +- lightning/src/ln/channel.rs | 15 ++-- lightning/src/sign/mod.rs | 98 +++++++++-------------- lightning/src/util/dyn_signer.rs | 2 +- lightning/src/util/test_channel_signer.rs | 4 +- 9 files changed, 56 insertions(+), 89 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 5e15cf6817e..a97e9b969c8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -382,11 +382,9 @@ impl SignerProvider for KeyProvider { } fn derive_channel_signer(&self, channel_keys_id: [u8; 32]) -> Self::EcdsaSigner { - let secp_ctx = Secp256k1::signing_only(); let id = channel_keys_id[0]; #[rustfmt::skip] let keys = InMemorySigner::new( - &secp_ctx, SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, self.node_secret[31]]).unwrap(), diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index e036a73e01a..ec2e7a59beb 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -458,7 +458,6 @@ impl SignerProvider for KeyProvider { } fn derive_channel_signer(&self, keys_id: [u8; 32]) -> Self::EcdsaSigner { - let secp_ctx = Secp256k1::signing_only(); let ctr = keys_id[0]; let (inbound, state) = self.signer_state.borrow().get(&ctr).unwrap().clone(); @@ -477,7 +476,7 @@ impl SignerProvider for KeyProvider { e = SecretKey::from_slice(&key).unwrap(); key[30] = 6 + if inbound { 0 } else { 6 }; f = key; - let signer = InMemorySigner::new(&secp_ctx, a, b, c, c, d, e, f, keys_id, keys_id); + let signer = InMemorySigner::new(a, b, c, c, d, e, f, keys_id, keys_id); TestChannelSigner::new_with_revoked(DynSigner::new(signer), state, false, false) } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 4dca0a72bc1..d6278a14f08 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -6699,7 +6699,7 @@ mod tests { use crate::ln::functional_test_utils::*; use crate::ln::script::ShutdownScript; use crate::ln::types::ChannelId; - use crate::sign::InMemorySigner; + use crate::sign::{ChannelSigner, InMemorySigner}; use crate::sync::Arc; use crate::types::features::ChannelTypeFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -6872,7 +6872,6 @@ mod tests { } let keys = InMemorySigner::new( - &secp_ctx, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), @@ -6894,7 +6893,7 @@ mod tests { let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_pubkeys: keys.new_pubkeys(None, &secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { @@ -7135,7 +7134,6 @@ mod tests { let dummy_key = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let keys = InMemorySigner::new( - &secp_ctx, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), @@ -7157,7 +7155,7 @@ mod tests { let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_pubkeys: keys.new_pubkeys(None, &secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 02c1f6b92be..77a2449f5b3 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1287,7 +1287,7 @@ mod tests { }; use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; use crate::ln::functional_test_utils::create_dummy_block; - use crate::sign::{ChannelDerivationParameters, HTLCDescriptor, InMemorySigner}; + use crate::sign::{ChannelDerivationParameters, ChannelSigner, HTLCDescriptor, InMemorySigner}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::test_utils::{TestBroadcaster, TestFeeEstimator, TestLogger}; @@ -1301,7 +1301,6 @@ mod tests { fn test_broadcast_height() { let secp_ctx = Secp256k1::new(); let signer = InMemorySigner::new( - &secp_ctx, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), @@ -1339,7 +1338,7 @@ mod tests { // Use non-anchor channels so that HTLC-Timeouts are broadcast immediately instead of sent // to the user for external funding. let chan_params = ChannelTransactionParameters { - holder_pubkeys: signer.holder_channel_pubkeys.clone(), + holder_pubkeys: signer.new_pubkeys(None, &secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 0a5e37286bc..e6a45527e3a 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -1014,11 +1014,11 @@ pub struct ChannelTransactionParameters { /// If a channel was funded with transaction A, and later spliced with transaction B, this field /// tracks the txid of transaction A. /// - /// See [`compute_funding_key_tweak`] and [`ChannelSigner::pubkeys`] for more context on how + /// See [`compute_funding_key_tweak`] and [`ChannelSigner::new_pubkeys`] for more context on how /// this may be used. /// /// [`compute_funding_key_tweak`]: crate::sign::compute_funding_key_tweak - /// [`ChannelSigner::pubkeys`]: crate::sign::ChannelSigner::pubkeys + /// [`ChannelSigner::new_pubkeys`]: crate::sign::ChannelSigner::new_pubkeys pub splice_parent_funding_txid: Option, /// This channel's type, as negotiated during channel open. For old objects where this field /// wasn't serialized, it will default to static_remote_key at deserialization. @@ -2240,8 +2240,8 @@ mod tests { let counterparty_signer = keys_provider.derive_channel_signer(keys_provider.generate_channel_keys_id(true, 1)); let per_commitment_secret = SecretKey::from_slice(&>::from_hex("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100").unwrap()[..]).unwrap(); let per_commitment_point = PublicKey::from_secret_key(&secp_ctx, &per_commitment_secret); - let holder_pubkeys = signer.pubkeys(None, &secp_ctx); - let counterparty_pubkeys = counterparty_signer.pubkeys(None, &secp_ctx).clone(); + let holder_pubkeys = signer.new_pubkeys(None, &secp_ctx); + let counterparty_pubkeys = counterparty_signer.new_pubkeys(None, &secp_ctx).clone(); let channel_parameters = ChannelTransactionParameters { holder_pubkeys: holder_pubkeys.clone(), holder_selected_contest_delay: 0, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4128dc30199..79762de2f5b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2380,7 +2380,7 @@ impl FundingScope { // Rotate the pubkeys using the prev_funding_txid as a tweak let prev_funding_txid = prev_funding.get_funding_txid(); - let holder_pubkeys = context.holder_pubkeys(prev_funding_txid); + let holder_pubkeys = context.new_holder_pubkeys(prev_funding_txid); let channel_parameters = &prev_funding.channel_transaction_parameters; let mut post_channel_transaction_parameters = ChannelTransactionParameters { @@ -3365,7 +3365,7 @@ where // TODO(dual_funding): Checks for `funding_feerate_sat_per_1000_weight`? - let pubkeys = holder_signer.pubkeys(None, &secp_ctx); + let pubkeys = holder_signer.new_pubkeys(None, &secp_ctx); let funding = FundingScope { value_to_self_msat, @@ -3603,7 +3603,7 @@ where Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get destination script".to_owned()}), }; - let pubkeys = holder_signer.pubkeys(None, &secp_ctx); + let pubkeys = holder_signer.new_pubkeys(None, &secp_ctx); let temporary_channel_id = temporary_channel_id_fn.map(|f| f(&pubkeys)) .unwrap_or_else(|| ChannelId::temporary_from_entropy_source(entropy_source)); @@ -3967,9 +3967,9 @@ where } /// Returns holder pubkeys to use for the channel. - fn holder_pubkeys(&self, prev_funding_txid: Option) -> ChannelPublicKeys { + fn new_holder_pubkeys(&self, prev_funding_txid: Option) -> ChannelPublicKeys { match &self.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => ecdsa.pubkeys(prev_funding_txid, &self.secp_ctx), + ChannelSignerType::Ecdsa(ecdsa) => ecdsa.new_pubkeys(prev_funding_txid, &self.secp_ctx), // TODO (taproot|arik) #[cfg(taproot)] _ => todo!(), @@ -11553,7 +11553,7 @@ where // Rotate the pubkeys using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); - let funding_pubkey = self.context.holder_pubkeys(prev_funding_txid).funding_pubkey; + let funding_pubkey = self.context.new_holder_pubkeys(prev_funding_txid).funding_pubkey; Ok(msgs::SpliceInit { channel_id: self.context.channel_id, @@ -15983,7 +15983,6 @@ mod tests { let secp_ctx = Secp256k1::new(); let signer = InMemorySigner::new( - &secp_ctx, SecretKey::from_slice(&>::from_hex("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), @@ -15997,7 +15996,7 @@ mod tests { [0; 32], ); - let holder_pubkeys = signer.pubkeys(None, &secp_ctx); + let holder_pubkeys = signer.new_pubkeys(None, &secp_ctx); assert_eq!(holder_pubkeys.funding_pubkey.serialize()[..], >::from_hex("023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb").unwrap()[..]); let keys_provider = Keys { signer: signer.clone() }; diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 42bf07dbd72..f3869532d34 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -270,11 +270,14 @@ pub enum SpendableOutputDescriptor { /// it is an output from an old state which we broadcast (which should never happen). /// /// To derive the delayed payment key which is used to sign this input, you must pass the - /// holder [`InMemorySigner::delayed_payment_base_key`] (i.e., the private key which corresponds to the - /// [`ChannelPublicKeys::delayed_payment_basepoint`] in [`ChannelSigner::pubkeys`]) and the provided - /// [`DelayedPaymentOutputDescriptor::per_commitment_point`] to [`chan_utils::derive_private_key`]. The DelayedPaymentKey can be - /// generated without the secret key using [`DelayedPaymentKey::from_basepoint`] and only the - /// [`ChannelPublicKeys::delayed_payment_basepoint`] which appears in [`ChannelSigner::pubkeys`]. + /// holder [`InMemorySigner::delayed_payment_base_key`] (i.e., the private key which + /// corresponds to the [`ChannelPublicKeys::delayed_payment_basepoint`] in + /// [`ChannelSigner::new_pubkeys`]) and the provided + /// [`DelayedPaymentOutputDescriptor::per_commitment_point`] to + /// [`chan_utils::derive_private_key`]. The DelayedPaymentKey can be generated without the + /// secret key using [`DelayedPaymentKey::from_basepoint`] and only the + /// [`ChannelPublicKeys::delayed_payment_basepoint`] which appears in + /// [`ChannelSigner::new_pubkeys`]. /// /// To derive the [`DelayedPaymentOutputDescriptor::revocation_pubkey`] provided here (which is /// used in the witness script generation), you must pass the counterparty @@ -289,7 +292,7 @@ pub enum SpendableOutputDescriptor { /// [`chan_utils::get_revokeable_redeemscript`]. DelayedPaymentOutput(DelayedPaymentOutputDescriptor), /// An output spendable exclusively by our payment key (i.e., the private key that corresponds - /// to the `payment_point` in [`ChannelSigner::pubkeys`]). The output type depends on the + /// to the `payment_point` in [`ChannelSigner::new_pubkeys`]). The output type depends on the /// channel type negotiated. /// /// On an anchor outputs channel, the witness in the spending input is: @@ -789,14 +792,17 @@ pub trait ChannelSigner { /// and pause future signing operations until this validation completes. fn validate_counterparty_revocation(&self, idx: u64, secret: &SecretKey) -> Result<(), ()>; - /// Returns the holder's channel public keys and basepoints. + /// Returns a *new* set of holder channel public keys and basepoints. They may be the same as a + /// previous value, but are also allowed to change arbitrarily. Signing methods must still + /// support both old and new versions, but this should only be called either for new channels + /// or new splices. /// /// `splice_parent_funding_txid` can be used to compute a tweak to rotate the funding key in the /// 2-of-2 multisig script during a splice. See [`compute_funding_key_tweak`] for an example /// tweak and more details. /// /// This method is *not* asynchronous. Instead, the value must be cached locally. - fn pubkeys( + fn new_pubkeys( &self, splice_parent_funding_txid: Option, secp_ctx: &Secp256k1, ) -> ChannelPublicKeys; @@ -1095,7 +1101,7 @@ mod sealed { use bitcoin::secp256k1::{Scalar, SecretKey}; #[derive(Clone, PartialEq)] - pub struct MaybeTweakedSecretKey(SecretKey); + pub struct MaybeTweakedSecretKey(pub(super) SecretKey); impl From for MaybeTweakedSecretKey { fn from(value: SecretKey) -> Self { @@ -1163,8 +1169,6 @@ pub struct InMemorySigner { pub htlc_base_key: SecretKey, /// Commitment seed. pub commitment_seed: [u8; 32], - /// Holder public keys and basepoints. - pub(crate) holder_channel_pubkeys: ChannelPublicKeys, /// Key derivation parameters. channel_keys_id: [u8; 32], /// A source of random bytes. @@ -1180,7 +1184,6 @@ impl PartialEq for InMemorySigner { && self.delayed_payment_base_key == other.delayed_payment_base_key && self.htlc_base_key == other.htlc_base_key && self.commitment_seed == other.commitment_seed - && self.holder_channel_pubkeys == other.holder_channel_pubkeys && self.channel_keys_id == other.channel_keys_id } } @@ -1195,7 +1198,6 @@ impl Clone for InMemorySigner { delayed_payment_base_key: self.delayed_payment_base_key.clone(), htlc_base_key: self.htlc_base_key.clone(), commitment_seed: self.commitment_seed.clone(), - holder_channel_pubkeys: self.holder_channel_pubkeys.clone(), channel_keys_id: self.channel_keys_id, entropy_source: RandomBytes::new(self.get_secure_random_bytes()), } @@ -1204,21 +1206,11 @@ impl Clone for InMemorySigner { impl InMemorySigner { #[cfg(any(feature = "_test_utils", test))] - pub fn new( - secp_ctx: &Secp256k1, funding_key: SecretKey, revocation_base_key: SecretKey, - payment_key_v1: SecretKey, payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, - htlc_base_key: SecretKey, commitment_seed: [u8; 32], channel_keys_id: [u8; 32], - rand_bytes_unique_start: [u8; 32], + pub fn new( + funding_key: SecretKey, revocation_base_key: SecretKey, payment_key_v1: SecretKey, + payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, + commitment_seed: [u8; 32], channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], ) -> InMemorySigner { - // TODO: Make the key used dynamic - let holder_channel_pubkeys = InMemorySigner::make_holder_keys( - secp_ctx, - &funding_key, - &revocation_base_key, - &payment_key_v1, - &delayed_payment_base_key, - &htlc_base_key, - ); InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, @@ -1227,28 +1219,17 @@ impl InMemorySigner { delayed_payment_base_key, htlc_base_key, commitment_seed, - holder_channel_pubkeys, channel_keys_id, entropy_source: RandomBytes::new(rand_bytes_unique_start), } } #[cfg(not(any(feature = "_test_utils", test)))] - fn new( - secp_ctx: &Secp256k1, funding_key: SecretKey, revocation_base_key: SecretKey, - payment_key_v1: SecretKey, payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, - htlc_base_key: SecretKey, commitment_seed: [u8; 32], channel_keys_id: [u8; 32], - rand_bytes_unique_start: [u8; 32], + fn new( + funding_key: SecretKey, revocation_base_key: SecretKey, payment_key_v1: SecretKey, + payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, + commitment_seed: [u8; 32], channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], ) -> InMemorySigner { - // TODO: Make the key used dynamic - let holder_channel_pubkeys = InMemorySigner::make_holder_keys( - secp_ctx, - &funding_key, - &revocation_base_key, - &payment_key_v1, - &delayed_payment_base_key, - &htlc_base_key, - ); InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, @@ -1257,7 +1238,6 @@ impl InMemorySigner { delayed_payment_base_key, htlc_base_key, commitment_seed, - holder_channel_pubkeys, channel_keys_id, entropy_source: RandomBytes::new(rand_bytes_unique_start), } @@ -1271,22 +1251,6 @@ impl InMemorySigner { self.funding_key.with_tweak(tweak) } - fn make_holder_keys( - secp_ctx: &Secp256k1, funding_key: &SecretKey, revocation_base_key: &SecretKey, - payment_key: &SecretKey, delayed_payment_base_key: &SecretKey, htlc_base_key: &SecretKey, - ) -> ChannelPublicKeys { - let from_secret = |s: &SecretKey| PublicKey::from_secret_key(secp_ctx, s); - ChannelPublicKeys { - funding_pubkey: from_secret(&funding_key), - revocation_basepoint: RevocationBasepoint::from(from_secret(&revocation_base_key)), - payment_point: from_secret(&payment_key), - delayed_payment_basepoint: DelayedPaymentBasepoint::from(from_secret( - &delayed_payment_base_key, - )), - htlc_basepoint: HtlcBasepoint::from(from_secret(&htlc_base_key)), - } - } - /// Sign the single input of `spend_tx` at index `input_idx`, which spends the output described /// by `descriptor`, returning the witness stack for the input. /// @@ -1476,10 +1440,21 @@ impl ChannelSigner for InMemorySigner { Ok(()) } - fn pubkeys( + fn new_pubkeys( &self, splice_parent_funding_txid: Option, secp_ctx: &Secp256k1, ) -> ChannelPublicKeys { - let mut pubkeys = self.holder_channel_pubkeys.clone(); + let from_secret = |s: &SecretKey| PublicKey::from_secret_key(secp_ctx, s); + let mut pubkeys = ChannelPublicKeys { + funding_pubkey: from_secret(&self.funding_key.0), + revocation_basepoint: RevocationBasepoint::from(from_secret(&self.revocation_base_key)), + // TODO: Make the payment_key used dynamic + payment_point: from_secret(&self.payment_key_v1), + delayed_payment_basepoint: DelayedPaymentBasepoint::from(from_secret( + &self.delayed_payment_base_key, + )), + htlc_basepoint: HtlcBasepoint::from(from_secret(&self.htlc_base_key)), + }; + if splice_parent_funding_txid.is_some() { pubkeys.funding_pubkey = self.funding_key(splice_parent_funding_txid).public_key(secp_ctx); @@ -2134,7 +2109,6 @@ impl KeysManager { let prng_seed = self.get_secure_random_bytes(); InMemorySigner::new( - &self.secp_ctx, funding_key, revocation_base_key, payment_key_v1, diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index 9e8ba4aee2c..c519484a938 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -174,7 +174,7 @@ delegate!(DynSigner, ChannelSigner, holder_tx: &HolderCommitmentTransaction, preimages: Vec ) -> Result<(), ()>, - fn pubkeys(, + fn new_pubkeys(, splice_parent_funding_txid: Option, secp_ctx: &Secp256k1 ) -> ChannelPublicKeys, fn channel_keys_id(,) -> [u8; 32], diff --git a/lightning/src/util/test_channel_signer.rs b/lightning/src/util/test_channel_signer.rs index 4d9bf244dbe..6b1950169e8 100644 --- a/lightning/src/util/test_channel_signer.rs +++ b/lightning/src/util/test_channel_signer.rs @@ -221,10 +221,10 @@ impl ChannelSigner for TestChannelSigner { Ok(()) } - fn pubkeys( + fn new_pubkeys( &self, splice_parent_funding_txid: Option, secp_ctx: &Secp256k1, ) -> ChannelPublicKeys { - self.inner.pubkeys(splice_parent_funding_txid, secp_ctx) + self.inner.new_pubkeys(splice_parent_funding_txid, secp_ctx) } fn channel_keys_id(&self) -> [u8; 32] { From f335ecb0ce6f8e7b618cdd3c173d2e613dcac722 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 24 Sep 2025 12:23:46 +0000 Subject: [PATCH 3/5] Allow `KeysManager` to opt-into the new `remote_key` derivation The `remote_key` derived by default in `KeysManager` depends on the chanel's `channel_keys_id`, which generally has sufficient entropy that without it the `remote_key` cannot be re-derived. In disaster case where there is no remaining state except the `KeysManager`'s `seed`, this results in lost funds, even if the counterparty force-closes the channel. Luckily, because of the `static_remote_key` feature, there's no need for this. If the `remote_key` we derive is one of a countable set, we can simply scan the chain for outputs to our `remote_key`s. Here we finally allow users to opt into the new derivation scheme, using the new derivation scheme for `remote_key`s for new and spliced channels if a new `KeysManager::new` argument is set to `true`. --- ext-functional-test-demo/src/main.rs | 2 +- fuzz/src/chanmon_consistency.rs | 1 + fuzz/src/full_stack.rs | 2 +- fuzz/src/lsps_message.rs | 2 +- lightning-background-processor/src/lib.rs | 6 ++- lightning-dns-resolver/src/lib.rs | 6 +-- lightning/src/chain/channelmonitor.rs | 2 + lightning/src/chain/onchaintx.rs | 1 + lightning/src/events/bump_transaction/mod.rs | 2 +- lightning/src/ln/channel.rs | 1 + lightning/src/ln/channelmanager.rs | 4 +- lightning/src/ln/functional_test_utils.rs | 11 ++-- lightning/src/ln/invoice_utils.rs | 2 +- lightning/src/ln/monitor_tests.rs | 6 ++- lightning/src/ln/onion_payment.rs | 8 +-- lightning/src/ln/our_peer_storage.rs | 2 +- lightning/src/onion_message/dns_resolution.rs | 2 +- lightning/src/onion_message/messenger.rs | 2 +- lightning/src/sign/mod.rs | 51 ++++++++++++++----- lightning/src/util/test_utils.rs | 46 ++++++++++++----- 20 files changed, 111 insertions(+), 48 deletions(-) diff --git a/ext-functional-test-demo/src/main.rs b/ext-functional-test-demo/src/main.rs index 943bacf85d4..654cf91e01c 100644 --- a/ext-functional-test-demo/src/main.rs +++ b/ext-functional-test-demo/src/main.rs @@ -16,7 +16,7 @@ mod tests { impl TestSignerFactory for BrokenSignerFactory { fn make_signer( - &self, _seed: &[u8; 32], _now: Duration, + &self, _seed: &[u8; 32], _now: Duration, _v2_remote_key_derivation: bool, ) -> Box> { panic!() } diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index a97e9b969c8..ca03139a433 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -389,6 +389,7 @@ impl SignerProvider for KeyProvider { SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, self.node_secret[31]]).unwrap(), + true, SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, self.node_secret[31]]).unwrap(), SecretKey::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, self.node_secret[31]]).unwrap(), [id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, self.node_secret[31]], diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index ec2e7a59beb..9f961bc814c 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -476,7 +476,7 @@ impl SignerProvider for KeyProvider { e = SecretKey::from_slice(&key).unwrap(); key[30] = 6 + if inbound { 0 } else { 6 }; f = key; - let signer = InMemorySigner::new(a, b, c, c, d, e, f, keys_id, keys_id); + let signer = InMemorySigner::new(a, b, c, c, true, d, e, f, keys_id, keys_id); TestChannelSigner::new_with_revoked(DynSigner::new(signer), state, false, false) } diff --git a/fuzz/src/lsps_message.rs b/fuzz/src/lsps_message.rs index 2bc83c3fcd6..8dfa92221be 100644 --- a/fuzz/src/lsps_message.rs +++ b/fuzz/src/lsps_message.rs @@ -39,7 +39,7 @@ pub fn do_test(data: &[u8]) { let scorer = Arc::new(LockingWrapper::new(TestScorer::new())); let now = Duration::from_secs(genesis_block.header.time as u64); let seed = sha256::Hash::hash(b"lsps-message-seed").to_byte_array(); - let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos())); + let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos(), true)); let router = Arc::new(DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 44ce52b8291..dc6fbc7f320 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -2313,7 +2313,8 @@ mod tests { let scorer = Arc::new(LockingWrapper::new(TestScorer::new())); let now = Duration::from_secs(genesis_block.header.time as u64); let seed = [i as u8; 32]; - let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos())); + let keys_manager = + Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos(), true)); let router = Arc::new(DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), @@ -2329,7 +2330,8 @@ mod tests { let kv_store = Arc::new(Persister::new(format!("{}_persister_{}", &persist_dir, i).into())); let now = Duration::from_secs(genesis_block.header.time as u64); - let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos())); + let keys_manager = + Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos(), true)); let chain_monitor = Arc::new(chainmonitor::ChainMonitor::new( Some(Arc::clone(&chain_source)), Arc::clone(&tx_broadcaster), diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 75fe06fcc9a..f5b1d53fc8a 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -231,7 +231,7 @@ mod test { &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, _peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - let keys = KeysManager::new(&[0; 32], 42, 43); + let keys = KeysManager::new(&[0; 32], 42, 43, true); Ok(vec![BlindedMessagePath::one_hop( recipient, local_node_receive_key, @@ -274,7 +274,7 @@ mod test { } fn create_resolver() -> (impl AOnionMessenger, PublicKey) { - let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43)); + let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43, true)); let resolver_logger = TestLogger { node: "resolver" }; let resolver = OMDomainResolver::ignoring_incoming_proofs("8.8.8.8:53".parse().unwrap()); let resolver = Arc::new(resolver); @@ -313,7 +313,7 @@ mod test { let payment_id = PaymentId([42; 32]); let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); - let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43)); + let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43, true)); let payer_logger = TestLogger { node: "payer" }; let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); let payer = Arc::new(URIResolver { diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index d6278a14f08..82446809342 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -6876,6 +6876,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + true, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], @@ -7138,6 +7139,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + true, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 77a2449f5b3..48bd40a8347 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1305,6 +1305,7 @@ mod tests { SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), + true, SecretKey::from_slice(&[41; 32]).unwrap(), SecretKey::from_slice(&[41; 32]).unwrap(), [41; 32], diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index f600397e060..399bd1f6d33 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -1147,7 +1147,7 @@ mod tests { ), ]), }; - let signer = KeysManager::new(&[42; 32], 42, 42); + let signer = KeysManager::new(&[42; 32], 42, 42, true); let logger = TestLogger::new(); let handler = BumpTransactionEventHandlerSync::new(&broadcaster, &source, &signer, &logger); diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 79762de2f5b..4ee60142603 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -15987,6 +15987,7 @@ mod tests { SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), + true, SecretKey::from_slice(&>::from_hex("3333333333333333333333333333333333333333333333333333333333333333").unwrap()[..]).unwrap(), SecretKey::from_slice(&>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 41af9145152..3878239e2e0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -19083,7 +19083,7 @@ pub mod bench { config.channel_handshake_config.minimum_depth = 1; let seed_a = [1u8; 32]; - let keys_manager_a = KeysManager::new(&seed_a, 42, 42); + let keys_manager_a = KeysManager::new(&seed_a, 42, 42, true); let chain_monitor_a = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_a, &keys_manager_a, keys_manager_a.get_peer_storage_key()); let node_a = ChannelManager::new(&fee_estimator, &chain_monitor_a, &tx_broadcaster, &router, &message_router, &logger_a, &keys_manager_a, &keys_manager_a, &keys_manager_a, config.clone(), ChainParameters { network, @@ -19093,7 +19093,7 @@ pub mod bench { let logger_b = test_utils::TestLogger::with_id("node a".to_owned()); let seed_b = [2u8; 32]; - let keys_manager_b = KeysManager::new(&seed_b, 42, 42); + let keys_manager_b = KeysManager::new(&seed_b, 42, 42, true); let chain_monitor_b = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_b, &keys_manager_b, keys_manager_b.get_peer_storage_key()); let node_b = ChannelManager::new(&fee_estimator, &chain_monitor_b, &tx_broadcaster, &router, &message_router, &logger_b, &keys_manager_b, &keys_manager_b, &keys_manager_b, config.clone(), ChainParameters { network, diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index f26ef03a74f..e838805bae4 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -4167,10 +4167,10 @@ pub fn fail_payment<'a, 'b, 'c>( } pub fn create_chanmon_cfgs(node_count: usize) -> Vec { - create_chanmon_cfgs_with_keys(node_count, None) + create_chanmon_cfgs_with_legacy_keys(node_count, None) } -pub fn create_chanmon_cfgs_with_keys( +pub fn create_chanmon_cfgs_with_legacy_keys( node_count: usize, predefined_keys_ids: Option>, ) -> Vec { let mut chan_mon_cfgs = Vec::new(); @@ -4181,7 +4181,12 @@ pub fn create_chanmon_cfgs_with_keys( let logger = test_utils::TestLogger::with_id(format!("node {}", i)); let persister = test_utils::TestPersister::new(); let seed = [i as u8; 32]; - let keys_manager = test_utils::TestKeysInterface::new(&seed, Network::Testnet); + let keys_manager = if predefined_keys_ids.is_some() { + // Use legacy (V1) remote_key derivation for tests using legacy key sets. + test_utils::TestKeysInterface::with_v1_remote_key_derivation(&seed, Network::Testnet) + } else { + test_utils::TestKeysInterface::new(&seed, Network::Testnet) + }; let scorer = RwLock::new(test_utils::TestScorer::new()); // Set predefined keys_id if provided diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index c08d4fa14c5..7c0190a23a9 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -1211,7 +1211,7 @@ mod test { fn make_dyn_keys_interface(seed: &[u8; 32]) -> DynKeysInterface { let cross_node_seed = [44u8; 32]; - let inner = PhantomKeysManager::new(&seed, 43, 44, &cross_node_seed); + let inner = PhantomKeysManager::new(&seed, 43, 44, &cross_node_seed, true); let dyn_inner = DynPhantomKeysInterface::new(inner); DynKeysInterface::new(Box::new(dyn_inner)) } diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index ba3312c9f15..d1d71399051 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -2301,7 +2301,7 @@ fn do_test_restored_packages_retry(check_old_monitor_retries_after_upgrade: bool let node1_key_id = <[u8; 32]>::from_hex("0000000000000000000000004D49E5DAD000D6201F116BAFD379F1D61DF161B9").unwrap(); let predefined_keys_ids = Some(vec![node0_key_id, node1_key_id]); - let chanmon_cfgs = create_chanmon_cfgs_with_keys(2, predefined_keys_ids); + let chanmon_cfgs = create_chanmon_cfgs_with_legacy_keys(2, predefined_keys_ids); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let persister; let new_chain_monitor; @@ -2451,7 +2451,9 @@ fn do_test_monitor_rebroadcast_pending_claims(anchors: bool) { if should_bump { assert!(htlc_tx_feerate > prev_htlc_tx_feerate.take().unwrap()); } else if let Some(prev_feerate) = prev_htlc_tx_feerate.take() { - assert_eq!(htlc_tx_feerate, prev_feerate); + // Feerates may fluctuate marginally based on signature size + assert!(htlc_tx_feerate >= prev_feerate - 1); + assert!(htlc_tx_feerate <= prev_feerate + 1); } prev_htlc_tx_feerate = Some(htlc_tx_feerate); Some(htlc_tx) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 0934c6c812b..84b1338b1a7 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -663,9 +663,9 @@ mod tests { // adding an intermediate onion layer, causing the receiver to error with "final payload // provided for us as an intermediate node." let secp_ctx = Secp256k1::new(); - let bob = crate::sign::KeysManager::new(&[2; 32], 42, 42); + let bob = crate::sign::KeysManager::new(&[2; 32], 42, 42, true); let bob_pk = PublicKey::from_secret_key(&secp_ctx, &bob.get_node_secret_key()); - let charlie = crate::sign::KeysManager::new(&[3; 32], 42, 42); + let charlie = crate::sign::KeysManager::new(&[3; 32], 42, 42, true); let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key()); let ( @@ -693,9 +693,9 @@ mod tests { use super::*; let secp_ctx = Secp256k1::new(); - let bob = crate::sign::KeysManager::new(&[2; 32], 42, 42); + let bob = crate::sign::KeysManager::new(&[2; 32], 42, 42, true); let bob_pk = PublicKey::from_secret_key(&secp_ctx, &bob.get_node_secret_key()); - let charlie = crate::sign::KeysManager::new(&[3; 32], 42, 42); + let charlie = crate::sign::KeysManager::new(&[3; 32], 42, 42, true); let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key()); let (session_priv, total_amt_msat, cur_height, recipient_onion, preimage, payment_hash, diff --git a/lightning/src/ln/our_peer_storage.rs b/lightning/src/ln/our_peer_storage.rs index 178637430b1..ab0e9783ffa 100644 --- a/lightning/src/ln/our_peer_storage.rs +++ b/lightning/src/ln/our_peer_storage.rs @@ -37,7 +37,7 @@ use crate::prelude::*; /// use lightning::ln::our_peer_storage::DecryptedOurPeerStorage; /// use lightning::sign::{KeysManager, NodeSigner}; /// let seed = [1u8; 32]; -/// let keys_mgr = KeysManager::new(&seed, 42, 42); +/// let keys_mgr = KeysManager::new(&seed, 42, 42, true); /// let key = keys_mgr.get_peer_storage_key(); /// let decrypted_ops = DecryptedOurPeerStorage::new(vec![1, 2, 3]); /// let our_peer_storage = decrypted_ops.encrypt(&key, &[0u8; 32]); diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 7961828b1b2..54eb16b5266 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -561,7 +561,7 @@ mod tests { #[test] #[cfg(feature = "dnssec")] fn test_expiry() { - let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0); + let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0, true); let resolver = OMNameResolver::new(42, 42); let name = HumanReadableName::new("user", "example.com").unwrap(); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 890eee8859b..9a2c06bb72f 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -222,7 +222,7 @@ where /// # } /// # let seed = [42u8; 32]; /// # let time = Duration::from_secs(123456); -/// # let keys_manager = KeysManager::new(&seed, time.as_secs(), time.subsec_nanos()); +/// # let keys_manager = KeysManager::new(&seed, time.as_secs(), time.subsec_nanos(), true); /// # let logger = Arc::new(FakeLogger {}); /// # let node_secret = SecretKey::from_slice(&>::from_hex("0101010101010101010101010101010101010101010101010101010101010101").unwrap()[..]).unwrap(); /// # let secp_ctx = Secp256k1::new(); diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index f3869532d34..fc74e9a9a5d 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -1163,6 +1163,8 @@ pub struct InMemorySigner { /// Holder secret key used for our balance in counterparty-broadcasted commitment transactions, /// new-style derivation. payment_key_v2: SecretKey, + /// Which of [`Self::payment_key_v1`] and [`Self::payment_key_v2`] to use by default. + v2_remote_key_derivation: bool, /// Holder secret key used in an HTLC transaction. pub delayed_payment_base_key: SecretKey, /// Holder HTLC secret key used in commitment transaction HTLC outputs. @@ -1181,6 +1183,7 @@ impl PartialEq for InMemorySigner { && self.revocation_base_key == other.revocation_base_key && self.payment_key_v1 == other.payment_key_v1 && self.payment_key_v2 == other.payment_key_v2 + && self.v2_remote_key_derivation == other.v2_remote_key_derivation && self.delayed_payment_base_key == other.delayed_payment_base_key && self.htlc_base_key == other.htlc_base_key && self.commitment_seed == other.commitment_seed @@ -1195,6 +1198,7 @@ impl Clone for InMemorySigner { revocation_base_key: self.revocation_base_key.clone(), payment_key_v1: self.payment_key_v1.clone(), payment_key_v2: self.payment_key_v2.clone(), + v2_remote_key_derivation: self.v2_remote_key_derivation, delayed_payment_base_key: self.delayed_payment_base_key.clone(), htlc_base_key: self.htlc_base_key.clone(), commitment_seed: self.commitment_seed.clone(), @@ -1208,14 +1212,16 @@ impl InMemorySigner { #[cfg(any(feature = "_test_utils", test))] pub fn new( funding_key: SecretKey, revocation_base_key: SecretKey, payment_key_v1: SecretKey, - payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, - commitment_seed: [u8; 32], channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], + payment_key_v2: SecretKey, v2_remote_key_derivation: bool, + delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, commitment_seed: [u8; 32], + channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], ) -> InMemorySigner { InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, payment_key_v1, payment_key_v2, + v2_remote_key_derivation, delayed_payment_base_key, htlc_base_key, commitment_seed, @@ -1227,14 +1233,16 @@ impl InMemorySigner { #[cfg(not(any(feature = "_test_utils", test)))] fn new( funding_key: SecretKey, revocation_base_key: SecretKey, payment_key_v1: SecretKey, - payment_key_v2: SecretKey, delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, - commitment_seed: [u8; 32], channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], + payment_key_v2: SecretKey, v2_remote_key_derivation: bool, + delayed_payment_base_key: SecretKey, htlc_base_key: SecretKey, commitment_seed: [u8; 32], + channel_keys_id: [u8; 32], rand_bytes_unique_start: [u8; 32], ) -> InMemorySigner { InMemorySigner { funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, payment_key_v1, payment_key_v2, + v2_remote_key_derivation, delayed_payment_base_key, htlc_base_key, commitment_seed, @@ -1443,12 +1451,13 @@ impl ChannelSigner for InMemorySigner { fn new_pubkeys( &self, splice_parent_funding_txid: Option, secp_ctx: &Secp256k1, ) -> ChannelPublicKeys { + let payment_key = + if self.v2_remote_key_derivation { &self.payment_key_v2 } else { &self.payment_key_v1 }; let from_secret = |s: &SecretKey| PublicKey::from_secret_key(secp_ctx, s); let mut pubkeys = ChannelPublicKeys { funding_pubkey: from_secret(&self.funding_key.0), revocation_basepoint: RevocationBasepoint::from(from_secret(&self.revocation_base_key)), - // TODO: Make the payment_key used dynamic - payment_point: from_secret(&self.payment_key_v1), + payment_point: from_secret(payment_key), delayed_payment_basepoint: DelayedPaymentBasepoint::from(from_secret( &self.delayed_payment_base_key, )), @@ -1914,6 +1923,7 @@ pub struct KeysManager { shutdown_pubkey: PublicKey, channel_master_key: Xpriv, static_payment_key: Xpriv, + v2_remote_key_derivation: bool, channel_child_index: AtomicUsize, peer_storage_key: PeerStorageKey, receive_auth_key: ReceiveAuthKey, @@ -1945,8 +1955,16 @@ impl KeysManager { /// [`ChannelMonitor`] data, though a current copy of [`ChannelMonitor`] data is also required /// for any channel, and some on-chain during-closing funds. /// + /// If `v2_remote_key_derivation` is set, the `script_pubkey`s which receive funds on-chain when + /// our counterparty force-closes will be one of a static set of [`STATIC_PAYMENT_KEY_COUNT`]*2 + /// possible `script_pubkey`s. This only applies to new or spliced channels, however if this is + /// set you *MUST NOT* downgrade to a version of LDK prior to 0.2. + /// /// [`ChannelMonitor`]: crate::chain::channelmonitor::ChannelMonitor - pub fn new(seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32) -> Self { + pub fn new( + seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, + v2_remote_key_derivation: bool, + ) -> Self { // Constants for key derivation path indices used in this function. const NODE_SECRET_INDEX: ChildNumber = ChildNumber::Hardened { index: 0 }; const DESTINATION_SCRIPT_INDEX: ChildNumber = ChildNumber::Hardened { index: 1 }; @@ -2029,7 +2047,9 @@ impl KeysManager { channel_master_key, channel_child_index: AtomicUsize::new(0), + static_payment_key, + v2_remote_key_derivation, entropy_source: RandomBytes::new(rand_bytes_unique_start), @@ -2113,6 +2133,7 @@ impl KeysManager { revocation_base_key, payment_key_v1, self.derive_payment_key_v2(&commitment_seed), + self.v2_remote_key_derivation, delayed_payment_base_key, htlc_base_key, commitment_seed, @@ -2516,8 +2537,8 @@ impl PhantomKeysManager { /// that is shared across all nodes that intend to participate in [phantom node payments] /// together. /// - /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and - /// `starting_time_nanos`. + /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, + /// `starting_time_nanos`, and `v2_remote_key_derivation`. /// /// `cross_node_seed` must be the same across all phantom payment-receiving nodes and also the /// same across restarts, or else inbound payments may fail. @@ -2525,9 +2546,14 @@ impl PhantomKeysManager { /// [phantom node payments]: PhantomKeysManager pub fn new( seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, - cross_node_seed: &[u8; 32], + cross_node_seed: &[u8; 32], v2_remote_key_derivation: bool, ) -> Self { - let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); + let inner = KeysManager::new( + seed, + starting_time_secs, + starting_time_nanos, + v2_remote_key_derivation, + ); let (inbound_key, phantom_key) = hkdf_extract_expand_twice( b"LDK Inbound and Phantom Payment Key Expansion", cross_node_seed, @@ -2605,7 +2631,8 @@ pub mod benches { pub fn bench_get_secure_random_bytes(bench: &mut Criterion) { let seed = [0u8; 32]; let now = Duration::from_secs(genesis_block(Network::Testnet).header.time as u64); - let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_micros())); + let keys_manager = + Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_micros(), true)); let mut handles = Vec::new(); let mut stops = Vec::new(); diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index deeb3a38e9f..eb7af681b79 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1893,7 +1893,7 @@ pub static SIGNER_FACTORY: MutGlobal> = pub trait TestSignerFactory: Send + Sync { /// Make a dynamic signer fn make_signer( - &self, seed: &[u8; 32], now: Duration, + &self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool, ) -> Box>; } @@ -1902,9 +1902,15 @@ struct DefaultSignerFactory(); impl TestSignerFactory for DefaultSignerFactory { fn make_signer( - &self, seed: &[u8; 32], now: Duration, + &self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool, ) -> Box> { - let phantom = sign::PhantomKeysManager::new(seed, now.as_secs(), now.subsec_nanos(), seed); + let phantom = sign::PhantomKeysManager::new( + seed, + now.as_secs(), + now.subsec_nanos(), + seed, + v2_remote_key_derivation, + ); let dphantom = DynPhantomKeysInterface::new(phantom); let backing = Box::new(dphantom) as Box>; backing @@ -1912,15 +1918,7 @@ impl TestSignerFactory for DefaultSignerFactory { } impl TestKeysInterface { - pub fn new(seed: &[u8; 32], network: Network) -> Self { - #[cfg(feature = "std")] - let factory = SIGNER_FACTORY.get(); - - #[cfg(not(feature = "std"))] - let factory = DefaultSignerFactory(); - - let now = Duration::from_secs(genesis_block(network).header.time as u64); - let backing = factory.make_signer(seed, now); + fn build(backing: Box>) -> Self { Self { backing: DynKeysInterface::new(backing), override_random_bytes: Mutex::new(None), @@ -1934,6 +1932,30 @@ impl TestKeysInterface { } } + pub fn new(seed: &[u8; 32], network: Network) -> Self { + #[cfg(feature = "std")] + let factory = SIGNER_FACTORY.get(); + + #[cfg(not(feature = "std"))] + let factory = DefaultSignerFactory(); + + let now = Duration::from_secs(genesis_block(network).header.time as u64); + let backing = factory.make_signer(seed, now, true); + Self::build(backing) + } + + pub fn with_v1_remote_key_derivation(seed: &[u8; 32], network: Network) -> Self { + #[cfg(feature = "std")] + let factory = SIGNER_FACTORY.get(); + + #[cfg(not(feature = "std"))] + let factory = DefaultSignerFactory(); + + let now = Duration::from_secs(genesis_block(network).header.time as u64); + let backing = factory.make_signer(seed, now, false); + Self::build(backing) + } + /// Sets an expectation that [`sign::SignerProvider::get_shutdown_scriptpubkey`] is /// called. pub fn expect(&self, expectation: OnGetShutdownScriptpubkey) -> &Self { From 5f34d5984dd5b02195a66fad9d5f20a11219e1d6 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 24 Sep 2025 14:08:57 +0000 Subject: [PATCH 4/5] Add a method to fetch all possible remote-closure `script_pubkey`s In the previous commit we (finally) allowed users to opt into a static `remote_key` derivation scheme, enabling them to scan the chain for funds on counterparty commitment transactions without any state at all. This is only possible, however, of course, if they have the full list of scripts to scan the chain for, which we expose here. --- lightning/src/sign/mod.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index fc74e9a9a5d..40d24778249 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -2070,6 +2070,35 @@ 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 the channel. + /// + /// Only channels opened or spliced when using a [`KeysManager`] with the + /// `v2_remote_key_derivation` argument to [`KeysManager::new`] will close to such scripts, + /// other channels will close to a randomly-generated `script_pubkey`. + pub fn possible_v2_counterparty_closed_balance_spks( + &self, secp_ctx: &Secp256k1, + ) -> Vec { + 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, params: &[u8; 32]) -> SecretKey { let mut eight_bytes = [0; 8]; eight_bytes.copy_from_slice(¶ms[0..8]); @@ -2171,6 +2200,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 sighers 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, From 9871b974a98793f21d38586cc4ccf88f4178e6d9 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 25 Sep 2025 00:18:38 +0000 Subject: [PATCH 5/5] Add a test of upgrading an old node with V1 `remote_key` derivation --- .github/workflows/build.yml | 2 + .../src/upgrade_downgrade_tests.rs | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8cfe44dfe4..2190dd55c4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -287,3 +287,5 @@ jobs: rustup component add rustfmt - name: Run rustfmt checks run: cargo fmt --check + - name: Run rustfmt checks on lightning-tests + run: cd lightning-tests && cargo fmt --check diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index 2b57cd23a9a..9e60ae847d7 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -10,6 +10,7 @@ //! Tests which test upgrading from previous versions of LDK or downgrading to previous versions of //! LDK. +use lightning_0_1::events::ClosureReason as ClosureReason_0_1; use lightning_0_1::get_monitor as get_monitor_0_1; use lightning_0_1::ln::functional_test_utils as lightning_0_1_utils; use lightning_0_1::util::ser::Writeable as _; @@ -28,10 +29,19 @@ use lightning_0_0_125::ln::msgs::ChannelMessageHandler as _; use lightning_0_0_125::routing::router as router_0_0_125; use lightning_0_0_125::util::ser::Writeable as _; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::events::{ClosureReason, Event}; use lightning::ln::functional_test_utils::*; +use lightning::sign::OutputSpender; use lightning_types::payment::PaymentPreimage; +use bitcoin::opcodes; +use bitcoin::script::Builder; +use bitcoin::secp256k1::Secp256k1; + +use std::sync::Arc; + #[test] fn simple_upgrade() { // Tests a simple case of upgrading from LDK 0.1 with a pending payment @@ -213,3 +223,85 @@ fn test_125_dangling_post_update_actions() { let config = test_default_channel_config(); reload_node!(nodes[3], config, &node_d_ser, &[&mon_ser], persister, chain_mon, node); } + +#[test] +fn test_0_1_legacy_remote_key_derivation() { + // Test that a channel opened with a v1/legacy `remote_key` derivation will be properly spent + // even after upgrading to 0.2 and opting into the new v2 derivation for new channels. + let (node_a_ser, node_b_ser, mon_a_ser, mon_b_ser, commitment_tx, channel_id); + let node_a_blocks; + { + let chanmon_cfgs = lightning_0_1_utils::create_chanmon_cfgs(2); + let node_cfgs = lightning_0_1_utils::create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = lightning_0_1_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = lightning_0_1_utils::create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + + let chan_id = lightning_0_1_utils::create_announced_chan_between_nodes(&nodes, 0, 1).2; + channel_id = chan_id.0; + + let err = "".to_owned(); + nodes[1].node.force_close_broadcasting_latest_txn(&chan_id, &node_a_id, err).unwrap(); + commitment_tx = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + assert_eq!(commitment_tx.len(), 1); + + lightning_0_1_utils::check_added_monitors(&nodes[1], 1); + let reason = ClosureReason_0_1::HolderForceClosed { broadcasted_latest_txn: Some(true) }; + lightning_0_1_utils::check_closed_event(&nodes[1], 1, reason, false, &[node_a_id], 100000); + lightning_0_1_utils::check_closed_broadcast(&nodes[1], 1, true); + + node_a_ser = nodes[0].node.encode(); + node_b_ser = nodes[1].node.encode(); + mon_a_ser = get_monitor_0_1!(nodes[0], chan_id).encode(); + mon_b_ser = get_monitor_0_1!(nodes[1], chan_id).encode(); + + node_a_blocks = Arc::clone(&nodes[0].blocks); + } + + // Create a dummy node to reload over with the 0.1 state + + let mut chanmon_cfgs = create_chanmon_cfgs(2); + + // Our TestChannelSigner will fail as we're jumping ahead, so disable its state-based checks + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_a, persister_b, chain_mon_a, chain_mon_b); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let (node_a, node_b); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let config = test_default_channel_config(); + let a_mons = &[&mon_a_ser[..]]; + reload_node!(nodes[0], config.clone(), &node_a_ser, a_mons, persister_a, chain_mon_a, node_a); + reload_node!(nodes[1], config, &node_b_ser, &[&mon_b_ser], persister_b, chain_mon_b, node_b); + + nodes[0].blocks = node_a_blocks; + + let node_b_id = nodes[1].node.get_our_node_id(); + + mine_transaction(&nodes[0], &commitment_tx[0]); + let reason = ClosureReason::CommitmentTxConfirmed; + check_closed_event(&nodes[0], 1, reason, false, &[node_b_id], 100_000); + check_added_monitors(&nodes[0], 1); + check_closed_broadcast(&nodes[0], 1, false); + + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + let mut spendable_event = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); + assert_eq!(spendable_event.len(), 1); + if let Event::SpendableOutputs { outputs, channel_id: ev_id } = spendable_event.pop().unwrap() { + assert_eq!(ev_id.unwrap().0, channel_id); + assert_eq!(outputs.len(), 1); + let spk = Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(); + let spend_tx = nodes[0] + .keys_manager + .backing + .spend_spendable_outputs(&[&outputs[0]], Vec::new(), spk, 253, None, &Secp256k1::new()) + .unwrap(); + check_spends!(spend_tx, commitment_tx[0]); + } else { + panic!("Wrong event"); + } +}