From 08edfa71521d000c5dc0b342e0429bbdc0b50fdb Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 27 Feb 2025 11:59:32 -0800 Subject: [PATCH] Support scalar tweak to rotate holder funding key during splicing A scalar tweak applied to the base funding key to obtain the channel's funding key used in the 2-of-2 multisig. This is used to derive additional keys from the same secret backing the base `funding_pubkey`, as we have to rotate keys for each successful splice attempt. The tweak is computed similar to existing tweaks used in [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md#key-derivation), but rather than using the `per_commitment_point`, we use the txid of the funding transaction the splice transaction is spending to guarantee uniqueness, and the `revocation_basepoint` to guarantee only the channel participants can re-derive the new funding key. tweak = SHA256(splice_parent_funding_txid || revocation_basepoint || base_funding_pubkey) tweaked_funding_key = base_funding_key + tweak --- lightning/src/chain/channelmonitor.rs | 10 +- lightning/src/chain/onchaintx.rs | 7 +- lightning/src/ln/chan_utils.rs | 154 ++++++++++++++++++++-- lightning/src/ln/channel.rs | 23 ++-- lightning/src/ln/dual_funding_tests.rs | 6 +- lightning/src/sign/ecdsa.rs | 8 +- lightning/src/sign/mod.rs | 89 +++++++++---- lightning/src/util/ser.rs | 20 ++- lightning/src/util/test_channel_signer.rs | 11 +- 9 files changed, 267 insertions(+), 61 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index df6cf06c061..adc87b48dd8 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -3461,7 +3461,7 @@ impl ChannelMonitorImpl { let broadcaster_keys = &self.onchain_tx_handler.channel_transaction_parameters .counterparty_parameters.as_ref().unwrap().pubkeys; let countersignatory_keys = - &self.onchain_tx_handler.channel_transaction_parameters.holder_pubkeys; + self.onchain_tx_handler.channel_transaction_parameters.holder_pubkeys.as_ref(); let broadcaster_funding_key = broadcaster_keys.funding_pubkey; let countersignatory_funding_key = countersignatory_keys.funding_pubkey; @@ -5136,7 +5136,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP if onchain_tx_handler.channel_type_features().supports_anchors_zero_fee_htlc_tx() && counterparty_payment_script.is_p2wpkh() { - let payment_point = onchain_tx_handler.channel_transaction_parameters.holder_pubkeys.payment_point; + let payment_point = onchain_tx_handler.channel_transaction_parameters.holder_pubkeys.as_ref().payment_point; counterparty_payment_script = chan_utils::get_to_countersignatory_with_anchors_redeemscript(&payment_point).to_p2wsh(); } @@ -5235,7 +5235,7 @@ mod tests { use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; use crate::ln::channel_keys::{DelayedPaymentBasepoint, DelayedPaymentKey, HtlcBasepoint, RevocationBasepoint, RevocationKey}; - use crate::ln::chan_utils::{self,HTLCOutputInCommitment, ChannelPublicKeys, ChannelTransactionParameters, HolderCommitmentTransaction, CounterpartyChannelTransactionParameters}; + use crate::ln::chan_utils::{self,HTLCOutputInCommitment, ChannelPublicKeys, ChannelTransactionParameters, HolderChannelPublicKeys, HolderCommitmentTransaction, CounterpartyChannelTransactionParameters}; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; use crate::ln::script::ShutdownScript; @@ -5415,7 +5415,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: HolderChannelPublicKeys::from(keys.holder_channel_pubkeys.clone()), holder_selected_contest_delay: 66, is_outbound_from_holder: true, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { @@ -5667,7 +5667,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: HolderChannelPublicKeys::from(keys.holder_channel_pubkeys.clone()), 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 457d0512e4c..299d12d02d7 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -665,7 +665,8 @@ impl OnchainTxHandler { } // We'll locate an anchor output we can spend within the commitment transaction. - let funding_pubkey = &self.channel_transaction_parameters.holder_pubkeys.funding_pubkey; + let funding_pubkey = + &self.channel_transaction_parameters.holder_pubkeys.as_ref().funding_pubkey; match chan_utils::get_anchor_output(&tx.0, funding_pubkey) { // An anchor output was found, so we should yield a funding event externally. Some((idx, _)) => { @@ -1290,7 +1291,7 @@ mod tests { use crate::chain::transaction::OutPoint; use crate::ln::chan_utils::{ ChannelPublicKeys, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, - HTLCOutputInCommitment, HolderCommitmentTransaction, + HTLCOutputInCommitment, HolderChannelPublicKeys, HolderCommitmentTransaction, }; use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; use crate::ln::functional_test_utils::create_dummy_block; @@ -1344,7 +1345,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: HolderChannelPublicKeys::from(signer.holder_channel_pubkeys.clone()), 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 2447386b72b..e1b80ad038b 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -35,7 +35,7 @@ use crate::util::transaction_utils; use bitcoin::locktime::absolute::LockTime; use bitcoin::ecdsa::Signature as BitcoinSignature; -use bitcoin::secp256k1::{SecretKey, PublicKey, Scalar}; +use bitcoin::secp256k1::{SecretKey, PublicKey, Scalar, Verification}; use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature, Message}; use bitcoin::{secp256k1, Sequence, Witness}; @@ -430,6 +430,26 @@ pub fn derive_private_revocation_key(secp_ctx: &Secp256k1 .expect("Addition only fails if the tweak is the inverse of the key. This is not possible when the tweak commits to the key.") } +/// Computes the tweak to apply to the base funding key of a channel. +/// +/// The tweak is computed similar to existing tweaks used in +/// [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md#key-derivation), but +/// rather than using the `per_commitment_point`, we use the txid of the funding transaction the +/// splice transaction is spending to guarantee uniqueness, and the `revocation_basepoint` to +/// guarantee only the channel participants can re-derive the new funding key. +/// +/// tweak = SHA256(splice_parent_funding_txid || revocation_basepoint || base_funding_pubkey) +/// tweaked_funding_key = base_funding_key + tweak +// +// TODO: Expose a helper on `FundingScope` that calls this. +pub fn compute_funding_key_tweak(base_funding_pubkey: &PublicKey, revocation_basepoint: &PublicKey, splice_parent_funding_txid: &Txid) -> Scalar { + let mut sha = Sha256::engine(); + sha.input(splice_parent_funding_txid.as_byte_array()); + sha.input(&revocation_basepoint.serialize()); + sha.input(&base_funding_pubkey.serialize()); + Scalar::from_be_bytes(Sha256::from_engine(sha).to_byte_array()).unwrap() +} + /// The set of public keys which are used in the creation of one commitment transaction. /// These are derived from the channel base keys and per-commitment data. /// @@ -470,6 +490,9 @@ impl_writeable_tlv_based!(TxCreationKeys, { pub struct ChannelPublicKeys { /// The public key which is used to sign all commitment transactions, as it appears in the /// on-chain channel lock-in 2-of-2 multisig output. + /// + /// NOTE: This key will already have the [`HolderChannelPublicKeys::funding_key_tweak`] applied + /// if one existed. pub funding_pubkey: PublicKey, /// The base point which is used (with [`RevocationKey::from_basepoint`]) to derive per-commitment /// revocation keys. This is combined with the per-commitment-secret generated by the @@ -497,6 +520,113 @@ impl_writeable_tlv_based!(ChannelPublicKeys, { (8, htlc_basepoint, required), }); +/// The holder's public keys which do not change over the life of a channel, except for the +/// `funding_pubkey`, which may rotate after each successful splice attempt via the +/// `funding_key_tweak`. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct HolderChannelPublicKeys { + keys: ChannelPublicKeys, + /// A optional scalar tweak applied to the base funding key to obtain the channel's funding key + /// used in the 2-of-2 multisig. This is used to derive additional keys from the same secret + /// backing the base `funding_pubkey`, as we have to rotate keys for each successful splice + /// attempt. The tweak is computed as described in [`compute_funding_key_tweak`]. + // + // TODO: Expose `splice_parent_funding_txid` instead so the signer can re-derive the tweak? + // There's no harm in the signer trusting the tweak as long as its funding secret has not + // been leaked. + pub funding_key_tweak: Option, +} + +// `HolderChannelPublicKeys` may have been previously written as `ChannelPublicKeys` so we have to +// mimic its serialization. +impl Writeable for HolderChannelPublicKeys { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + write_tlv_fields!(writer, { + (0, self.keys.funding_pubkey, required), + (2, self.keys.revocation_basepoint, required), + (4, self.keys.payment_point, required), + (6, self.keys.delayed_payment_basepoint, required), + (8, self.keys.htlc_basepoint, required), + (10, self.funding_key_tweak, option), + }); + Ok(()) + } +} + +impl Readable for HolderChannelPublicKeys { + fn read(reader: &mut R) -> Result { + let mut funding_pubkey = RequiredWrapper(None); + let mut revocation_basepoint = RequiredWrapper(None); + let mut payment_point = RequiredWrapper(None); + let mut delayed_payment_basepoint = RequiredWrapper(None); + let mut htlc_basepoint = RequiredWrapper(None); + let mut funding_key_tweak: Option = None; + + read_tlv_fields!(reader, { + (0, funding_pubkey, required), + (2, revocation_basepoint, required), + (4, payment_point, required), + (6, delayed_payment_basepoint, required), + (8, htlc_basepoint, required), + (10, funding_key_tweak, option), + }); + + Ok(Self { + keys: ChannelPublicKeys { + funding_pubkey: funding_pubkey.0.unwrap(), + revocation_basepoint: revocation_basepoint.0.unwrap(), + payment_point: payment_point.0.unwrap(), + delayed_payment_basepoint: delayed_payment_basepoint.0.unwrap(), + htlc_basepoint: htlc_basepoint.0.unwrap(), + }, + funding_key_tweak, + }) + } +} + +impl AsRef for HolderChannelPublicKeys { + fn as_ref(&self) -> &ChannelPublicKeys { + &self.keys + } +} + +impl From for HolderChannelPublicKeys { + fn from(value: ChannelPublicKeys) -> Self { + Self { + keys: value, + funding_key_tweak: None, + } + } +} + +impl HolderChannelPublicKeys { + /// Constructs a new instance of [`HolderChannelPublicKeys`]. + pub fn new( + funding_pubkey: PublicKey, revocation_basepoint: RevocationBasepoint, + payment_point: PublicKey, delayed_payment_basepoint: DelayedPaymentBasepoint, + htlc_basepoint: HtlcBasepoint, funding_key_tweak: Option, secp: &Secp256k1, + ) -> Self { + let funding_pubkey = funding_key_tweak + .map(|tweak| { + funding_pubkey + .add_exp_tweak(secp, &tweak) + .expect("Addition only fails if the tweak is the inverse of the key") + }) + .unwrap_or(funding_pubkey); + + Self { + keys: ChannelPublicKeys { + funding_pubkey, + revocation_basepoint, + payment_point, + delayed_payment_basepoint, + htlc_basepoint, + }, + funding_key_tweak, + } + } +} + impl TxCreationKeys { /// Create per-state keys from channel base points and the per-commitment point. /// Key set is asymmetric and can't be used as part of counter-signatory set of transactions. @@ -869,7 +999,7 @@ pub fn build_anchor_input_witness(funding_key: &PublicKey, funding_sig: &Signatu #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct ChannelTransactionParameters { /// Holder public keys - pub holder_pubkeys: ChannelPublicKeys, + pub holder_pubkeys: HolderChannelPublicKeys, /// The contest delay selected by the holder, which applies to counterparty-broadcast transactions pub holder_selected_contest_delay: u16, /// Whether the holder is the initiator of this channel. @@ -933,7 +1063,7 @@ impl ChannelTransactionParameters { pub(crate) fn make_funding_redeemscript(&self) -> ScriptBuf { make_funding_redeemscript( - &self.holder_pubkeys.funding_pubkey, + &self.holder_pubkeys.as_ref().funding_pubkey, &self.counterparty_parameters.as_ref().unwrap().pubkeys.funding_pubkey ) } @@ -953,7 +1083,10 @@ impl ChannelTransactionParameters { htlc_basepoint: PublicKey::from_slice(&[2; 33]).unwrap().into(), }; Self { - holder_pubkeys: dummy_keys.clone(), + holder_pubkeys: HolderChannelPublicKeys { + keys: dummy_keys.clone(), + funding_key_tweak: None, + }, holder_selected_contest_delay: 42, is_outbound_from_holder: true, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { @@ -1050,7 +1183,7 @@ impl<'a> DirectedChannelTransactionParameters<'a> { /// Get the channel pubkeys for the broadcaster pub fn broadcaster_pubkeys(&self) -> &'a ChannelPublicKeys { if self.holder_is_broadcaster { - &self.inner.holder_pubkeys + self.inner.holder_pubkeys.as_ref() } else { &self.inner.counterparty_parameters.as_ref().unwrap().pubkeys } @@ -1061,7 +1194,7 @@ impl<'a> DirectedChannelTransactionParameters<'a> { if self.holder_is_broadcaster { &self.inner.counterparty_parameters.as_ref().unwrap().pubkeys } else { - &self.inner.holder_pubkeys + self.inner.holder_pubkeys.as_ref() } } @@ -1149,7 +1282,10 @@ impl HolderCommitmentTransaction { htlc_basepoint: HtlcBasepoint::from(dummy_key.clone()) }; let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: channel_pubkeys.clone(), + holder_pubkeys: HolderChannelPublicKeys { + keys: channel_pubkeys.clone(), + funding_key_tweak: None, + }, holder_selected_contest_delay: 0, is_outbound_from_holder: false, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: channel_pubkeys.clone(), selected_contest_delay: 0 }), @@ -1918,7 +2054,7 @@ pub fn get_commitment_transaction_number_obscure_factor( #[cfg(test)] mod tests { - use super::{CounterpartyCommitmentSecrets, ChannelPublicKeys}; + use super::{CounterpartyCommitmentSecrets, ChannelPublicKeys, HolderChannelPublicKeys}; use crate::chain; use crate::ln::chan_utils::{get_htlc_redeemscript, get_to_countersignatory_with_anchors_redeemscript, CommitmentTransaction, TxCreationKeys, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, HTLCOutputInCommitment}; use bitcoin::secp256k1::{PublicKey, SecretKey, Secp256k1}; @@ -1961,7 +2097,7 @@ mod tests { let counterparty_pubkeys = counterparty_signer.pubkeys().clone(); let keys = TxCreationKeys::derive_new(&secp_ctx, &per_commitment_point, delayed_payment_base, htlc_basepoint, &counterparty_pubkeys.revocation_basepoint, &counterparty_pubkeys.htlc_basepoint); let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: holder_pubkeys.clone(), + holder_pubkeys: HolderChannelPublicKeys::from(holder_pubkeys.clone()), holder_selected_contest_delay: 0, is_outbound_from_holder: false, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys.clone(), selected_contest_delay: 0 }), diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f43ad342b54..b69cc0d7a3b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -42,7 +42,7 @@ use crate::ln::channel_state::{ChannelShutdownState, CounterpartyForwardingInfo, use crate::ln::channelmanager::{self, OpenChannelMessage, PendingHTLCStatus, HTLCSource, SentHTLCId, HTLCFailureMsg, PendingHTLCInfo, RAACommitmentOrder, PaymentClaimDetails, BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, MAX_LOCAL_BREAKDOWN_TIMEOUT}; use crate::ln::chan_utils::{ CounterpartyCommitmentSecrets, TxCreationKeys, HTLCOutputInCommitment, htlc_success_tx_weight, - htlc_timeout_tx_weight, ChannelPublicKeys, CommitmentTransaction, + htlc_timeout_tx_weight, ChannelPublicKeys, HolderChannelPublicKeys, CommitmentTransaction, HolderCommitmentTransaction, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, MAX_HTLCS, get_commitment_transaction_number_obscure_factor, @@ -1694,7 +1694,7 @@ impl FundingScope { } fn get_holder_pubkeys(&self) -> &ChannelPublicKeys { - &self.channel_transaction_parameters.holder_pubkeys + self.channel_transaction_parameters.holder_pubkeys.as_ref() } pub fn get_counterparty_selected_contest_delay(&self) -> Option { @@ -2586,7 +2586,7 @@ impl ChannelContext where SP::Target: SignerProvider { next_remote_commitment_tx_fee_info_cached: Mutex::new(None), channel_transaction_parameters: ChannelTransactionParameters { - holder_pubkeys: pubkeys, + holder_pubkeys: HolderChannelPublicKeys::from(pubkeys), holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, is_outbound_from_holder: false, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { @@ -2823,7 +2823,7 @@ impl ChannelContext where SP::Target: SignerProvider { next_remote_commitment_tx_fee_info_cached: Mutex::new(None), channel_transaction_parameters: ChannelTransactionParameters { - holder_pubkeys: pubkeys, + holder_pubkeys: HolderChannelPublicKeys::from(pubkeys), holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, is_outbound_from_holder: true, counterparty_parameters: None, @@ -8160,7 +8160,11 @@ impl FundedChannel where }; match &self.context.holder_signer { ChannelSignerType::Ecdsa(ecdsa) => { - let our_bitcoin_sig = match ecdsa.sign_channel_announcement_with_funding_key(&announcement, &self.context.secp_ctx) { + let funding_key_tweak = + self.funding.channel_transaction_parameters.holder_pubkeys.funding_key_tweak; + let our_bitcoin_sig = match ecdsa.sign_channel_announcement_with_funding_key( + &announcement, funding_key_tweak, &self.context.secp_ctx, + ) { Err(_) => { log_error!(logger, "Signer rejected channel_announcement signing. Channel will not be announced!"); return None; @@ -8201,8 +8205,11 @@ impl FundedChannel where .map_err(|_| ChannelError::Ignore("Failed to generate node signature for channel_announcement".to_owned()))?; match &self.context.holder_signer { ChannelSignerType::Ecdsa(ecdsa) => { - let our_bitcoin_sig = ecdsa.sign_channel_announcement_with_funding_key(&announcement, &self.context.secp_ctx) - .map_err(|_| ChannelError::Ignore("Signer rejected channel_announcement".to_owned()))?; + let funding_key_tweak = + self.funding.channel_transaction_parameters.holder_pubkeys.funding_key_tweak; + let our_bitcoin_sig = ecdsa.sign_channel_announcement_with_funding_key( + &announcement, funding_key_tweak, &self.context.secp_ctx, + ).map_err(|_| ChannelError::Ignore("Signer rejected channel_announcement".to_owned()))?; Ok(msgs::ChannelAnnouncement { node_signature_1: if were_node_one { our_node_sig } else { their_node_sig }, node_signature_2: if were_node_one { their_node_sig } else { our_node_sig }, @@ -10738,7 +10745,7 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch }, }; let is_v2_established = channel_id.is_v2_channel_id( - &channel_parameters.holder_pubkeys.revocation_basepoint, + &channel_parameters.holder_pubkeys.as_ref().revocation_basepoint, &channel_parameters.counterparty_parameters.as_ref() .expect("Persisted channel must have counterparty parameters").pubkeys.revocation_basepoint); diff --git a/lightning/src/ln/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs index 72fa049d6b0..f2f80eb1868 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -14,7 +14,7 @@ use { crate::events::{Event, MessageSendEvent, MessageSendEventsProvider}, crate::ln::chan_utils::{ make_funding_redeemscript, ChannelPublicKeys, ChannelTransactionParameters, - CounterpartyChannelTransactionParameters, + CounterpartyChannelTransactionParameters, HolderChannelPublicKeys, }, crate::ln::channel::PendingV2Channel, crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}, @@ -155,7 +155,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) }, selected_contest_delay: accept_channel_v2_msg.common_fields.to_self_delay, }), - holder_pubkeys: ChannelPublicKeys { + holder_pubkeys: HolderChannelPublicKeys::from(ChannelPublicKeys { funding_pubkey: open_channel_v2_msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint( open_channel_v2_msg.common_fields.revocation_basepoint, @@ -165,7 +165,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) open_channel_v2_msg.common_fields.delayed_payment_basepoint, ), htlc_basepoint: HtlcBasepoint(open_channel_v2_msg.common_fields.htlc_basepoint), - }, + }), holder_selected_contest_delay: open_channel_v2_msg.common_fields.to_self_delay, is_outbound_from_holder: true, funding_outpoint, diff --git a/lightning/src/sign/ecdsa.rs b/lightning/src/sign/ecdsa.rs index 6fbf730ae17..5afaf37ab80 100644 --- a/lightning/src/sign/ecdsa.rs +++ b/lightning/src/sign/ecdsa.rs @@ -4,7 +4,7 @@ use bitcoin::transaction::Transaction; use bitcoin::secp256k1; use bitcoin::secp256k1::ecdsa::Signature; -use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use crate::ln::chan_utils::{ ClosingTransaction, CommitmentTransaction, HTLCOutputInCommitment, HolderCommitmentTransaction, @@ -227,7 +227,8 @@ pub trait EcdsaChannelSigner: ChannelSigner { input: usize, secp_ctx: &Secp256k1, ) -> Result; /// Signs a channel announcement message with our funding key proving it comes from one of the - /// channel participants. + /// channel participants. The `funding_key_tweak`, if set, must be added to the funding secret + /// key prior to signing in order to arrive at the key found in the 2-of-2 multisig. /// /// Channel announcements also require a signature from each node's network key. Our node /// signature is computed through [`NodeSigner::sign_gossip_message`]. @@ -238,7 +239,8 @@ pub trait EcdsaChannelSigner: ChannelSigner { /// /// [`NodeSigner::sign_gossip_message`]: crate::sign::NodeSigner::sign_gossip_message fn sign_channel_announcement_with_funding_key( - &self, msg: &UnsignedChannelAnnouncement, secp_ctx: &Secp256k1, + &self, msg: &UnsignedChannelAnnouncement, funding_key_tweak: Option, + secp_ctx: &Secp256k1, ) -> Result; /// Signs the input of a splicing funding transaction with our funding key. diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index df7c2c79dec..2e52eb86a0e 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -166,7 +166,7 @@ impl StaticPaymentOutputDescriptor { pub fn witness_script(&self) -> Option { self.channel_transaction_parameters.as_ref().and_then(|channel_params| { if channel_params.supports_anchors() { - let payment_point = channel_params.holder_pubkeys.payment_point; + let payment_point = channel_params.holder_pubkeys.as_ref().payment_point; Some(chan_utils::get_to_countersignatory_with_anchors_redeemscript(&payment_point)) } else { None @@ -356,7 +356,7 @@ impl SpendableOutputDescriptor { }) => { let delayed_payment_basepoint = channel_transaction_parameters .as_ref() - .map(|params| params.holder_pubkeys.delayed_payment_basepoint); + .map(|params| params.holder_pubkeys.as_ref().delayed_payment_basepoint); let (witness_script, add_tweak) = if let Some(basepoint) = delayed_payment_basepoint.as_ref() { @@ -776,6 +776,10 @@ pub trait ChannelSigner { /// Returns the holder's channel public keys and basepoints. /// /// This method is *not* asynchronous. Instead, the value must be cached locally. + /// + /// NOTE: The [`ChannelPublicKeys::funding_pubkey`] returned will not necessarily correspond to + /// the channel's current holder public key, as it may have rotated due to a splice. The value + /// should not be relied upon once the channel's initial funding transaction has been created. fn pubkeys(&self) -> &ChannelPublicKeys; /// Returns an arbitrary identifier describing the set of keys which are provided back to you in @@ -978,6 +982,31 @@ pub trait ChangeDestinationSource { fn get_change_destination_script(&self) -> Result; } +mod sealed { + use bitcoin::secp256k1::{Scalar, SecretKey}; + + #[derive(Clone, PartialEq)] + pub struct MaybeTweakedSecretKey(SecretKey); + + impl From for MaybeTweakedSecretKey { + fn from(value: SecretKey) -> Self { + Self(value) + } + } + + impl MaybeTweakedSecretKey { + pub fn with_tweak(&self, tweak: Option) -> SecretKey { + tweak + .map(|tweak| { + self.0 + .add_tweak(&tweak) + .expect("Addition only fails if the tweak is the inverse of the key") + }) + .unwrap_or(self.0) + } + } +} + /// A simple implementation of [`EcdsaChannelSigner`] that just keeps the private keys in memory. /// /// This implementation performs no policy checks and is insufficient by itself as @@ -985,7 +1014,7 @@ pub trait ChangeDestinationSource { pub struct InMemorySigner { /// Holder secret key in the 2-of-2 multisig script of a channel. This key also backs the /// holder's anchor output in a commitment transaction, if one is present. - pub funding_key: SecretKey, + 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. @@ -1049,7 +1078,7 @@ impl InMemorySigner { &htlc_base_key, ); InMemorySigner { - funding_key, + funding_key: sealed::MaybeTweakedSecretKey::from(funding_key), revocation_base_key, payment_key, delayed_payment_base_key, @@ -1077,6 +1106,14 @@ impl InMemorySigner { } } + /// Holder secret key in the 2-of-2 multisig script of a channel. This key also backs the + /// holder's anchor output in a commitment transaction, if one is present. A tweak may need to + /// be applied if the channel has been spliced. It can be obtained from + /// [`ChannelTransactionParameters::holder_pubkeys`]. + pub fn funding_key(&self, tweak: Option) -> SecretKey { + self.funding_key.with_tweak(tweak) + } + /// 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. /// @@ -1275,7 +1312,7 @@ impl EcdsaChannelSigner for InMemorySigner { let trusted_tx = commitment_tx.trust(); let keys = trusted_tx.keys(); - let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key); + let funding_pubkey = channel_parameters.holder_pubkeys.as_ref().funding_pubkey; let counterparty_keys = channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); let channel_funding_redeemscript = @@ -1283,7 +1320,7 @@ impl EcdsaChannelSigner for InMemorySigner { let built_tx = trusted_tx.built_transaction(); let commitment_sig = built_tx.sign_counterparty_commitment( - &self.funding_key, + &self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak), &channel_funding_redeemscript, channel_parameters.channel_value_satoshis, secp_ctx, @@ -1336,14 +1373,14 @@ impl EcdsaChannelSigner for InMemorySigner { ) -> Result { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key); + let funding_pubkey = channel_parameters.holder_pubkeys.as_ref().funding_pubkey; let counterparty_keys = channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &counterparty_keys.funding_pubkey); let trusted_tx = commitment_tx.trust(); Ok(trusted_tx.built_transaction().sign_holder_commitment( - &self.funding_key, + &self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak), &funding_redeemscript, channel_parameters.channel_value_satoshis, &self, @@ -1358,14 +1395,14 @@ impl EcdsaChannelSigner for InMemorySigner { ) -> Result { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key); + let funding_pubkey = channel_parameters.holder_pubkeys.as_ref().funding_pubkey; let counterparty_keys = channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &counterparty_keys.funding_pubkey); let trusted_tx = commitment_tx.trust(); Ok(trusted_tx.built_transaction().sign_holder_commitment( - &self.funding_key, + &self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak), &funding_redeemscript, channel_parameters.channel_value_satoshis, &self, @@ -1388,7 +1425,7 @@ impl EcdsaChannelSigner for InMemorySigner { let per_commitment_point = PublicKey::from_secret_key(secp_ctx, &per_commitment_key); let revocation_pubkey = RevocationKey::from_basepoint( &secp_ctx, - &channel_parameters.holder_pubkeys.revocation_basepoint, + &channel_parameters.holder_pubkeys.as_ref().revocation_basepoint, &per_commitment_point, ); let witness_script = { @@ -1435,7 +1472,7 @@ impl EcdsaChannelSigner for InMemorySigner { let per_commitment_point = PublicKey::from_secret_key(secp_ctx, &per_commitment_key); let revocation_pubkey = RevocationKey::from_basepoint( &secp_ctx, - &channel_parameters.holder_pubkeys.revocation_basepoint, + &channel_parameters.holder_pubkeys.as_ref().revocation_basepoint, &per_commitment_point, ); let witness_script = { @@ -1448,7 +1485,7 @@ impl EcdsaChannelSigner for InMemorySigner { ); let holder_htlcpubkey = HtlcKey::from_basepoint( &secp_ctx, - &channel_parameters.holder_pubkeys.htlc_basepoint, + &channel_parameters.holder_pubkeys.as_ref().htlc_basepoint, &per_commitment_point, ); chan_utils::get_htlc_redeemscript_with_explicit_keys( @@ -1510,7 +1547,7 @@ impl EcdsaChannelSigner for InMemorySigner { chan_utils::derive_private_key(&secp_ctx, &per_commitment_point, &self.htlc_base_key); let revocation_pubkey = RevocationKey::from_basepoint( &secp_ctx, - &channel_parameters.holder_pubkeys.revocation_basepoint, + &channel_parameters.holder_pubkeys.as_ref().revocation_basepoint, &per_commitment_point, ); let counterparty_keys = @@ -1520,7 +1557,7 @@ impl EcdsaChannelSigner for InMemorySigner { &counterparty_keys.htlc_basepoint, &per_commitment_point, ); - let htlc_basepoint = channel_parameters.holder_pubkeys.htlc_basepoint; + let htlc_basepoint = channel_parameters.holder_pubkeys.as_ref().htlc_basepoint; let htlcpubkey = HtlcKey::from_basepoint(&secp_ctx, &htlc_basepoint, &per_commitment_point); let chan_type = &channel_parameters.channel_type_features; let witness_script = chan_utils::get_htlc_redeemscript_with_explicit_keys( @@ -1550,13 +1587,13 @@ impl EcdsaChannelSigner for InMemorySigner { ) -> Result { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key); + let funding_pubkey = channel_parameters.holder_pubkeys.as_ref().funding_pubkey; let counterparty_funding_key = &channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR).funding_pubkey; let channel_funding_redeemscript = make_funding_redeemscript(&funding_pubkey, counterparty_funding_key); Ok(closing_tx.trust().sign( - &self.funding_key, + &self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak), &channel_funding_redeemscript, channel_parameters.channel_value_satoshis, secp_ctx, @@ -1569,8 +1606,9 @@ impl EcdsaChannelSigner for InMemorySigner { ) -> Result { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let witness_script = - chan_utils::get_anchor_redeemscript(&channel_parameters.holder_pubkeys.funding_pubkey); + let witness_script = chan_utils::get_anchor_redeemscript( + &channel_parameters.holder_pubkeys.as_ref().funding_pubkey, + ); let sighash = sighash::SighashCache::new(&*anchor_tx) .p2wsh_signature_hash( input, @@ -1579,14 +1617,16 @@ impl EcdsaChannelSigner for InMemorySigner { EcdsaSighashType::All, ) .unwrap(); - Ok(sign_with_aux_rand(secp_ctx, &hash_to_message!(&sighash[..]), &self.funding_key, &self)) + let funding_key = self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak); + Ok(sign_with_aux_rand(secp_ctx, &hash_to_message!(&sighash[..]), &funding_key, &self)) } fn sign_channel_announcement_with_funding_key( - &self, msg: &UnsignedChannelAnnouncement, secp_ctx: &Secp256k1, + &self, msg: &UnsignedChannelAnnouncement, funding_key_tweak: Option, + secp_ctx: &Secp256k1, ) -> Result { let msghash = hash_to_message!(&Sha256dHash::hash(&msg.encode()[..])[..]); - Ok(secp_ctx.sign_ecdsa(&msghash, &self.funding_key)) + Ok(secp_ctx.sign_ecdsa(&msghash, &self.funding_key(funding_key_tweak))) } fn sign_splicing_funding_input( @@ -1595,7 +1635,7 @@ impl EcdsaChannelSigner for InMemorySigner { ) -> Result { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key); + let funding_pubkey = channel_parameters.holder_pubkeys.as_ref().funding_pubkey; let counterparty_funding_key = &channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR).funding_pubkey; let funding_redeemscript = @@ -1609,7 +1649,8 @@ impl EcdsaChannelSigner for InMemorySigner { ) .unwrap()[..]; let msg = hash_to_message!(sighash); - Ok(sign(secp_ctx, &msg, &self.funding_key)) + let funding_key = self.funding_key(channel_parameters.holder_pubkeys.funding_key_tweak); + Ok(sign(secp_ctx, &msg, &funding_key)) } } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 21997c09c1a..4c0d0fb41ba 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -36,7 +36,7 @@ use bitcoin::secp256k1::constants::{ }; use bitcoin::secp256k1::ecdsa; use bitcoin::secp256k1::schnorr; -use bitcoin::secp256k1::{PublicKey, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Scalar, SecretKey}; use bitcoin::transaction::{OutPoint, Transaction, TxOut}; use bitcoin::{consensus, Witness}; @@ -1156,6 +1156,24 @@ impl Readable for SecretKey { } } +impl Writeable for Scalar { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_be_bytes().write(w) + } + + #[inline] + fn serialized_length(&self) -> usize { + 32 + } +} + +impl Readable for Scalar { + fn read(r: &mut R) -> Result { + let buf: [u8; 32] = Readable::read(r)?; + Scalar::from_be_bytes(buf).map_err(|_| DecodeError::InvalidValue) + } +} + #[cfg(taproot)] impl Writeable for musig2::types::PublicNonce { fn write(&self, w: &mut W) -> Result<(), io::Error> { diff --git a/lightning/src/util/test_channel_signer.rs b/lightning/src/util/test_channel_signer.rs index 7c3a687d494..163d267098d 100644 --- a/lightning/src/util/test_channel_signer.rs +++ b/lightning/src/util/test_channel_signer.rs @@ -40,7 +40,7 @@ use bitcoin::secp256k1; #[cfg(taproot)] use bitcoin::secp256k1::All; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; -use bitcoin::secp256k1::{PublicKey, SecretKey}; +use bitcoin::secp256k1::{PublicKey, Scalar, SecretKey}; #[cfg(taproot)] use musig2::types::{PartialSignature, PublicNonce}; @@ -466,9 +466,10 @@ impl EcdsaChannelSigner for TestChannelSigner { } fn sign_channel_announcement_with_funding_key( - &self, msg: &msgs::UnsignedChannelAnnouncement, secp_ctx: &Secp256k1, + &self, msg: &msgs::UnsignedChannelAnnouncement, funding_key_tweak: Option, + secp_ctx: &Secp256k1, ) -> Result { - self.inner.sign_channel_announcement_with_funding_key(msg, secp_ctx) + self.inner.sign_channel_announcement_with_funding_key(msg, funding_key_tweak, secp_ctx) } fn sign_splicing_funding_input( @@ -559,7 +560,7 @@ impl TestChannelSigner { .verify( &channel_parameters.as_counterparty_broadcastable(), channel_parameters.counterparty_pubkeys().unwrap(), - &channel_parameters.holder_pubkeys, + channel_parameters.holder_pubkeys.as_ref(), secp_ctx, ) .expect("derived different per-tx keys or built transaction") @@ -572,7 +573,7 @@ impl TestChannelSigner { commitment_tx .verify( &channel_parameters.as_holder_broadcastable(), - &channel_parameters.holder_pubkeys, + channel_parameters.holder_pubkeys.as_ref(), channel_parameters.counterparty_pubkeys().unwrap(), secp_ctx, )