Skip to content

Commit

Permalink
Support holder funding key rotation during splicing
Browse files Browse the repository at this point in the history
We introduce a scalar tweak that can be 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):

1. We use the txid of the funding transaction the splice transaction is
   spending instead of the `per_commitment_point` to guarantee
   uniqueness.
2. We include the private key instead of the public key to guarantee
   only those with knowledge of it can re-derive the new funding key.

  tweak = SHA256(splice_parent_funding_txid || base_funding_secret_key)
  tweaked_funding_key = base_funding_key + tweak

While the use of this tweak is not required (signers may choose to
compute a tweak of their choice), signers must ensure their tweak
guarantees the two properties mentioned above: uniqueness and derivable
only by one or both of the channel participants.
  • Loading branch information
wpaulino committed Mar 4, 2025
1 parent 36ba27a commit 2afbb41
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 54 deletions.
7 changes: 5 additions & 2 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1362,8 +1362,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
) -> ChannelMonitor<Signer> {

assert!(commitment_transaction_number_obscure_factor <= (1 << 48));
let holder_pubkeys = &channel_parameters.holder_pubkeys;
let counterparty_payment_script = chan_utils::get_counterparty_payment_script(
&channel_parameters.channel_type_features, &keys.pubkeys().payment_point
&channel_parameters.channel_type_features, &holder_pubkeys.payment_point
);

let counterparty_channel_parameters = channel_parameters.counterparty_parameters.as_ref().unwrap();
Expand All @@ -1372,7 +1373,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
let counterparty_commitment_params = CounterpartyCommitmentParameters { counterparty_delayed_payment_base_key, counterparty_htlc_base_key, on_counterparty_tx_csv };

let channel_keys_id = keys.channel_keys_id();
let holder_revocation_basepoint = keys.pubkeys().revocation_basepoint;
let holder_revocation_basepoint = holder_pubkeys.revocation_basepoint;

// block for Rust 1.34 compat
let (holder_commitment_tx, current_holder_commitment_number) = {
Expand Down Expand Up @@ -5423,6 +5424,7 @@ mod tests {
selected_contest_delay: 67,
}),
funding_outpoint: Some(funding_outpoint),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::only_static_remote_key(),
channel_value_satoshis: 0,
};
Expand Down Expand Up @@ -5675,6 +5677,7 @@ mod tests {
selected_contest_delay: 67,
}),
funding_outpoint: Some(funding_outpoint),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::only_static_remote_key(),
channel_value_satoshis: 0,
};
Expand Down
1 change: 1 addition & 0 deletions lightning/src/chain/onchaintx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,7 @@ mod tests {
selected_contest_delay: 67,
}),
funding_outpoint: Some(funding_outpoint),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::only_static_remote_key(),
channel_value_satoshis: 0,
};
Expand Down
60 changes: 56 additions & 4 deletions lightning/src/chain/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,20 @@ impl PackageSolvingData {
let channel_parameters = &onchain_handler.channel_transaction_parameters;
match self {
PackageSolvingData::RevokedOutput(ref outp) => {
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
let directed_parameters =
&onchain_handler.channel_transaction_parameters.as_counterparty_broadcastable();
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().delayed_payment_basepoint,
outp.counterparty_delayed_payment_base_key,
);
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().htlc_basepoint,
outp.counterparty_htlc_base_key,
);
let chan_keys = TxCreationKeys::from_channel_static_keys(
&outp.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
);
let witness_script = chan_utils::get_revokeable_redeemscript(&chan_keys.revocation_key, outp.on_counterparty_tx_csv, &chan_keys.broadcaster_delayed_payment_key);
//TODO: should we panic on signer failure ?
if let Ok(sig) = onchain_handler.signer.sign_justice_revoked_output(channel_parameters, &bumped_tx, i, outp.amount.to_sat(), &outp.per_commitment_key, &onchain_handler.secp_ctx) {
Expand All @@ -617,7 +630,20 @@ impl PackageSolvingData {
} else { return false; }
},
PackageSolvingData::RevokedHTLCOutput(ref outp) => {
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
let directed_parameters =
&onchain_handler.channel_transaction_parameters.as_counterparty_broadcastable();
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().delayed_payment_basepoint,
outp.counterparty_delayed_payment_base_key,
);
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().htlc_basepoint,
outp.counterparty_htlc_base_key,
);
let chan_keys = TxCreationKeys::from_channel_static_keys(
&outp.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
);
let witness_script = chan_utils::get_htlc_redeemscript_with_explicit_keys(&outp.htlc, &onchain_handler.channel_type_features(), &chan_keys.broadcaster_htlc_key, &chan_keys.countersignatory_htlc_key, &chan_keys.revocation_key);
//TODO: should we panic on signer failure ?
if let Ok(sig) = onchain_handler.signer.sign_justice_revoked_htlc(channel_parameters, &bumped_tx, i, outp.amount, &outp.per_commitment_key, &outp.htlc, &onchain_handler.secp_ctx) {
Expand All @@ -629,7 +655,20 @@ impl PackageSolvingData {
} else { return false; }
},
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => {
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
let directed_parameters =
&onchain_handler.channel_transaction_parameters.as_counterparty_broadcastable();
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().delayed_payment_basepoint,
outp.counterparty_delayed_payment_base_key,
);
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().htlc_basepoint,
outp.counterparty_htlc_base_key,
);
let chan_keys = TxCreationKeys::from_channel_static_keys(
&outp.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
);
let witness_script = chan_utils::get_htlc_redeemscript_with_explicit_keys(&outp.htlc, &onchain_handler.channel_type_features(), &chan_keys.broadcaster_htlc_key, &chan_keys.countersignatory_htlc_key, &chan_keys.revocation_key);

