Uh oh!
There was an error while loading. Please reload this page.
Support scalar tweak to rotate holder funding key during splicing - #3624
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
fda4235 to
def03b5Comparewpaulino
commented
Feb 27, 2025
Pushed an update addressing both of your comments @TheBlueMatt@jkczyz. |
jkczyz
left a comment
There was a problem hiding this comment.
Just some minor comments. Feel free to ignore if you prefer. Otherwise, LGTM.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| /// NOTE: The [`ChannelPublicKeys::funding_pubkey`] returned will not necessarily correspond to | ||
| /// the channel's current holder public key, as it may have rotated due a splice. The value | ||
| /// should not be relied upon once the channel's initial funding transaction has been created. |
There was a problem hiding this comment.
I wonder if we should return the ChannelPublicKeys as part of the derive_channel_signer so that we can remove this method. Then we wouldn't need this caveat. Doesn't need to be done in this PR if it's going to be involved, though.
There was a problem hiding this comment.
It would be nice to remove the method, but I think the caveat would still be there regardless?
Uh oh!
There was an error while loading. Please reload this page.
def03b5 to
4bf1e8bCompareCodecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## main #3624 +/- ##
==========================================
- Coverage 89.19% 89.18% -0.02%
==========================================
Files 155 155 Lines 118974 119068 +94 Branches 118974 119068 +94 ==========================================
+ Hits 106119 106188 +69 - Misses 10265 10285 +20 - Partials 2590 2595 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Uh oh!
There was an error while loading. Please reload this page.
66b084a to
08edfa7Compare| // 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() | ||
| } |
There was a problem hiding this comment.
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 existing ChannelSigner::pubkeys call to take some id / idx of the funding transaction we need keys for? Then write the returned ChannelPublicKeys to ChannelTransactionParameters, 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 on TxBuilder - this comment is in the same direction.
There was a problem hiding this comment.
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 on channel_keys_id, we are just proposing a tweak to be added to the funding key at the last mile.
Could we expand the existing ChannelSigner::pubkeys call to take some id / idx of the funding transaction we need keys for?
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.
not something that both peers of the channel need to agree upon and therefore needs to be customized.
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.
the less work the signer has to do, the better IMO, especially for those not using InMemorySigner
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.
The signer is still free to choose their derivation scheme of choice based on channel_keys_id, we are just proposing a tweak to be added to the funding key at the last mile.
Yes I agree signer already has some choice of the derivation scheme. Though letting people customize the derivation of TxCreationKeys in TxBuilder could also be seen as a last mile operation - and at the moment we'd let people customize that part.
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.
Certainly
There was a problem hiding this comment.
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.
I appreciate you doing this thank you.
If we go for this direction, I would hope we can maintain the invariant on ChannelTransactionParameters that the holder_pubkeys matches the keys expected for the parent_txid.
So in src/chain/package, we could read the pubkeys directly from channel_parameters instead of doing a call to the signer.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
wpaulino
commented
Mar 3, 2025
Ended up reworking this a bit thanks to @tankyleo's suggestion to allow the signer to customize the tweak if they wish to. |
Uh oh!
There was an error while loading. Please reload this page.
| current_chain_height: u32, | ||
| outbound_scid_alias: u64, | ||
| temporary_channel_id: Option<ChannelId>, | ||
| temporary_channel_id_fn: Option<impl Fn(&ChannelPublicKeys) -> ChannelId>, |
There was a problem hiding this comment.
Isn't clear to me why this is needed for the PR or why it needs to be a Fn.
There was a problem hiding this comment.
Because we need a secp_ctx now to call ChannelSigner::pubkeys, which is instantiated in ChannelContext::new_outbound_channel.
| /// 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. |
There was a problem hiding this comment.
Seems like this should live on the trait somewhere?
There was a problem hiding this comment.
This is just an example tweak so I figured this would be the best place to mention it. We already link to this from the trait method anyway.
Uh oh!
There was an error while loading. Please reload this page.
| /// 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 fn funding_key(&self, splice_parent_funding_txid: Option<Txid>) -> SecretKey { |
There was a problem hiding this comment.
nit: Might be worth passing the entire ChannelTransactionParameters to avoid mistakenly passing None. Your call.
There was a problem hiding this comment.
This is also called by ChannelSigner::pubkeys which cannot have access to ChannelTransactionParameters before it is called before they are created.
| fn pubkeys( | ||
| &self, splice_parent_funding_txid: Option<Txid>, secp_ctx: &Secp256k1<secp256k1::All>, | ||
| ) -> ChannelPublicKeys; |
There was a problem hiding this comment.
Looks like this only used externally now. Isn't clear to me we should keep this as one method or make two methods -- one for initial funding and one for splicing -- to avoid needing to pass secp_ctx when None. Not sure if it matters all that much, but it would make what's happening at the call sites explicit.
There was a problem hiding this comment.
Your call.
Maybe someone wants to do a derivation away from the "origin" keys of ChannelSigner for the 0th funding transaction ? A bit like we have m/0 then m/1 in BIP 32, instead of m then m/1
Also if we consider the 0th funding transaction to be a special case in a channel where many many splices will occur over its lifetime, maybe it's not worth breaking out a standalone function for a single case ?
There was a problem hiding this comment.
Looks like this only used externally now.
It's only called during channel open currently, and we'll need to call it for the new ChannelTransactionParameters of a splice's FundingScope.
one for initial funding and one for splicing -- to avoid needing to pass secp_ctx when None.
If it's just for this purpose, I'd say it's not worth it. You're expected to have a secp_ctx available when interacting with the signer. If anything, I'd argue secp_ctx was missing from this method as it's required when going from secret keys to public keys. This isn't a problem for InMemorySigner because it is done on initialization to cache them.
tankyleo
left a comment
There was a problem hiding this comment.
Just have some nits, thank you!
Uh oh!
There was an error while loading. Please reload this page.
| (8, self.funding_outpoint, option), | ||
| (10, legacy_deserialization_prevention_marker, option), | ||
| (11, self.channel_type_features, required), | ||
| (12, self.splice_parent_funding_txid, option), |
There was a problem hiding this comment.
Checking myself: once a user has made a splice, we cannot downgrade to some release before this commit here - that's why we assign an even field ?
Uh oh!
There was an error while loading. Please reload this page.
| fn pubkeys( | ||
| &self, splice_parent_funding_txid: Option<Txid>, secp_ctx: &Secp256k1<secp256k1::All>, | ||
| ) -> ChannelPublicKeys; |
There was a problem hiding this comment.
Your call.
Maybe someone wants to do a derivation away from the "origin" keys of ChannelSigner for the 0th funding transaction ? A bit like we have m/0 then m/1 in BIP 32, instead of m then m/1
Also if we consider the 0th funding transaction to be a special case in a channel where many many splices will occur over its lifetime, maybe it's not worth breaking out a standalone function for a single case ?
wpaulino
commented
Mar 4, 2025
Had to push again to fix CI |
jkczyz
commented
Mar 4, 2025
Looks like CI is sad still. |
wpaulino
commented
Mar 5, 2025
Somehow I didn't rebase on latest main the first time, so had to rebase again to fix the |
jkczyz
commented
Mar 5, 2025
Hmm... seems there's a test using |
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.
| fn get_change_destination_script(&self) -> Result<ScriptBuf, ()>; | ||
| } | ||
| mod sealed { |
There was a problem hiding this comment.
nit: this isn't really "sealed" in the traditional rust definition (which only applies to traits), this is just private...
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, 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 therevocation_basepointto guarantee only the channel participants can re-derive the new funding key.Depends on #3604.
Fixes#3542.