-
Notifications
You must be signed in to change notification settings - Fork 15
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
Add high-level helpers for using Musig2 with Taproot #114
Conversation
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'm not convinced that it makes the code that uses musig2 simpler or more compact. For now, this code is encapsulated in specific swap-in protocol
objects (for both lightning-kmp
and eclair
), this is where we can hide musig2 complexity from the rest of the codebase.
Also, the really tricky part imho (same with singing regular transaction inputs) is how to properly set up a musig2 signing session and this requires users to pass the right transaction, index, nonces etc... and it can't really be made simpler.
public fun add(pubkeys: List<PublicKey>, cache: KeyAggCache?): Pair<XonlyPublicKey, KeyAggCache> { | ||
val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) | ||
val aggkey = Secp256k1.musigPubkeyAgg(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache) | ||
public fun create(publicKeys: List<PublicKey>): Pair<XonlyPublicKey, KeyAggCache> { |
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.
This means that we cannot add new public keys to an existing key aggregation cache, which was possible before.
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.
Yes, it was possible, but not used anywhere. I don't see any scenario where that is useful: why create a key aggregation cache before you have all public keys, since you need to have all of them later? A general rule of library design is to start simple and only introduce more complexity when it's necessary, and here we were really doing the opposite...
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.
... A general rule of library design is to start simple and only introduce more complexity when it's necessary, and here we were really doing the opposite...
No, we keep things simple by exposing the musig2 API as-is and not second-guessing how it could be improved.
We've had very few bugs in this library, and always because we (it's a royal "we" here) were trying to be "smarter" than bitcoin-core.
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.
No, we keep things simple by exposing the musig2 API as-is and not second-guessing how it could be improved.
We've had very few bugs in this library, and always because we (it's a royal "we" here) were trying to be "smarter" than bitcoin-core.
I disagree: the musig2 API exposed in secp256k1
needs to be much more generic than what we're exposing, and doesn't want to compromise on performance. We have a more restricted set of use-cases, which allow us to provide higher-level APIs that are simpler to use, harder to misuse. Without those higher-level APIs, it is really hard and footgunny to use musig2.
val publicKeyX = uncompressedPublicKey.drop(1).take(32).reversed().toByteArray() | ||
val publicKeyY = uncompressedPublicKey.takeLast(32).reversed().toByteArray() | ||
val magic = Hex.decode("220EDCF1") | ||
return SecretNonce(magic + serialized.take(64) + publicKeyX + publicKeyY) |
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.
This will likely break when we update secp256k1
through secp256k1-kmp
. If we really want to add these low-level checks (not sure we need to), they belong in secp256k1-kmp
.
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.
You mean that secp256k1
may break the format they use for SecretNonce
s? If so yes, we'll need to update those tests, but this is only used for tests, to make sure we're able to validate the official test vectors. I like validating the test vectors at the bitcoin-kmp
layer rather than in secp256k1-kmp
, because they could pass in secp256k1-kmp
, but bitcoin-kmp
may use incorrectly the functions exposed, and then we wouldn't have tests to catch it.
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.
Verifying that the public nonce is correct should be enough here.
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.
It's true for the secret_nonce_gen
test vectors, but without that function we cannot run the signing test vectors, nor the aggregation test vectors, which are pretty important test vectors to have!
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.
It should be possible to generate the secret nonces used in these tests (though it does not seem to be documented), I'll investigate.
I really don't understand how you can't be convinced. We're now exposing an API that makes it trivial to spend any output that uses musig2 for its key path with any script tree, without having to know or care about low-level musig2 details (key aggregation cache and opaque session data). That's exactly the abstraction that every use-case of musig2 needs? So instead of duplicating this code in The musig2 complexity should be hidden in
I don't understand what you mean: a signer only has to generate his secret nonce, receive public nonces from other participants, and then the existing functions make it trivial to spend the utxos. I believe the swap-in-potentiam unit test showcases that pretty well? |
inputIndex: Int, | ||
inputs: List<TxOut>, | ||
publicKeys: List<PublicKey>, | ||
publicNonces: List<IndividualNonce>, |
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.
This means that all public nonces and public keys must be available. This may not be practical, users could have discarded public nonces and just have kept the aggregated nonce. This is what we currently do in ACINQ/lightning-kmp#563.
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.
This is what we currently do in ACINQ/lightning-kmp#563.
But it's much simpler if eclair/lightning-kmp don't have to care about nonce aggregation at all, isn't it? It's simpler to just store the local and remote nonces and then pass them to the signing helper (and those nonces are most likely already stored internally as part of the interactive-tx types). This gets rid of one "internal detail" of musig2 that higher-level applications don't even need to know about, that's part of encapsulating the complexity inside bitcoin-kmp
.
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 have updated lightning-kmp
to simply keep the local and remote nonce instead of aggregating them in ACINQ/lightning-kmp#591 and it works well.
355959e
to
4ead0d7
Compare
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
b377703
to
8e0194f
Compare
09ff67a
to
eb10a83
Compare
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
When using Musig2 for a taproot key path, we can provide simpler helper functions to collaboratively build a shared signature for the spending transaction. Those helper functions hide the low-level details of using an opaque key aggregation cache or signing session. This comes with a small performance penalty, as we recompute the key aggregation cache. We also document the exposed APIs, import more tests from the official test vectors, and make APIs safe: they should never throw exceptions, except when invalid public keys are provided as inputs (which should be verified by the caller beforehand).
This is more consistent with the existing `signInput` helpers.
8e0194f
to
b7ca44e
Compare
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 updated and tested lightning-kmp/eclair/phoenix with these changes and everything works, and it seems that it makes the code in lightning-kmp
more readable so we can go with this. We'll end up with an API that is a bit more restrictive than the raw musig2 API but we can always revisit this later.
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.
* Move swap-in related methods into their own class * Add musig2-based swap-in protocol * Use different user keys for the common and refund paths This allows us to easily rotate swap-in addresses and generate a single generic taproot descriptor (for bitcoin core 26 and newer) that can be used to recover swap-in funds once the refund delay has passed, assuming that: - user and server keys are static - user refund keys follow BIP derivation * Add a musig2 secret nonce field to local/remote musing2 swap-in classes It makes the code cleaner and we get rid of the secret nonces map. These nonces are replaced with dummy values whenever this classes are serialized, which is safe since they're never reused for signing txs. * Rework TxComplete to use implicit ordering for musig2 nonces Instead of sending an explicit serialId -> nonce map, we send a list of public nonces ordered by serial id. This matches how signatures are sent in TxSignatures. * Address review comments - add a pubkey script to the SharedInput() class (we don't need the full TxOut which we can recreate) - remove aggregate nonce check ins FullySignedTx: code already handles transactions that are not properly signed - generate musig2 nonces when we send TxAddInput * Use musig2 helpers to simplify swap-in protocol (#592) We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation). The code is simpler to reason about and signing is more similar to signing normal single-sig inputs. * Rework recovery procedure The current recovery process needed to be updated to derive the correct master priv key from the seed by specifying our custom BIP32 path (m/52h/0h/2h/0) when we create the wallet. We also export 2 descriptor methods: one to get the private swap-in wallet descriptor, which can be used as-is, and the other to get the public swap-in wallet descriptor, which can be used to create a watch-only wallet to monitor swap-in funds and to recovery funds using our recovery procedure. Both descriptor use the refund master key, and not the master key itself because we use hardened paths to derive the refund key, which means that it is not possible to compute the refund master public key from the master public: importing the descriptor would fail. --------- Co-authored-by: Bastien Teinturier <[email protected]>
When using Musig2 for a taproot key path, we can provide simpler helper functions to collaboratively build a shared signature for the spending transaction. Those helper functions hide the low-level details of using an opaque key aggregation cache or signing session. This comes with a small performance penalty, as we recompute the key aggregation cache.
We also document the exposed APIs, import more tests from the official test vectors, and make APIs safe: they should never throw exceptions, except when invalid public keys are provided as inputs (which should be verified by the caller beforehand).
I removed the musig2 prototype as I don't see any valid reason to keep it: it doesn't even have the same API as the official implementation anymore so it's rather confusing, and can be found in the git history if necessary.
This is a PR targeting #107