if let Ok(sig) = onchain_handler.signer.sign_counterparty_htlc_transaction(channel_parameters, &bumped_tx, i, &outp.htlc.amount_msat / 1000, &outp.per_commitment_point, &outp.htlc, &onchain_handler.secp_ctx) {
Expand All @@ -641,7 +680,20 @@ impl PackageSolvingData {
}
},
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => {
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
let directed_parameters =
&onchain_handler.channel_transaction_parameters.as_counterparty_broadcastable();
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().delayed_payment_basepoint,
outp.counterparty_delayed_payment_base_key,
);
debug_assert_eq!(
directed_parameters.broadcaster_pubkeys().htlc_basepoint,
outp.counterparty_htlc_base_key,
);
let chan_keys = TxCreationKeys::from_channel_static_keys(
&outp.per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &onchain_handler.secp_ctx,
);
let witness_script = chan_utils::get_htlc_redeemscript_with_explicit_keys(&outp.htlc, &onchain_handler.channel_type_features(), &chan_keys.broadcaster_htlc_key, &chan_keys.countersignatory_htlc_key, &chan_keys.revocation_key);

if let Ok(sig) = onchain_handler.signer.sign_counterparty_htlc_transaction(channel_parameters, &bumped_tx, i, &outp.htlc.amount_msat / 1000, &outp.per_commitment_point, &outp.htlc, &onchain_handler.secp_ctx) {
Expand Down
30 changes: 25 additions & 5 deletions lightning/src/ln/chan_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,17 @@ pub struct ChannelTransactionParameters {
pub counterparty_parameters: Option<CounterpartyChannelTransactionParameters>,
/// The late-bound funding outpoint
pub funding_outpoint: Option<chain::transaction::OutPoint>,
/// The parent funding txid for a channel that has been spliced.
///
/// 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
/// this may be used.
///
/// [`compute_funding_key_tweak`]: crate::sign::compute_funding_key_tweak
/// [`ChannelSigner::pubkeys`]: crate::sign::ChannelSigner::pubkeys
pub splice_parent_funding_txid: Option<Txid>,
/// 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.
pub channel_type_features: ChannelTypeFeatures,
Expand Down Expand Up @@ -963,6 +974,7 @@ impl ChannelTransactionParameters {
funding_outpoint: Some(chain::transaction::OutPoint {
txid: Txid::from_byte_array([42; 32]), index: 0
}),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::empty(),
channel_value_satoshis,
}
Expand All @@ -985,6 +997,7 @@ impl Writeable for ChannelTransactionParameters {
(8, self.funding_outpoint, option),
(10, legacy_deserialization_prevention_marker, option),
(11, self.channel_type_features, required),
(12, self.splice_parent_funding_txid, option),
(13, self.channel_value_satoshis, required),
});
Ok(())
Expand All @@ -998,6 +1011,7 @@ impl ReadableArgs<u64> for ChannelTransactionParameters {
let mut is_outbound_from_holder = RequiredWrapper(None);
let mut counterparty_parameters = None;
let mut funding_outpoint = None;
let mut splice_parent_funding_txid = None;
let mut _legacy_deserialization_prevention_marker: Option<()> = None;
let mut channel_type_features = None;
let mut channel_value_satoshis = None;
Expand All @@ -1010,6 +1024,7 @@ impl ReadableArgs<u64> for ChannelTransactionParameters {
(8, funding_outpoint, option),
(10, _legacy_deserialization_prevention_marker, option),
(11, channel_type_features, option),
(12, splice_parent_funding_txid, option),
(13, channel_value_satoshis, option),
});

Expand All @@ -1028,6 +1043,7 @@ impl ReadableArgs<u64> for ChannelTransactionParameters {
is_outbound_from_holder: is_outbound_from_holder.0.unwrap(),
counterparty_parameters,
funding_outpoint,
splice_parent_funding_txid,
channel_type_features: channel_type_features.unwrap_or(ChannelTypeFeatures::only_static_remote_key()),
channel_value_satoshis,
})
Expand Down Expand Up @@ -1154,6 +1170,7 @@ impl HolderCommitmentTransaction {
is_outbound_from_holder: false,
counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: channel_pubkeys.clone(), selected_contest_delay: 0 }),
funding_outpoint: Some(chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::only_static_remote_key(),
channel_value_satoshis,
};
Expand Down Expand Up @@ -1953,22 +1970,25 @@ mod tests {
let keys_provider = test_utils::TestKeysInterface::new(&seed, network);
let signer = keys_provider.derive_channel_signer(keys_provider.generate_channel_keys_id(false, 0));
let counterparty_signer = keys_provider.derive_channel_signer(keys_provider.generate_channel_keys_id(true, 1));
let delayed_payment_base = &signer.pubkeys().delayed_payment_basepoint;
let per_commitment_secret = SecretKey::from_slice(&<Vec<u8>>::from_hex("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100").unwrap()[..]).unwrap();
let per_commitment_point = PublicKey::from_secret_key(&secp_ctx, &per_commitment_secret);
let htlc_basepoint = &signer.pubkeys().htlc_basepoint;
let holder_pubkeys = signer.pubkeys();
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 holder_pubkeys = signer.pubkeys(None, &secp_ctx);
let counterparty_pubkeys = counterparty_signer.pubkeys(None, &secp_ctx).clone();
let channel_parameters = ChannelTransactionParameters {
holder_pubkeys: 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 }),
funding_outpoint: Some(chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }),
splice_parent_funding_txid: None,
channel_type_features: ChannelTypeFeatures::only_static_remote_key(),
channel_value_satoshis: 3000,
};
let directed_parameters = channel_parameters.as_holder_broadcastable();
let keys = TxCreationKeys::from_channel_static_keys(
&per_commitment_point, directed_parameters.broadcaster_pubkeys(),
directed_parameters.countersignatory_pubkeys(), &secp_ctx,
);
let htlcs_with_aux = Vec::new();

