Allow `KeysManager` to opt-into the new `remote_key` derivation
What changed, and why it matters
This commit adds an opt-in feature to LDK's key manager that changes how on-chain backup addresses are derived when a channel is force-closed by the other party. The new derivation makes these addresses come from a small, predictable set, so a user who loses all channel state but still has their seed can scan the blockchain and recover funds. Without this, the old derivation required per-channel secrets that might be lost, making funds unrecoverable in a disaster scenario. It is a safety/recovery improvement, not a fix for an active exploit.
No immediate action is required; this is a defensive recovery feature. Node operators who value disaster recovery should opt into `v2_remote_key_derivation` when creating new `KeysManager` instances, understanding that enabling it for new channels prevents downgrades to LDK versions before 0.2. Operators should ensure backups of channel state remain the primary protection, with this feature as a safety net.
Security signals we found
New key derivation flag added to `KeysManager::new` and `PhantomKeysManager::new`
Old `remote_key` derivation depended on `channel_keys_id`, making funds unrecoverable if channel state is lost
New derivation uses a static set of keys, enabling blockchain scanning for disaster recovery
Spliced channels are forced onto the new derivation because downgrades are already broken by splicing
Documentation warns users must not downgrade to LDK < 0.2 if the flag is enabled
Evidence from the diff
The change introduces a new boolean argument v2_remote_key_derivation to KeysManager::new and PhantomKeysManager::new. When enabled, InMemorySigner stores a flag and uses payment_key_v2 for the payment_point in ChannelPublicKeys for new channels and for all spliced channels. The v2 remote key is derived from a static path (static_payment_key) yielding a small, enumerable set of script_pubkeys (STATIC_PAYMENT_KEY_COUNT * 2). This allows chain scanning for recoverable outputs if only the master seed remains. Existing channels keep their original derivation unless spliced. The commit also updates tests and fuzz harnesses to pass the new parameter, and adds a legacy test helper for tests that rely on v1 derivation.
Changed components
lightning/src/sign/mod.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/chain/channelmonitor.rslightning/src/util/test_utils.rsfuzz and test harnesses across the repositoryInspect captured patch +118 / −51
diff --git a/ext-functional-test-demo/src/main.rs b/ext-functional-test-demo/src/main.rs
index 943bacf..654cf91 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<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
panic!()
}
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 57a80f3..b697faf 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -391,6 +391,7 @@ impl SignerProvider for KeyProvider {
// real reason to fuzz differences here.
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 5d500d4..277dc62 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -478,7 +478,7 @@ impl SignerProvider for KeyProvider {
f = key;
// We leave both the v1 and v2 derivation to_remote keys the same as there's not any real
// reason to fuzz differences here, and it keeps us consistent with past behavior.
- 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 2bc83c3..8dfa922 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 44ce52b..dc6fbc7 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 75fe06f..f5b1d53 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<MessageForwardNode>, secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedMessagePath>, ()> {
- 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 d6278a1..8244680 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 77a2449..48bd40a 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 f600397..399bd1f 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 79762de..4ee6014 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -15987,6 +15987,7 @@ mod tests {
SecretKey::from_slice(&<Vec<u8>>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(),
SecretKey::from_slice(&<Vec<u8>>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(),
SecretKey::from_slice(&<Vec<u8>>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(),
+ true,
SecretKey::from_slice(&<Vec<u8>>::from_hex("3333333333333333333333333333333333333333333333333333333333333333").unwrap()[..]).unwrap(),
SecretKey::from_slice(&<Vec<u8>>::from_hex("1111111111111111111111111111111111111111111111111111111111111111").unwrap()[..]).unwrap(),
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 41af914..3878239 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 f26ef03..e838805 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<TestChanMonCfg> {
- 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<[u8; 32]>>,
) -> Vec<TestChanMonCfg> {
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 c08d4fa..7c0190a 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 ba3312c..d1d7139 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 0934c6c..84b1338 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 1786374..ab0e978 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 7961828..54eb16b 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 890eee8..9a2c06b 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(&<Vec<u8>>::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 bde241e..2417a8d 100644
--- a/lightning/src/sign/mod.rs
+++ b/lightning/src/sign/mod.rs
@@ -141,9 +141,9 @@ 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.
+/// If a [`KeysManager`] is built with [`KeysManager::new`] with `v2_remote_key_derivation` set
+/// (and for all channels after they've been spliced), the script which we receive funds to on-chain
+/// when our counterparty force-closes a channel is one of this many possible derivation paths.
///
/// Keeping 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
@@ -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,17 @@ impl ChannelSigner for InMemorySigner {
fn new_pubkeys(
&self, splice_parent_funding_txid: Option<Txid>, secp_ctx: &Secp256k1<secp256k1::All>,
) -> ChannelPublicKeys {
+ // Because splices always break downgrades, we go ahead and always use the new derivation
+ // here as its just much better.
+ let use_v2_derivation =
+ self.v2_remote_key_derivation || splice_parent_funding_txid.is_some();
+ let payment_key =
+ if use_v2_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 +1927,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 +1959,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 +2051,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),
@@ -2114,6 +2138,7 @@ impl KeysManager {
revocation_base_key,
payment_key_v1,
self.derive_payment_key_v2(payment_key_v2_idx),
+ self.v2_remote_key_derivation,
delayed_payment_base_key,
htlc_base_key,
commitment_seed,
@@ -2517,8 +2542,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.
@@ -2526,9 +2551,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,
@@ -2606,7 +2636,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 deeb3a3..eb7af68 100644
--- a/lightning/src/util/test_utils.rs
+++ b/lightning/src/util/test_utils.rs
@@ -1893,7 +1893,7 @@ pub static SIGNER_FACTORY: MutGlobal<Arc<dyn TestSignerFactory>> =
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<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>>;
}
@@ -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<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
- 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<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>>;
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<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>>) -> 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 {
Why this scored 31/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.