-
Notifications
You must be signed in to change notification settings - Fork 384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support scalar tweak to rotate holder funding key during splicing #3624
Open
wpaulino
wants to merge
1
commit into
lightningdevkit:main
Choose a base branch
from
wpaulino:funding-key-tweak
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+267
−61
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<T: secp256k1::Signing>(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 was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, I think its fine, as you note the only invalid scalar would be the negation of the private key and calculating that requires knowing the private key, so....Just remove the TODO :) |
||
// 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<Scalar>, | ||
} | ||
|
||
// `HolderChannelPublicKeys` may have been previously written as `ChannelPublicKeys` so we have to | ||
// mimic its serialization. | ||
impl Writeable for HolderChannelPublicKeys { | ||
fn write<W: Writer>(&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<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> { | ||
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<Scalar> = 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<ChannelPublicKeys> for HolderChannelPublicKeys { | ||
fn as_ref(&self) -> &ChannelPublicKeys { | ||
&self.keys | ||
} | ||
} | ||
|
||
impl From<ChannelPublicKeys> 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<C: Verification>( | ||
funding_pubkey: PublicKey, revocation_basepoint: RevocationBasepoint, | ||
payment_point: PublicKey, delayed_payment_basepoint: DelayedPaymentBasepoint, | ||
htlc_basepoint: HtlcBasepoint, funding_key_tweak: Option<Scalar>, secp: &Secp256k1<C>, | ||
) -> 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 }), | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be more comfortable with a roundtrip to the signer to ask for a fresh funding key to use for the new funding transaction, rather than
FundingScope
deriving this new public key. Could we expand the existingChannelSigner::pubkeys
call to take some id / idx of the funding transaction we need keys for? Then write the returnedChannelPublicKeys
toChannelTransactionParameters
, similar to how we do it today for the initial funding transaction?I am not sure that
FundingScope
should define the derivation scheme to use for new funding keys; the signer should do that.In PR 3606 I propose that we avoid having channel objects impose a particular derivation scheme for
TxCreationKeys
onTxBuilder
- this comment is in the same direction.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I consider this tweak a bit different than others because it's something we're doing locally, not something that both peers of the channel need to agree upon and therefore needs to be customized. While we could certainly go to the signer to ask for a new funding key, this mostly abstracts that away from them (the less work the signer has to do, the better IMO, especially for those not using
InMemorySigner
). The signer is still free to choose their derivation scheme of choice based onchannel_keys_id
, we are just proposing a tweak to be added to the funding key at the last mile.This would be nice to get rid of the caveat pointed out in the docs of
ChannelSigner::pubkeys
, but we'd want to guarantee the signer implementation only tweaks the funding key and not any of the others, as that would break the channel.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting my thinking was - Spec does not dictate how to derive the new funding key, and the counterparty does not care which derivation we use, so LDK should let people customize this derivation.
Also interesting - I've been under the impression that signer does too little, and channel does too much :) At least too much that is specific to ecdsa signatures for example.
Yes I agree signer already has some choice of the derivation scheme. Though letting people customize the derivation of
TxCreationKeys
inTxBuilder
could also be seen as a last mile operation - and at the moment we'd let people customize that part.Certainly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one benefit of having the signer derive the new funding key is it allows them to reuse that same funding key somewhere else within the commitment transaction as a spending condition or destination. I explored the changes you proposed in this separate branch: https://github.com/wpaulino/rust-lightning/tree/funding-key-tweak-alt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate you doing this thank you.
If we go for this direction, I would hope we can maintain the invariant on
ChannelTransactionParameters
that theholder_pubkeys
matches the keys expected for theparent_txid
.So in
src/chain/package
, we could read the pubkeys directly fromchannel_parameters
instead of doing a call to the signer.