Self {
Expand Down
36 changes: 23 additions & 13 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2410,7 +2410,6 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {

let channel_keys_id = signer_provider.generate_channel_keys_id(true, user_id);
let holder_signer = signer_provider.derive_channel_signer(channel_keys_id);
let pubkeys = holder_signer.pubkeys().clone();

if config.channel_handshake_config.our_to_self_delay < BREAKDOWN_TIMEOUT {
return Err(ChannelError::close(format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {}", config.channel_handshake_config.our_to_self_delay, BREAKDOWN_TIMEOUT)));
Expand Down Expand Up @@ -2570,6 +2569,8 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {

// TODO(dual_funding): Checks for `funding_feerate_sat_per_1000_weight`?

let pubkeys = holder_signer.pubkeys(None, &secp_ctx);

let funding = FundingScope {
value_to_self_msat,
counterparty_selected_channel_reserve_satoshis: Some(msg_channel_reserve_satoshis),
Expand All @@ -2594,6 +2595,7 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
pubkeys: counterparty_pubkeys,
}),
funding_outpoint: None,
splice_parent_funding_txid: None,
channel_type_features: channel_type.clone(),
channel_value_satoshis,
},
Expand Down Expand Up @@ -2733,11 +2735,10 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
config: &'a UserConfig,
current_chain_height: u32,
outbound_scid_alias: u64,
temporary_channel_id: Option<ChannelId>,
temporary_channel_id_fn: Option<impl Fn(&ChannelPublicKeys) -> ChannelId>,
holder_selected_channel_reserve_satoshis: u64,
channel_keys_id: [u8; 32],
holder_signer: <SP::Target as SignerProvider>::EcdsaSigner,
pubkeys: ChannelPublicKeys,
_logger: L,
) -> Result<(FundingScope, ChannelContext<SP>), APIError>
where
Expand Down Expand Up @@ -2803,7 +2804,9 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get destination script".to_owned()}),
};

let temporary_channel_id = temporary_channel_id.unwrap_or_else(|| ChannelId::temporary_from_entropy_source(entropy_source));
let pubkeys = holder_signer.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));

let funding = FundingScope {
value_to_self_msat,
Expand All @@ -2828,6 +2831,7 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
is_outbound_from_holder: true,
counterparty_parameters: None,
funding_outpoint: None,
splice_parent_funding_txid: None,
channel_type_features: channel_type.clone(),
// We'll add our counterparty's `funding_satoshis` when we receive `accept_channel2`.
channel_value_satoshis,
Expand Down Expand Up @@ -8160,7 +8164,9 @@ impl<SP: Deref> FundedChannel<SP> 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 our_bitcoin_sig = match ecdsa.sign_channel_announcement_with_funding_key(
&self.funding.channel_transaction_parameters, &announcement, &self.context.secp_ctx,
) {
Err(_) => {
log_error!(logger, "Signer rejected channel_announcement signing. Channel will not be announced!");
return None;
Expand Down Expand Up @@ -8201,7 +8207,9 @@ impl<SP: Deref> FundedChannel<SP> 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)
let our_bitcoin_sig = ecdsa.sign_channel_announcement_with_funding_key(
&self.funding.channel_transaction_parameters, &announcement, &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 },
Expand Down Expand Up @@ -9007,7 +9015,10 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {

let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id);
let holder_signer = signer_provider.derive_channel_signer(channel_keys_id);
let pubkeys = holder_signer.pubkeys().clone();

let temporary_channel_id_fn = temporary_channel_id.map(|id| {
move |_: &ChannelPublicKeys| id
});

let (funding, context) = ChannelContext::new_for_outbound_channel(
fee_estimator,
Expand All @@ -9021,11 +9032,10 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {
config,
current_chain_height,
outbound_scid_alias,
temporary_channel_id,
temporary_channel_id_fn,
holder_selected_channel_reserve_satoshis,
channel_keys_id,
holder_signer,
pubkeys,
logger,
)?;
let unfunded_context = UnfundedChannelContext {
Expand Down Expand Up @@ -9564,9 +9574,10 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
{
let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id);
let holder_signer = signer_provider.derive_channel_signer(channel_keys_id);
let pubkeys = holder_signer.pubkeys().clone();

let temporary_channel_id = Some(ChannelId::temporary_v2_from_revocation_basepoint(&pubkeys.revocation_basepoint));
let temporary_channel_id_fn = Some(|pubkeys: &ChannelPublicKeys| {
ChannelId::temporary_v2_from_revocation_basepoint(&pubkeys.revocation_basepoint)
});

let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
Expand All @@ -9590,11 +9601,10 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
config,
current_chain_height,
outbound_scid_alias,
temporary_channel_id,
temporary_channel_id_fn,
holder_selected_channel_reserve_satoshis,
channel_keys_id,
holder_signer,
pubkeys,
logger,
)?;
let unfunded_context = UnfundedChannelContext {
Expand Down
1 change: 1 addition & 0 deletions lightning/src/ln/dual_funding_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession)
holder_selected_contest_delay: open_channel_v2_msg.common_fields.to_self_delay,
is_outbound_from_holder: true,
funding_outpoint,
splice_parent_funding_txid: None,
channel_type_features,
channel_value_satoshis: funding_satoshis,
};
Expand Down
Loading

0 comments on commit 2afbb41

Please sign in to comment.