diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4df9eb24..87392495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: run: | echo "node $(node --version)" echo "npm $(npm --version)" + echo "npx $(npx --version)" echo "rustc $(rustc --version)" echo "wasm-pack $(wasm-pack --version)" echo "wasm-opt $(wasm-opt --version)" @@ -71,20 +72,21 @@ jobs: run: cargo deny check working-directory: packages/wasm-utxo - - name: test - run: npx --version - - name: build packages run: npm --workspaces run build - name: Check Source Code Formatting run: npm run check-fmt - - name: Wasm-Pack Test (Node) + - name: wasm-utxo / cargo test + run: cargo test --workspace + working-directory: packages/wasm-utxo + + - name: wasm-utxo / Wasm-Pack Test (Node) run: npm run test:wasm-pack-node working-directory: packages/wasm-utxo - - name: Wasm-Pack Test (Chrome) + - name: wasm-utxo / Wasm-Pack Test (Chrome) run: npm run test:wasm-pack-chrome working-directory: packages/wasm-utxo diff --git a/packages/wasm-utxo/bips/bip-0327/bip-0327.mediawiki b/packages/wasm-utxo/bips/bip-0327/bip-0327.mediawiki new file mode 100644 index 00000000..1cb2b64f --- /dev/null +++ b/packages/wasm-utxo/bips/bip-0327/bip-0327.mediawiki @@ -0,0 +1,830 @@ +
+  BIP: 327
+  Title: MuSig2 for BIP340-compatible Multi-Signatures
+  Author: Jonas Nick 
+          Tim Ruffing 
+          Elliott Jin 
+  Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0327
+  Status: Final
+  Type: Informational
+  Created: 2022-03-22
+  License: BSD-3-Clause
+  Post-History: 2022-04-05: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-April/020198.html [bitcoin-dev] MuSig2 BIP
+                2022-10-11: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-October/021000.html [bitcoin-dev] MuSig2 BIP
+
+ +== Introduction == + +=== Abstract === + +This document proposes a standard for the [https://eprint.iacr.org/2020/1261.pdf MuSig2] multi-signature scheme. +The standard is compatible with [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki BIP340] public keys and signatures. +It supports ''tweaking'', which allows deriving [https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki BIP32] child keys from aggregate public keys and creating [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341] Taproot outputs with key and script paths. + +=== Copyright === + +This document is licensed under the 3-clause BSD license. + +=== Motivation === + +MuSig2 is a multi-signature scheme that allows multiple signers to create a single aggregate public key and cooperatively create ordinary Schnorr signatures valid under the aggregate public key. +Signing requires interaction between ''all'' signers involved in key aggregation. +(MuSig2 is a ''n-of-n'' multi-signature scheme and not a ''t-of-n'' threshold-signature scheme.) + +The primary motivation is to create a standard that allows users of different software projects to jointly control Taproot outputs ([https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341]). +Such an output contains a public key which, in this case, would be the aggregate of all users' individual public keys. +It can be spent using MuSig2 to produce a signature for the key-based spending path. + +The on-chain footprint of a MuSig2 Taproot output is essentially a single BIP340 public key, and a transaction spending the output only requires a single signature cooperatively produced by all signers. This is '''more compact''' and has '''lower verification cost''' than each signer providing an individual public key and signature, as would be required by an ''n-of-n'' policy implemented using OP_CHECKSIGADD as introduced in ([https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki BIP342]). +As a side effect, the number ''n'' of signers is not limited by any consensus rules when using MuSig2. + +Moreover, MuSig2 offers a '''higher level of privacy''' than OP_CHECKSIGADD: MuSig2 Taproot outputs are indistinguishable for a blockchain observer from regular, single-signer Taproot outputs even though they are actually controlled by multiple signers. By tweaking an aggregate public key, the shared Taproot output can have script spending paths that are hidden unless used. + +There are multi-signature schemes other than MuSig2 that are fully compatible with Schnorr signatures. +The MuSig2 variant proposed below stands out by combining all the following features: +* '''Simple Key Setup''': Key aggregation is non-interactive and fully compatible with BIP340 public keys. +* '''Two Communication Rounds''': MuSig2 is faster in practice than previous three-round multi-signature schemes such as [https://eprint.iacr.org/2018/068.pdf MuSig1], particularly when signers are connected through high-latency anonymous links. Moreover, the need for fewer communication rounds simplifies the algorithms and reduces the probability that implementations and users make security-relevant mistakes. +* '''Provable security''': MuSig2 has been [https://eprint.iacr.org/2020/1261.pdf proven existentially unforgeable] under the algebraic one-more discrete logarithm (AOMDL) assumption (instead of the discrete logarithm assumption required for single-signer Schnorr signatures). AOMDL is a falsifiable and weaker variant of the well-studied OMDL problem. +* '''Low complexity''': MuSig2 has a substantially lower computational and implementation complexity than alternative schemes like [https://eprint.iacr.org/2020/1057 MuSig-DN]. However, this comes at the cost of having no ability to generate nonces deterministically and the requirement to securely handle signing state. + +=== Design === + +* '''Compatibility with BIP340''': In this proposal, the aggregate public key is a BIP340 X-only public key, and the signature output at the end of the signing protocol is a BIP340 signature that passes BIP340 verification for the aggregate public key and a message. The individual public keys that are input to the key aggregation algorithm are ''plain'' public keys in compressed format. +* '''Tweaking for BIP32 derivations and Taproot''': This proposal supports tweaking aggregate public keys and signing for tweaked aggregate public keys. We distinguish two modes of tweaking: ''Plain'' tweaking can be used to derive child aggregate public keys per [https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki BIP32]. ''X-only'' tweaking, on the other hand, allows creating a [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341] tweak to add script paths to a Taproot output. See [[#tweaking-the-aggregate-public-key|below]] for details. +* '''Non-interactive signing with preprocessing''': The first communication round, exchanging the nonces, can happen before the message or the exact set of signers is determined. Once the parameters of the signing session are finalized, the signers can send partial signatures without additional interaction. +* '''Key aggregation optionally independent of order''': The output of the key aggregation algorithm depends on the order in which the individual public keys are provided as input. Key aggregation does not sort the individual public keys by default because applications often already have a canonical order of signers. Nonetheless, applications can mandate sorting before aggregation,Applications that sort individual public keys before aggregation should ensure that the implementation of sorting is reasonably efficient, and in particular does not degenerate to quadratic runtime on pathological inputs. and this proposal specifies a canonical order to sort the individual public keys before key aggregation. Sorting will ensure the same output, independent of the initial order. +* '''Third-party nonce and partial signature aggregation''': Instead of every signer sending their nonce and partial signature to every other signer, it is possible to use an untrusted third-party ''aggregator'' in order to reduce the communication complexity from quadratic to linear in the number of signers. In each of the two rounds, the aggregator collects all signers' contributions (nonces or partial signatures), aggregates them, and broadcasts the aggregate back to the signers. A malicious aggregator can force the signing session to fail to produce a valid Schnorr signature but cannot negatively affect the unforgeability of the scheme. +* '''Partial signature verification''': If any signer sends a partial signature contribution that was not created by honestly following the signing protocol, the signing session will fail to produce a valid Schnorr signature. This proposal specifies a partial signature verification algorithm to identify disruptive signers. It is incompatible with third-party nonce aggregation because the individual nonce is required for partial verification. +* '''MuSig2* optimization''': This proposal uses an optimized scheme MuSig2*, which allows saving a point multiplication in key aggregation as compared to MuSig2. MuSig2* is proven secure in the appendix of the [https://eprint.iacr.org/2020/1261 MuSig2 paper]. The optimization consists of assigning the constant key aggregation coefficient ''1'' to the second distinct key in the list of individual public keys to be aggregated (as well as to any key identical to this key). +* '''Size of the nonce and security''': In this proposal, each signer's nonce consists of two elliptic curve points. The [https://eprint.iacr.org/2020/1261 MuSig2 paper] gives distinct security proofs depending on the number of points that constitute a nonce. See section [[#choosing-the-size-of-the-nonce|Choosing the Size of the Nonce]] for a discussion. + +== Overview == + +Implementers must make sure to understand this section thoroughly to avoid subtle mistakes that may lead to catastrophic failure. + +=== Optionality of Features === + +The goal of this proposal is to support a wide range of possible application scenarios. +Given a specific application scenario, some features may be unnecessary or not desirable, and implementers can choose not to support them. +Such optional features include: +* Applying plain tweaks after x-only tweaks. +* Applying tweaks at all. +* Dealing with messages that are not exactly 32 bytes. +* Identifying a disruptive signer after aborting (aborting itself remains mandatory). +* Dealing with duplicate individual public keys in key aggregation. +If applicable, the corresponding algorithms should simply fail when encountering inputs unsupported by a particular implementation. (For example, the signing algorithm may fail when given a message which is not 32 bytes.) +Similarly, the test vectors that exercise the unimplemented features should be re-interpreted to expect an error, or be skipped if appropriate. + +=== General Signing Flow === + +The signers start by exchanging their individual public keys and computing an aggregate public key using the ''KeyAgg'' algorithm. +Whenever they want to sign a message, the basic order of operations to create a multi-signature is as follows: + +'''First broadcast round:''' +The signers start the signing session by running ''NonceGen'' to compute ''secnonce'' and ''pubnonce''.We treat the ''secnonce'' and ''pubnonce'' as grammatically singular even though they include serializations of two scalars and two elliptic curve points, respectively. This treatment may be confusing for readers familiar with the MuSig2 paper. However, serialization is a technical detail that is irrelevant for users of MuSig2 interfaces. +Then, the signers broadcast their ''pubnonce'' to each other and run ''NonceAgg'' to compute an aggregate nonce. + +'''Second broadcast round:''' +At this point, every signer has the required data to sign, which, in the algorithms specified below, is stored in a data structure called [[#session-context|Session Context]]. +Every signer computes a partial signature by running ''Sign'' with the secret signing key, the ''secnonce'' and the session context. +Then, the signers broadcast their partial signatures to each other and run ''PartialSigAgg'' to obtain the final signature. +If all signers behaved honestly, the result passes [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki BIP340] verification. + +Both broadcast rounds can be optimized by using an aggregator who collects all signers' nonces or partial signatures, aggregates them using ''NonceAgg'' or ''PartialSigAgg'', respectively, and broadcasts the aggregate result back to the signers. A malicious aggregator can force the signing session to fail to produce a valid Schnorr signature but cannot negatively affect the unforgeability of the scheme, i.e., even a malicious aggregator colluding with all but one signer cannot forge a signature. + +'''IMPORTANT''': The ''Sign'' algorithm must '''not''' be executed twice with the same ''secnonce''. +Otherwise, it is possible to extract the secret signing key from the two partial signatures output by the two executions of ''Sign''. +To avoid accidental reuse of ''secnonce'', an implementation may securely erase the ''secnonce'' argument by overwriting it with 64 zero bytes after it has been read by ''Sign''. +A ''secnonce'' consisting of only zero bytes is invalid for ''Sign'' and will cause it to fail. + +To simplify the specification of the algorithms, some intermediary values are unnecessarily recomputed from scratch, e.g., when executing ''GetSessionValues'' multiple times. +Actual implementations can cache these values. +As a result, the [[#session-context|Session Context]] may look very different in implementations or may not exist at all. +However, computation of ''GetSessionValues'' and storage of the result must be protected against modification from an untrusted third party. +This party would have complete control over the aggregate public key and message to be signed. + +=== Public Key Aggregation === + +We distinguish between two public key types, namely ''plain public keys'', the key type traditionally used in Bitcoin, and ''X-only public keys''. +Plain public keys are byte strings of length 33 (often called ''compressed'' format). +In contrast, X-only public keys are 32-byte strings defined in [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki BIP340]. + +The individual public keys of signers as input to the key aggregation algorithm ''KeyAgg'' (and to ''GetSessionValues'' and ''PartialSigVerify'') are plain public keys. +The output of ''KeyAgg'' is a [[#keyagg-context|KeyAgg Context]] which stores information required for tweaking the aggregate public key (see [[#tweaking-the-aggregate-public-key|below]]), +and it can be used to produce an X-only aggregate public key, or a plain aggregate public key. +In order to obtain an X-only public key compatible with BIP340 verification, implementations call the ''GetXonlyPubkey'' function with the KeyAgg Context. +To get the plain aggregate public key, which is required for some applications of [[#tweaking-the-aggregate-public-key|tweaking]], implementations call ''GetPlainPubkey'' instead. + +The aggregate public key produced by ''KeyAgg'' (regardless of the type) depends on the order of the individual public keys. +If the application does not have a canonical order of the signers, the individual public keys can be sorted with the ''KeySort'' algorithm to ensure that the aggregate public key is independent of the order of signers. + +The same individual public key is allowed to occur more than once in the input of ''KeyAgg'' and ''KeySort''. +This is by design: All algorithms in this proposal handle multiple signers who (claim to) have identical individual public keys properly, +and applications are not required to check for duplicate individual public keys. +In fact, applications are recommended to omit checks for duplicate individual public keys in order to simplify error handling. +Moreover, it is often impossible to tell at key aggregation which signer is to blame for the duplicate, i.e., which signer came up with an individual public key honestly and which disruptive signer copied it. +In contrast, MuSig2 is designed to identify disruptive signers at signing time (see [[#identifying-disruptive-signers|Identifying Disruptive Signers]]). + +While the algorithms in this proposal are able to handle duplicate individual public keys, there are scenarios where applications may choose to abort when encountering duplicates. +For example, we can imagine a scenario where a single entity creates a MuSig2 setup with multiple signing devices. +In that case, duplicates may not result from a malicious signing device copying an individual public key of another signing device but from accidental initialization of two devices with the same seed. +Since MuSig2 key aggregation would accept the duplicate keys and not error out, which would in turn reduce the security compared to the intended key setup, applications may reject duplicate individual public keys before passing them to MuSig2 key aggregation and ask the user to investigate. + +=== Nonce Generation === + +'''IMPORTANT''': ''NonceGen'' must have access to a high-quality random generator to draw an unbiased, uniformly random value ''rand' ''. +In contrast to BIP340 signing, the values ''k1'' and ''k2'' '''must not be derived deterministically''' from the session parameters because otherwise active adversaries can [https://medium.com/blockstream/musig-dn-schnorr-multisignatures-with-verifiably-deterministic-nonces-27424b5df9d6#e3b6 trick the victim into reusing a nonce]. + +The optional arguments to ''NonceGen'' enable a defense-in-depth mechanism that may prevent secret key exposure if ''rand' '' is accidentally not drawn uniformly at random. +If the value ''rand' '' was identical in two ''NonceGen'' invocations, but any other argument was different, the ''secnonce'' would still be guaranteed to be different as well (with overwhelming probability), and thus accidentally using the same ''secnonce'' for ''Sign'' in both sessions would be avoided. +Therefore, it is recommended to provide the optional arguments ''sk'', ''aggpk'', and ''m'' if these session parameters are already determined during nonce generation. +The auxiliary input ''extra_in'' can contain additional contextual data that has a chance of changing between ''NonceGen'' runs, +e.g., a supposedly unique session id (taken from the application), a session counter wide enough not to repeat in practice, any nonces by other signers (if already known), or the serialization of a data structure containing multiple of the above. +However, the protection provided by the optional arguments should only be viewed as a last resort. +In most conceivable scenarios, the assumption that the arguments are different between two executions of ''NonceGen'' is relatively strong, particularly when facing an active adversary. + +In some applications, it is beneficial to generate and send a ''pubnonce'' before the other signers, their individual public keys, or the message to sign is known. +In this case, only the available arguments are provided to the ''NonceGen'' algorithm. +After this preprocessing phase, the ''Sign'' algorithm can be run immediately when the message and set of signers is determined. +This way, the final signature is created quicker and with fewer round trips. +However, applications that use this method presumably store the nonces for a longer time and must therefore be even more careful not to reuse them. +Moreover, this method is not compatible with the defense-in-depth mechanism described in the previous paragraph. + +Instead of every signer broadcasting their ''pubnonce'' to every other signer, the signers can send their ''pubnonce'' to a single aggregator node that runs ''NonceAgg'' and sends the ''aggnonce'' back to the signers. +This technique reduces the overall communication. +A malicious aggregator can force the signing session to fail to produce a valid Schnorr signature but cannot negatively affect the unforgeability of the scheme. + +In general, MuSig2 signers are stateful in the sense that they first generate ''secnonce'' and then need to store it until they receive the other signers' ''pubnonces'' or the ''aggnonce''. +However, it is possible for one of the signers to be stateless. +This signer waits until it receives the ''pubnonce'' of all the other signers and until session parameters such as a message to sign, individual public keys, and tweaks are determined. +Then, the signer can run ''NonceGen'', ''NonceAgg'' and ''Sign'' in sequence and send out its ''pubnonce'' along with its partial signature. +Stateless signers may want to consider signing deterministically (see [[#modifications-to-nonce-generation|Modifications to Nonce Generation]]) to remove the reliance on the random number generator in the ''NonceGen'' algorithm. + +=== Identifying Disruptive Signers === + +The signing protocol makes it possible to identify malicious signers who send invalid contributions to a signing session in order to make the signing session abort and prevent the honest signers from obtaining a valid signature. +This property is called "identifiable aborts" and ensures that honest parties can assign blame to malicious signers who cause an abort in the signing protocol. + +Aborts are identifiable for an honest party if the following conditions hold in a signing session: +* The contributions received from all signers have not been tampered with (e.g., because they were sent over authenticated connections). +* Nonce aggregation is performed honestly (e.g., because the honest signer performs nonce aggregation on its own or because the aggregator is trusted). +* The partial signatures received from all signers are verified using the algorithm ''PartialSigVerify''. + +If these conditions hold and an honest party (signer or aggregator) runs an algorithm that fails due to invalid protocol contributions from malicious signers, then the algorithm run by the honest party will output the index of exactly one malicious signer. +Additionally, if the honest parties agree on the contributions sent by all signers in the signing session, all the honest parties who run the aborting algorithm will identify the same malicious signer. + +==== Further Remarks ==== + +Some of the algorithms specified below may also assign blame to a malicious aggregator. +While this is possible for some particular misbehavior of the aggregator, it is not guaranteed that a malicious aggregator can be identified. +More specifically, a malicious aggregator (whose existence violates the second condition above) can always make signing abort and wrongly hold honest signers accountable for the abort (e.g., by claiming to have received an invalid contribution from a particular honest signer). + +The only purpose of the algorithm ''PartialSigVerify'' is to ensure identifiable aborts, and it is not necessary to use it when identifiable aborts are not desired. +In particular, partial signatures are ''not'' signatures. +An adversary can forge a partial signature, i.e., create a partial signature without knowing the secret key for the claimed individual public key.Assume an adversary wants to forge a partial signature for individual public key ''P''. It joins the signing session pretending to be two different signers, one with individual public key ''P'' and one with another individual public key. The adversary can then set the second signer's nonce such that it will be able to produce a partial signature for ''P'' but not for the other claimed signer. An explanation of the individual steps required to create a partial signature forgery can be found in [https://gist.github.com/AdamISZ/ca974ed67889cedc738c4a1f65ff620b a write up by Adam Gibson]. +However, if ''PartialSigVerify'' succeeds for all partial signatures then ''PartialSigAgg'' will return a valid Schnorr signature.Given a list of individual public keys, it is an open question whether a BIP-340 signature valid under the corresponding aggregate public key is a proof of knowledge of all secret keys of the individual public keys. + +=== Tweaking the Aggregate Public Key === + +The aggregate public key can be ''tweaked'', which modifies the key as defined in the [[#tweaking-definition|Tweaking Definition]] subsection. +In order to apply a tweak, the KeyAgg Context output by ''KeyAgg'' is provided to the ''ApplyTweak'' algorithm with the ''is_xonly_t'' argument set to false for plain tweaking and true for X-only tweaking. +The resulting KeyAgg Context can be used to apply another tweak with ''ApplyTweak'' or obtain the aggregate public key with ''GetXonlyPubkey'' or ''GetPlainPubkey''. + +The purpose of supporting tweaking is to ensure compatibility with existing uses of tweaking, i.e., that the result of signing is a valid signature for the tweaked public key. +The MuSig2 algorithms take arbitrary tweaks as input but accepting arbitrary tweaks may negatively affect the security of the scheme.It is an open question whether allowing arbitrary tweaks from an adversary affects the unforgeability of MuSig2. +Instead, signers should obtain the tweaks according to other specifications. +This typically involves deriving the tweaks from a hash of the aggregate public key and some other information. +Depending on the specific scheme that is used for tweaking, either the plain or the X-only aggregate public key is required. +For example, to do [https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki BIP32] derivation, you call ''GetPlainPubkey'' to be able to compute the tweak, whereas [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341] TapTweaks require X-only public keys that are obtained with ''GetXonlyPubkey''. + +The tweak mode provided to ''ApplyTweak'' depends on the application: +Plain tweaking can be used to derive child public keys from an aggregate public key using [https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki BIP32]. +On the other hand, X-only tweaking is required for Taproot tweaking per [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341]. +A Taproot-tweaked public key commits to a ''script path'', allowing users to create transaction outputs that are spendable either with a MuSig2 multi-signature or by providing inputs that satisfy the script path. +Script path spends require a control block that contains a parity bit for the tweaked X-only public key. +The bit can be obtained with ''GetPlainPubkey(keyagg_ctx)[0] & 1''. + +== Algorithms == + +The following specification of the algorithms has been written with a focus on clarity. +As a result, the specified algorithms are not always optimal in terms of computation and space. +In particular, some values are recomputed but can be cached in actual implementations (see [[#general-signing-flow|General Signing Flow]]). + +=== Notation === + +The following conventions are used, with constants as defined for [https://www.secg.org/sec2-v2.pdf secp256k1]. We note that adapting this proposal to other elliptic curves is not straightforward and can result in an insecure scheme. +* Lowercase variables represent integers or byte arrays. +** The constant ''p'' refers to the field size, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F''. +** The constant ''n'' refers to the curve order, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141''. +* Uppercase variables refer to points on the curve with equation ''y2 = x3 + 7'' over the integers modulo ''p''. +** ''is_infinite(P)'' returns whether ''P'' is the point at infinity. +** ''x(P)'' and ''y(P)'' are integers in the range ''0..p-1'' and refer to the X and Y coordinates of a point ''P'' (assuming it is not infinity). +** The constant ''G'' refers to the base point, for which ''x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'' and ''y(G) = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8''. +** Addition of points refers to the usual [https://en.wikipedia.org/wiki/Elliptic_curve#The_group_law elliptic curve group operation]. +** [https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication Multiplication (⋅) of an integer and a point] refers to the repeated application of the group operation. +* Functions and operations: +** ''||'' refers to byte array concatenation. +** The function ''x[i:j]'', where ''x'' is a byte array and ''i, j ≥ 0'', returns a ''(j - i)''-byte array with a copy of the ''i''-th byte (inclusive) to the ''j''-th byte (exclusive) of ''x''. +** The function ''bytes(n, x)'', where ''x'' is an integer, returns the n-byte encoding of ''x'', most significant byte first. +** The constant ''empty_bytestring'' refers to the empty byte array. It holds that ''len(empty_bytestring) = 0''. +** The function ''xbytes(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''bytes(32, x(P))''. +** The function ''len(x)'' where ''x'' is a byte array returns the length of the array. +** The function ''has_even_y(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''y(P) mod 2 == 0''. +** The function ''with_even_y(P)'', where ''P'' is a point, returns ''P'' if ''is_infinite(P)'' or ''has_even_y(P)''. Otherwise, ''with_even_y(P)'' returns ''-P''. +** The function ''cbytes(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''a || xbytes(P)'' where ''a'' is a byte that is ''2'' if ''has_even_y(P)'' and ''3'' otherwise. +** The function ''cbytes_ext(P)'', where ''P'' is a point, returns ''bytes(33, 0)'' if ''is_infinite(P)''. Otherwise, it returns ''cbytes(P)''. +** The function ''int(x)'', where ''x'' is a 32-byte array, returns the 256-bit unsigned integer whose most significant byte first encoding is ''x''. +** The function ''lift_x(x)'', where ''x'' is an integer in range ''0..2256-1'', returns the point ''P'' for which ''x(P) = x'' + Given a candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. The valid Y coordinates for a given candidate ''x'' are the square roots of ''c = x3 + 7 mod p'' and they can be computed as ''y = ±c(p+1)/4 mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. and ''has_even_y(P)'', or fails if ''x'' is greater than ''p-1'' or no such point exists. The function ''lift_x(x)'' is equivalent to the following pseudocode: +*** Fail if ''x > p-1''. +*** Let ''c = x3 + 7 mod p''. +*** Let ''y' = c(p+1)/4 mod p''. +*** Fail if ''c ≠ y'2 mod p''. +*** Let ''y = y' '' if ''y' mod 2 = 0'', otherwise let ''y = p - y' ''. +*** Return the unique point ''P'' such that ''x(P) = x'' and ''y(P) = y''. +** The function ''cpoint(x)'', where ''x'' is a 33-byte array (compressed serialization), sets ''P = lift_x(int(x[1:33]))'' and fails if that fails. If ''x[0] = 2'' it returns ''P'' and if ''x[0] = 3'' it returns ''-P''. Otherwise, it fails. +** The function ''cpoint_ext(x)'', where ''x'' is a 33-byte array (compressed serialization), returns the point at infinity if ''x = bytes(33, 0)''. Otherwise, it returns ''cpoint(x)'' and fails if that fails. +** The function ''hashtag(x)'' where ''tag'' is a UTF-8 encoded tag name and ''x'' is a byte array returns the 32-byte hash ''SHA256(SHA256(tag) || SHA256(tag) || x)''. +* Other: +** Tuples are written by listing the elements within parentheses and separated by commas. For example, ''(2, 3, 1)'' is a tuple. + +=== Key Generation and Aggregation === + +==== Key Generation of an Individual Signer ==== + +
+Algorithm ''IndividualPubkey(sk)'':The ''IndividualPubkey'' algorithm matches the key generation procedure traditionally used for ECDSA in Bitcoin +* Inputs: +** The secret key ''sk'': a 32-byte array, freshly generated uniformly at random +* Let ''d' = int(sk)''. +* Fail if ''d' = 0'' or ''d' ≥ n''. +* Return ''cbytes(d'⋅G)''. +
+ +==== KeyAgg Context ==== + +The KeyAgg Context is a data structure consisting of the following elements: +* The point ''Q'' representing the potentially tweaked aggregate public key: an elliptic curve point +* The accumulated tweak ''tacc'': an integer with ''0 ≤ tacc < n'' +* The value ''gacc'' : 1 or -1 mod n + +We write "Let ''(Q, gacc, tacc) = keyagg_ctx''" to assign names to the elements of a KeyAgg Context. + +
+Algorithm ''GetXonlyPubkey(keyagg_ctx)'': +* Let ''(Q, _, _) = keyagg_ctx'' +* Return ''xbytes(Q)'' +
+ +
+Algorithm ''GetPlainPubkey(keyagg_ctx)'': +* Let ''(Q, _, _) = keyagg_ctx'' +* Return ''cbytes(Q)'' +
+ +==== Key Sorting ==== + +
+Algorithm ''KeySort(pk1..u)'': +* Inputs: +** The number ''u'' of individual public keys with ''0 < u < 2^32'' +** The individual public keys ''pk1..u'': ''u'' 33-byte arrays +* Return ''pk1..u'' sorted in lexicographical order. +
+ +==== Key Aggregation ==== + +
+Algorithm ''KeyAgg(pk1..u)'': +* Inputs: +** The number ''u'' of individual public keys with ''0 < u < 2^32'' +** The individual public keys ''pk1..u'': ''u'' 33-byte arrays +* Let ''pk2 = GetSecondKey(pk1..u)'' +* For ''i = 1 .. u'': +** Let ''Pi = cpoint(pki)''; fail if that fails and blame signer ''i'' for invalid individual public key. +** Let ''ai = KeyAggCoeffInternal(pk1..u, pki, pk2)''. +* Let ''Q = a1⋅P1 + a2⋅P2 + ... + au⋅Pu'' +* Fail if ''is_infinite(Q)''. +* Let ''gacc = 1'' +* Let ''tacc = 0'' +* Return ''keyagg_ctx = (Q, gacc, tacc)''. +
+ +
+Internal Algorithm ''HashKeys(pk1..u)'': +* Return ''hashKeyAgg list(pk1 || pk2 || ... || pku)'' +
+ +
+Internal Algorithm ''GetSecondKey(pk1..u)'': +* For ''j = 1 .. u'': +** If ''pkj ≠ pk1'': +*** Return ''pkj'' +* Return ''bytes(33, 0)'' +
+ +
+Internal Algorithm ''KeyAggCoeff(pk1..u, pk')'': +* Let ''pk2 = GetSecondKey(pk1..u)'': +* Return ''KeyAggCoeffInternal(pk1..u, pk', pk2)'' +
+ +
+Internal Algorithm ''KeyAggCoeffInternal(pk1..u, pk', pk2)'': +* Let ''L = HashKeys(pk1..u)'' +* If ''pk' = pk2'': +** Return 1 +* Return ''int(hashKeyAgg coefficient(L || pk')) mod n''The key aggregation coefficient is computed by hashing the individual public key instead of its index, which requires one more invocation of the SHA-256 compression function. However, it results in significantly simpler implementations because signers do not need to translate between public key indices before and after sorting. +
+ +==== Applying Tweaks ==== + +
+Algorithm ''ApplyTweak(keyagg_ctx, tweak, is_xonly_t)'': +* Inputs: +** The ''keyagg_ctx'': a [[#keyagg-context|KeyAgg Context]] data structure +** The ''tweak'': a 32-byte array +** The tweak mode ''is_xonly_t'': a boolean +* Let ''(Q, gacc, tacc) = keyagg_ctx'' +* If ''is_xonly_t'' and ''not has_even_y(Q)'': +** Let ''g = -1 mod n'' +* Else: +** Let ''g = 1'' +* Let ''t = int(tweak)''; fail if ''t ≥ n'' +* Let ''Q' = g⋅Q + t⋅G'' +** Fail if ''is_infinite(Q')'' +* Let ''gacc' = g⋅gacc mod n'' +* Let ''tacc' = t + g⋅tacc mod n'' +* Return ''keyagg_ctx' = (Q', gacc', tacc')'' +
+ +=== Nonce Generation === + +
+Algorithm ''NonceGen(sk, pk, aggpk, m, extra_in)'': +* Inputs: +** The secret signing key ''sk'': a 32-byte array (optional argument) +** The individual public key ''pk'': a 33-byte array (see [[#signing-with-tweaked-individual-keys|Signing with Tweaked Individual Keys]] for the reason that this argument is mandatory) +** The x-only aggregate public key ''aggpk'': a 32-byte array (optional argument) +** The message ''m'': a byte array (optional argument)In theory, the allowed message size is restricted because SHA256 accepts byte strings only up to size of 2^61-1 bytes (and because of the 8-byte length encoding). +** The auxiliary input ''extra_in'': a byte array with ''0 ≤ len(extra_in) ≤ 232-1'' (optional argument) +* Let ''rand' '' be a 32-byte array freshly drawn uniformly at random +* If the optional argument ''sk'' is present: +** Let ''rand'' be the byte-wise xor of ''sk'' and ''hashMuSig/aux(rand')''The random data is hashed (with a unique tag) as a precaution against situations where the randomness may be correlated with the secret signing key itself. It is xored with the secret key (rather than combined with it in a hash) to reduce the number of operations exposed to the actual secret key. +* Else: +** Let ''rand = rand' '' +* If the optional argument ''aggpk'' is not present: +** Let ''aggpk = empty_bytestring'' +* If the optional argument ''m'' is not present: +** Let ''m_prefixed = bytes(1, 0)'' +* Else: +** Let ''m_prefixed = bytes(1, 1) || bytes(8, len(m)) || m'' +* If the optional argument ''extra_in'' is not present: +** Let ''extra_in = empty_bytestring'' +* Let ''ki = int(hashMuSig/nonce(rand || bytes(1, len(pk)) || pk || bytes(1, len(aggpk)) || aggpk || m_prefixed || bytes(4, len(extra_in)) || extra_in || bytes(1, i - 1))) mod n'' for ''i = 1,2'' +* Fail if ''k1 = 0'' or ''k2 = 0'' +* Let ''R⁎,1 = k1⋅G, R⁎,2 = k2⋅G'' +* Let ''pubnonce = cbytes(R⁎,1) || cbytes(R⁎,2)'' +* Let ''secnonce = bytes(32, k1) || bytes(32, k2) || pk''The algorithms as specified here assume that the ''secnonce'' is stored as a 97-byte array using the serialization ''secnonce = bytes(32, k1) || bytes(32, k2) || pk''. The same format is used in the reference implementation and in the test vectors. However, since the ''secnonce'' is (obviously) not meant to be sent over the wire, compatibility between implementations is not a concern, and this method of storing the ''secnonce'' is merely a suggestion.
+The ''secnonce'' is effectively a local data structure of the signer which comprises the value triple ''(k1, k2, pk)'', and implementations may choose any suitable method to carry it from ''NonceGen'' (first communication round) to ''Sign'' (second communication round). In particular, implementations may choose to hide the ''secnonce'' in internal state without exposing it in an API explicitly, e.g., in an effort to prevent callers from reusing a ''secnonce'' accidentally.
+* Return ''(secnonce, pubnonce)'' +
+ +=== Nonce Aggregation === + +
+Algorithm ''NonceAgg(pubnonce1..u)'': +* Inputs: +** The number ''u'' of ''pubnonces'' with ''0 < u < 2^32'' +** The public nonces ''pubnonce1..u'': ''u'' 66-byte arrays +* For ''j = 1 .. 2'': +** For ''i = 1 .. u'': +*** Let ''Ri,j = cpoint(pubnoncei[(j-1)*33:j*33])''; fail if that fails and blame signer ''i'' for invalid ''pubnonce''. +** Let ''Rj = R1,j + R2,j + ... + Ru,j'' +* Return ''aggnonce = cbytes_ext(R1) || cbytes_ext(R2)'' +
+ +=== Session Context === + +The Session Context is a data structure consisting of the following elements: +* The aggregate public nonce ''aggnonce'': a 66-byte array +* The number ''u'' of individual public keys with ''0 < u < 2^32'' +* The individual public keys ''pk1..u'': ''u'' 33-byte arrays +* The number ''v'' of tweaks with ''0 ≤ v < 2^32'' +* The tweaks ''tweak1..v'': ''v'' 32-byte arrays +* The tweak modes ''is_xonly_t1..v'' : ''v'' booleans +* The message ''m'': a byte array + +We write "Let ''(aggnonce, u, pk1..u, v, tweak1..v, is_xonly_t1..v, m) = session_ctx''" to assign names to the elements of a Session Context. + +
+Algorithm ''GetSessionValues(session_ctx)'': +* Let ''(aggnonce, u, pk1..u, v, tweak1..v, is_xonly_t1..v, m) = session_ctx'' +* Let ''keyagg_ctx0 = KeyAgg(pk1..u)''; fail if that fails +* For ''i = 1 .. v'': +** Let ''keyagg_ctxi = ApplyTweak(keyagg_ctxi-1, tweaki, is_xonly_ti)''; fail if that fails +* Let ''(Q, gacc, tacc) = keyagg_ctxv'' +* Let ''b = int(hashMuSig/noncecoef(aggnonce || xbytes(Q) || m)) mod n'' +* Let ''R1 = cpoint_ext(aggnonce[0:33]), R2 = cpoint_ext(aggnonce[33:66])''; fail if that fails and blame nonce aggregator for invalid ''aggnonce''. +* Let ''R' = R1 + b⋅R2'' +* If ''is_infinite(R'): +** Let final nonce ''R = G'' (see [[#dealing-with-infinity-in-nonce-aggregation|Dealing with Infinity in Nonce Aggregation]]) +* Else: +** Let final nonce ''R = R' '' +* Let ''e = int(hashBIP0340/challenge(xbytes(R) || xbytes(Q) || m)) mod n'' +* Return ''(Q, gacc, tacc, b, R, e)'' +
+ +
+Algorithm ''GetSessionKeyAggCoeff(session_ctx, P)'': +* Let ''(_, u, pk1..u, _, _, _, _) = session_ctx'' +* Let ''pk = cbytes(P)'' +* Fail if ''pk'' not in ''pk1..u'' +* Return ''KeyAggCoeff(pk1..u, pk)'' +
+ +=== Signing === + +
+Algorithm ''Sign(secnonce, sk, session_ctx)'': +* Inputs: +** The secret nonce ''secnonce'' that has never been used as input to ''Sign'' before: a 97-byte array +** The secret key ''sk'': a 32-byte array +** The ''session_ctx'': a [[#session-context|Session Context]] data structure +* Let ''(Q, gacc, _, b, R, e) = GetSessionValues(session_ctx)''; fail if that fails +* Let ''k1' = int(secnonce[0:32]), k2' = int(secnonce[32:64])'' +* Fail if ''ki' = 0'' or ''ki' ≥ n'' for ''i = 1..2'' +* Let ''k1 = k1', k2 = k2' '' if ''has_even_y(R)'', otherwise let ''k1 = n - k1', k2 = n - k2' '' +* Let ''d' = int(sk)'' +* Fail if ''d' = 0'' or ''d' ≥ n'' +* Let ''P = d'⋅G'' +* Let ''pk = cbytes(P)'' +* Fail if ''pk ≠ secnonce[64:97]'' +* Let ''a = GetSessionKeyAggCoeff(session_ctx, P)''; fail if that failsFailing ''Sign'' when ''GetSessionKeyAggCoeff(session_ctx, P)'' fails is not necessary for unforgeability. It merely indicates to the caller that the scheme is not being used correctly. +* Let ''g = 1'' if ''has_even_y(Q)'', otherwise let ''g = -1 mod n'' +*
Let ''d = g⋅gacc⋅d' mod n'' (See [[#negation-of-the-secret-key-when-signing|Negation Of The Secret Key When Signing]]) +* Let ''s = (k1 + b⋅k2 + e⋅a⋅d) mod n'' +* Let ''psig = bytes(32, s)'' +* Let ''pubnonce = cbytes(k1'⋅G) || cbytes(k2'⋅G)'' +* If ''PartialSigVerifyInternal(psig, pubnonce, pk, session_ctx)'' (see below) returns failure, failVerifying the signature before leaving the signer prevents random or adversarially provoked computation errors. This prevents publishing invalid signatures which may leak information about the secret key. It is recommended but can be omitted if the computation cost is prohibitive. +* Return partial signature ''psig'' +
+ +=== Partial Signature Verification === + +
+Algorithm ''PartialSigVerify(psig, pubnonce1..u, pk1..u, tweak1..v, is_xonly_t1..v, m, i)'': +* Inputs: +** The partial signature ''psig'': a 32-byte array +** The number ''u'' of public nonces and individual public keys with ''0 < u < 2^32'' +** The public nonces ''pubnonce1..u'': ''u'' 66-byte arrays +** The individual public keys ''pk1..u'': ''u'' 33-byte arrays +** The number ''v'' of tweaks with ''0 ≤ v < 2^32'' +** The tweaks ''tweak1..v'': ''v'' 32-byte arrays +** The tweak modes ''is_xonly_t1..v'' : ''v'' booleans +** The message ''m'': a byte array +** The index of the signer ''i'' in the of public nonces and individual public keys with ''0 < i ≤ u'' +* Let ''aggnonce = NonceAgg(pubnonce1..u)''; fail if that fails +* Let ''session_ctx = (aggnonce, u, pk1..u, v, tweak1..v, is_xonly_t1..v, m)'' +* Run ''PartialSigVerifyInternal(psig, pubnoncei, pki, session_ctx)'' +* Return success iff no failure occurred before reaching this point. +
+ +
+Internal Algorithm ''PartialSigVerifyInternal(psig, pubnonce, pk, session_ctx)'': +* Let ''(Q, gacc, _, b, R, e) = GetSessionValues(session_ctx)''; fail if that fails +* Let ''s = int(psig)''; fail if ''s ≥ n'' +* Let ''R⁎,1 = cpoint(pubnonce[0:33]), R⁎,2 = cpoint(pubnonce[33:66])'' +* Let ''Re' = R⁎,1 + b⋅R⁎,2'' +* Let effective nonce ''Re = Re' '' if ''has_even_y(R)'', otherwise let ''Re = -Re' '' +* Let ''P = cpoint(pk)''; fail if that fails +* Let ''a = GetSessionKeyAggCoeff(session_ctx, P)''''GetSessionKeyAggCoeff(session_ctx, P)'' cannot fail when called from ''PartialSigVerifyInternal''. +* Let ''g = 1'' if ''has_even_y(Q)'', otherwise let ''g = -1 mod n'' +*
Let ''g' = g⋅gacc mod n'' (See [[#negation-of-the-individual-public-key-when-partially-verifying|Negation Of The Individual Public Key When Partially Verifying]]) +* Fail if ''s⋅G ≠ Re + e⋅a⋅g'⋅P'' +* Return success iff no failure occurred before reaching this point. +
+ +=== Partial Signature Aggregation === + +
+Algorithm ''PartialSigAgg(psig1..u, session_ctx)'': +* Inputs: +** The number ''u'' of signatures with ''0 < u < 2^32'' +** The partial signatures ''psig1..u'': ''u'' 32-byte arrays +** The ''session_ctx'': a [[#session-context|Session Context]] data structure +* Let ''(Q, _, tacc, _, _, R, e) = GetSessionValues(session_ctx)''; fail if that fails +* For ''i = 1 .. u'': +** Let ''si = int(psigi)''; fail if ''si ≥ n'' and blame signer ''i'' for invalid partial signature. +* Let ''g = 1'' if ''has_even_y(Q)'', otherwise let ''g = -1 mod n'' +* Let ''s = s1 + ... + su + e⋅g⋅tacc mod n'' +* Return ''sig = ''xbytes(R) || bytes(32, s)'' +
+ +=== Test Vectors and Reference Code === + +We provide a naive, highly inefficient, and non-constant time [[bip-0327/reference.py|pure Python 3 reference implementation of the key aggregation, partial signing, and partial signature verification algorithms]]. + +Standalone JSON test vectors are also available in the [[bip-0327|same directory]], to facilitate porting the test vectors into other implementations. + +The reference implementation is for demonstration purposes only and not to be used in production environments. + +== Remarks on Security and Correctness == + +=== Signing with Tweaked Individual Keys === + +The scheme in this proposal has been designed to be secure +even if signers tweak their individual secret keys with tweaks known to the adversary (e.g., as in BIP32 unhardened derivation) +before providing the corresponding individual public keys as input to key aggregation. +In particular, the scheme as specified above requires each signer to provide a final individual public key ''pk'' already to ''NonceGen'', +which writes it into the ''secnonce'' array +so that it can be checked against ''IndividualPubkey(sk)'' in the ''Sign'' algorithm. +The purpose of this check in ''Sign'' is to ensure that ''pk'', +and thus the secret key ''sk'' that will be provided to ''Sign'', +is determined before the signer sends out the ''pubnonce''. + +If the check in ''Sign'' was omitted, +and a signer supported signing with at least two different secret keys ''sk1'' and ''sk2'' +which have been obtained via tweaking another secret key with tweaks known to the adversary, +then the adversary could, after having seen the ''pubnonce'', +influence whether ''sk1'' or ''sk2'' is provided to ''Sign''. +This degree of freedom may allow the adversary to perform a generalized birthday attack and thereby forge a signature +(see [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-October/021000.html bitcoin-dev mailing list post] and [https://github.com/jonasnick/musig2-tweaking writeup] for details). + +Checking ''pk'' against ''IndividualPubkey(sk)'' is a simple way to ensure +that the secret key provided to ''Sign'' is fully determined already when ''NonceGen'' is invoked. +This removes the adversary's ability to influence the secret key after having seen the ''pubnonce'' +and thus rules out the attack.Ensuring that the secret key provided to ''Sign'' is fully determined already when ''NonceGen'' is invoked is a simple policy to rule out the attack, +but more flexible polices are conceivable. +In fact, if the signer uses nothing but the message to be signed and the list of the individual public keys of all signers to decide which secret key to use, +then it is not a problem that the adversary can influence this decision after having seen the ''pubnonce''.
+More formally, consider modified algorithms ''NonceGen' '' and ''Sign' '', where ''NonceGen' '' does not take the individual public key of the signer as input and does not store it in pubnonce, and Sign' does not check read the individual public key from pubnonce and does not check it against the secret key taken as input. +Then it suffices that for each invocation of ''NonceGen' '' with output ''(secnonce, pubnonce)'', +a function ''fsk'' is determined before sending out ''pubnonce'', +where ''fsk'' maps a pair consisting of a list of individual public keys and a message to a secret key, +such that the secret key ''sk'' and the session context ''session_ctx = (_, _, pk1..u, _, _, _, m)'' +provided to the corresponding invocation of ''Sign'(secnonce, sk, session_ctx)'', +adhere to the condition ''fsk(pk1..u, m) = sk''.
+However, this requirement is complex and hard to enforce in implementations. +The algorithms ''NonceGen'' and ''Sign'' specified in this BIP are effectively restricted to constant functions ''fsk(_, _) = sk''. +In other words, their usage ensure that the secret key ''sk'' of the signers is determined entirely when invoking ''NonceGen'', +which is enforced easily by letting ''NonceGen'' take the corresponding individual public key ''pk'' as input and checking ''pk'' against ''IndividualPubKey(sk)'' in ''Sign''.
+Note that the scheme as given in the [https://eprint.iacr.org/2020/1261 MuSig2 paper] does not perform the check in ''Sign''. +However, the security model in the paper does not cover tweaking at all and assumes a single fixed secret key. + +=== Modifications to Nonce Generation === + +Implementers must avoid modifying the ''NonceGen'' algorithm without being fully aware of the implications. +We provide two modifications to ''NonceGen'' that are secure when applied correctly and may be useful in special circumstances, summarized in the following table. + +{| class="wikitable" style="margin:auto" +! !! needs secure randomness !! needs secure counter !! needs to keep state securely !! needs aggregate nonce of all other signers (only possible for one signer) +|- +! NonceGen || ✓ ||   || ✓ ||   +|- +! CounterNonceGen ||   || ✓ || ✓ ||   +|- +! DeterministicSign ||   ||   ||   || ✓ +|} + +First, on systems where obtaining uniformly random values is much harder than maintaining a global atomic counter, it can be beneficial to modify ''NonceGen''. +The resulting algorithm ''CounterNonceGen'' does not draw ''rand' '' uniformly at random but instead sets ''rand' '' to the value of an atomic counter that is incremented whenever it is read. +With this modification, the secret signing key ''sk'' of the signer generating the nonce is '''not''' an optional argument and must be provided to ''NonceGen''. +The security of the resulting scheme then depends on the requirement that reading the counter must never yield the same counter value in two ''NonceGen'' invocations with the same ''sk''. + +Second, if there is a unique signer who is supposed to send the ''pubnonce'' last, it is possible to modify nonce generation for this single signer to not require high-quality randomness. +Such a nonce generation algorithm ''DeterministicSign'' is specified below. +Note that the only optional argument is ''rand'', which can be omitted if randomness is entirely unavailable. +''DeterministicSign'' requires the argument ''aggothernonce'' which should be set to the output of ''NonceAgg'' run on the ''pubnonce'' value of '''all''' other signers (but can be provided by an untrusted party). +Hence, using ''DeterministicSign'' is only possible for the last signer to generate a nonce and makes the signer stateless, similar to the stateless signer described in the [[#nonce-generation|Nonce Generation]] section. + +==== Deterministic and Stateless Signing for a Single Signer ==== + +
+Algorithm ''DeterministicSign(sk, aggothernonce, pk1..u, tweak1..v, is_xonly_t1..v, m, rand)'': +* Inputs: +** The secret signing key ''sk'': a 32-byte array +** The aggregate public nonce ''aggothernonce'' (see [[#modifications-to-nonce-generation|above]]): a 66-byte array +** The number ''u'' of individual public keys with ''0 < u < 2^32'' +** The individual public keys ''pk1..u'': ''u'' 32-byte arrays +** The number ''v'' of tweaks with ''0 ≤ v < 2^32'' +** The tweaks ''tweak1..v'': ''v'' 32-byte arrays +** The tweak methods ''is_xonly_t1..v'': ''v'' booleans +** The message ''m'': a byte array +** The auxiliary randomness ''rand'': a 32-byte array (optional argument) +* If the optional argument ''rand'' is present: +** Let ''sk' '' be the byte-wise xor of ''sk'' and ''hashMuSig/aux(rand)'' +* Else: +** Let ''sk' = sk'' +* Let ''keyagg_ctx0 = KeyAgg(pk1..u)''; fail if that fails +* For ''i = 1 .. v'': +** Let ''keyagg_ctxi = ApplyTweak(keyagg_ctxi-1, tweaki, is_xonly_ti)''; fail if that fails +* Let ''aggpk = GetXonlyPubkey(keyagg_ctxv)'' +* Let ''ki = int(hashMuSig/deterministic/nonce(sk' || aggothernonce || aggpk || bytes(8, len(m)) || m || bytes(1, i - 1))) mod n'' for ''i = 1,2'' +* Fail if ''k1 = 0'' or ''k2 = 0'' +* Let ''R⁎,1 = k1⋅G, R⁎,2 = k2⋅G'' +* Let ''pubnonce = cbytes(R⁎,2) || cbytes(R⁎,2)'' +* Let ''d = int(sk)'' +* Fail if ''d = 0'' or ''d ≥ n'' +* Let ''pk = cbytes(d⋅G)'' +* Let ''secnonce = bytes(32, k1) || bytes(32, k2) || pk'' +* Let ''aggnonce = NonceAgg((pubnonce, aggothernonce))''; fail if that fails and blame nonce aggregator for invalid ''aggothernonce''. +* Let ''session_ctx = (aggnonce, u, pk1..u, v, tweak1..v, is_xonly_t1..v, m)'' +* Return ''(pubnonce, Sign(secnonce, sk, session_ctx))'' +
+ +=== Tweaking Definition === + +Two modes of tweaking the aggregate public key are supported. They correspond to the following algorithms: + +
+Algorithm ''ApplyPlainTweak(P, t)'': +* Inputs: +** ''P'': a point +** The tweak ''t'': an integer with ''0 ≤ t < n '' +* Return ''P + t⋅G'' +
+ +
+Algorithm ''ApplyXonlyTweak(P, t)'': +* Return ''with_even_y(P) + t⋅G'' +
+ +=== Negation Of The Secret Key When Signing === + +In order to produce a partial signature for an X-only aggregate public key that is an aggregate of ''u'' individual public keys and tweaked ''v'' times (X-only or plain), the ''[[#Sign negation|Sign]]'' algorithm may need to negate the secret key during the signing process. + + +The following elliptic curve points arise as intermediate steps when creating a signature: +• ''Pi'' as computed in ''KeyAgg'' is the point corresponding to the ''i''-th signer's individual public key. Defining ''di' '' to be the ''i''-th signer's secret key as an integer, i.e., the ''d' '' value as computed in the ''Sign'' algorithm of the ''i''-th signer, we have + ''Pi = di'⋅G ''. +• ''Q0'' is the aggregate of the individual public keys. It is identical to value ''Q'' computed in ''KeyAgg'' and therefore defined as + ''Q0 = a1⋅P1 + a2⋅P2 + ... + au⋅Pu''. +• ''Qi'' is the tweaked aggregate public key after the ''i''-th execution of ''ApplyTweak'' for ''1 ≤ i ≤ v''. It holds that + ''Qi = f(i-1) + ti⋅G'' for ''i = 1, ..., v'' where + ''f(i-1) := with_even_y(Qi-1)'' if ''is_xonly_ti'' and + ''f(i-1) := Qi-1'' otherwise. +• ''with_even_y(Qv)'' is the final result of the key aggregation and tweaking operations. It corresponds to the output of ''GetXonlyPubkey'' applied on the final KeyAgg Context. + + +The signer's goal is to produce a partial signature corresponding to the final result of key aggregation and tweaking, i.e., the X-only public key ''with_even_y(Qv)''. + + +For ''1 ≤ i ≤ v'', we denote the value ''g'' computed in the ''i''-th execution of ''ApplyTweak'' by ''gi-1''. Therefore, ''gi-1'' is ''-1 mod n'' if and only if ''is_xonly_ti'' is true and ''Qi-1'' has an odd Y coordinate. In other words, ''gi-1'' indicates whether ''Qi-1'' needed to be negated to apply an X-only tweak: + ''f(i-1) = gi-1⋅Qi-1'' for ''1 ≤ i ≤ v''. + +Furthermore, the ''Sign'' and ''PartialSigVerify'' algorithms set value ''g'' depending on whether ''Qv'' needed to be negated to produce the (X-only) final output. For consistency, this value ''g'' is referred to as ''gv'' in this section. + ''with_even_y(Qv) = gv⋅Qv''. + + + +So, the (X-only) final public key is + ''with_even_y(Qv) + = gv⋅Qv + = gv⋅(f(v-1) + tv⋅G) + = gv⋅(gv-1⋅(f(v-2) + tv-1⋅G) + tv⋅G) + = gv⋅gv-1⋅f(v-2) + gv⋅(tv + gv-1⋅tv-1)⋅G + = gv⋅gv-1⋅f(v-2) + (sumi=v-1..v ti⋅prodj=i..v gj)⋅G + = gv⋅gv-1⋅...⋅g1⋅f(0) + (sumi=1..v ti⋅prodj=i..v gj)⋅G + = gv⋅...⋅g0⋅Q0 + gv⋅taccv⋅G'' + where ''tacci'' is computed by ''KeyAgg'' and ''ApplyTweak'' as follows: + ''tacc0 = 0 + tacci = ti + gi-1⋅tacci-1 for i=1..v mod n'' + for which it holds that ''gv⋅taccv = sumi=1..v ti⋅prodj=i..v gj''. + + + +''KeyAgg'' and ''ApplyTweak'' compute + ''gacc0 = 1 + gacci = gi-1⋅gacci-1 for i=1..v mod n'' +So we can rewrite above equation for the final public key as + ''with_even_y(Qv) = gv⋅gaccv⋅Q0 + gv⋅taccv⋅G''. + + + +Then we have + ''with_even_y(Qv) - gv⋅taccv⋅G + = gv⋅gaccv⋅Q0 + = gv⋅gaccv⋅(a1⋅P1 + ... + au⋅Pu) + = gv⋅gaccv⋅(a1⋅d1'⋅G + ... + au⋅du'⋅G) + = sumi=1..u(gv⋅gaccv⋅ai⋅di')*G''. + + +Intuitively, ''gacci'' tracks accumulated sign flipping and ''tacci'' tracks the accumulated tweak value after applying the first ''i'' individual tweaks. Additionally, ''gv'' indicates whether ''Qv'' needed to be negated to produce the final X-only result. Thus, signer ''i'' multiplies its secret key ''di' '' with ''gv⋅gaccv'' in the ''[[#Sign negation|Sign]]'' algorithm. + +==== Negation Of The Individual Public Key When Partially Verifying ==== + + +As explained in [[#negation-of-the-secret-key-when-signing|Negation Of The Secret Key When Signing]] the signer uses a possibly negated secret key + ''d = gv⋅gaccv⋅d' mod n'' +when producing a partial signature to ensure that the aggregate signature will correspond to an aggregate public key with even Y coordinate. + + + +The ''[[#SigVerify negation|PartialSigVerifyInternal]]'' algorithm is supposed to check + ''s⋅G = Re + e⋅a⋅d⋅G''. + + + +The verifier doesn't have access to ''d⋅G'' but can construct it using the individual public key ''pk'' as follows: +''d⋅G + = gv⋅gaccv⋅d'⋅G + = gv⋅gaccv⋅cpoint(pk)'' +Note that the aggregate public key and list of tweaks are inputs to partial signature verification, so the verifier can also construct ''gv'' and ''gaccv''. + + +=== Dealing with Infinity in Nonce Aggregation === + +If the nonce aggregator provides ''aggnonce = bytes(33,0) || bytes(33,0)'', either the nonce aggregator is dishonest or there is at least one dishonest signer (except with negligible probability). +If signing aborted in this case, it would be impossible to determine who is dishonest. +Therefore, signing continues so that the culprit is revealed when collecting and verifying partial signatures. + +However, the final nonce ''R'' of a BIP340 Schnorr signature cannot be the point at infinity. +If we would nonetheless allow the final nonce to be the point at infinity, then the scheme would lose the following property: +if ''PartialSigVerify'' succeeds for all partial signatures, then ''PartialSigAgg'' will return a valid Schnorr signature. +Since this is a valuable feature, we modify MuSig2* (which is defined in the appendix of the [https://eprint.iacr.org/2020/1261 MuSig2 paper]) to avoid producing an invalid Schnorr signature while still allowing detection of the dishonest signer: In ''GetSessionValues'', if the final nonce ''R'' would be the point at infinity, set it to the generator instead (an arbitrary choice). + +This modification to ''GetSessionValues'' does not affect the unforgeability of the scheme. +Given a successful adversary against the unforgeability game (EUF-CMA) for the modified scheme, a reduction can win the unforgeability game for the original scheme by simulating the modification towards the adversary: +When the adversary provides ''aggnonce' = bytes(33, 0) || bytes(33, 0)'', the reduction sets ''aggnonce = cbytes_ext(G) || bytes(33, 0)''. +For any other ''aggnonce' '', the reduction sets ''aggnonce = aggnonce' ''. +(The case that the adversary provides an ''aggnonce' ≠ bytes(33, 0) || bytes(33, 0) '' but nevertheless ''R' '' in ''GetSessionValues'' is the point at infinity happens only with negligible probability.) + +=== Choosing the Size of the Nonce === + +The [https://eprint.iacr.org/2020/1261 MuSig2 paper] contains two security proofs that apply to different variants of the scheme. +The first proof relies on the random oracle model (ROM) and applies to a scheme variant where each signer's nonce consists of four elliptic curve points. +The second proof requires a stronger model, namely the combination of the ROM and the algebraic group model (AGM), +and applies to an optimized scheme variant where the signers' nonces consist of only two points. +This proposal uses the latter, optimized scheme variant. +Relying on the stronger model is a legitimate choice for the following reasons: + +First, an approach widely taken is interpreting a Forking Lemma proof in the ROM merely as design justification and ignoring the loss of security due to the Forking Lemma. +If one believes in this approach, then the ROM may not be the optimal model in the first place because some parts of the concrete security bound are arbitrarily ignored. +One may just as well move to the ROM+AGM model, which produces bounds close to the best-known attacks, e.g., for Schnorr signatures. + +Second, as of this writing, there is no instance of a serious cryptographic scheme with a security proof in the AGM that is not secure in practice. +There are, however, insecure toy schemes with AGM security proofs, but those explicitly violate the requirements of the AGM. +[https://eprint.iacr.org/2022/226.pdf Broken AGM proofs of toy schemes] provide group elements to the adversary without declaring them as group element inputs. +In contrast, in MuSig2, all group elements that arise in the scheme are known to the adversary and declared as group element inputs. +A scheme very similar to MuSig2 and with two-point nonces was independently proven secure in the ROM and AGM by [https://eprint.iacr.org/2020/1245 Alper and Burdges]. + +== Backwards Compatibility == + +This document proposes a standard for the MuSig2 multi-signature scheme that is compatible with [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki BIP340]. +MuSig2 is ''not'' compatible with ECDSA signatures traditionally used in Bitcoin. + +== Change Log == + +To help implementers understand updates to this document, we attach a version number that resembles ''semantic versioning'' (MAJOR.MINOR.PATCH). +The MAJOR version is incremented if changes to the BIP are introduced that are incompatible with prior versions. +An exception to this rule is MAJOR version zero (0.y.z) which is for development and does not need to be incremented if backwards incompatible changes are introduced. +The MINOR version is incremented whenever the inputs or the output of an algorithm changes in a backward-compatible way or new backward-compatible functionality is added. +The PATCH version is incremented for other changes that are noteworthy (bug fixes, test vectors, important clarifications, etc.). + +* '''1.0.2''' (2024-07-22): +** Fix minor bug in the specification of ''DeterministicSign'' and add small improvement to a ''PartialSigAgg'' test vector. +* '''1.0.1''' (2024-05-14): +** Fix minor issue in ''PartialSigVerify'' vectors. +* '''1.0.0''' (2023-03-26): +** Number 327 was assigned to this BIP. +* '''1.0.0-rc.4''' (2023-03-02): +** Add expected value of ''pubnonce'' to ''NonceGen'' test vectors. +* '''1.0.0-rc.3''' (2023-02-28): +** Improve ''NonceGen'' test vectors by not using an all-zero hex string as ''rand_'' values. This change addresses potential issues in some implementations that interpret this as a special value indicating uninitialized memory or a broken random number generator and therefore return an error. +** Fix invalid length of a ''pubnonce'' in the ''PartialSigVerify'' test vectors. +** Improve ''KeySort'' test vector. +** Add explicit ''IndividualPubkey'' algorithm. +** Rename KeyGen Context to KeyAgg Context. +* '''1.0.0-rc.2''' (2022-10-28): +** Fix vulnerability that can occur in certain unusual scenarios (see [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-October/021000.html bitcoin-dev mailing list]: Add mandatory ''pk'' argument to ''NonceGen'', append ''pk'' to ''secnonce'' and check in ''Sign'' that the ''pk'' in ''secnonce'' matches. Update test vectors. +** Make sure that signer's key is in list of individual public keys by adding failure case to ''GetSessionKeyAggCoeff'' and add test vectors. +* '''1.0.0-rc.1''' (2022-10-03): Submit draft BIP to the BIPs repository +* '''0.8.6''' (2022-09-15): Clarify that implementations do not need to support every feature and add a test vector for signing with a tweaked key +* '''0.8.5''' (2022-09-05): Rename some functions to improve clarity. +* '''0.8.4''' (2022-09-02): Make naming of nonce variants ''R'' in specifications of the algorithms and reference code easier to read and more consistent. +* '''0.8.3''' (2022-09-01): Overwrite ''secnonce'' in ''sign'' reference implementation to help prevent accidental reuse and add test vector for invalid ''secnonce''. +* '''0.8.2''' (2022-08-30): Fix ''KeySort'' input length and add test vectors +* '''0.8.1''' (2022-08-26): Add ''DeterministicSign'' algorithm +* '''0.8.0''' (2022-08-26): Switch from X-only to plain public key for individual public keys. This requires updating a large portion of the test vectors. +* '''0.7.2''' (2022-08-17): Add ''NonceGen'' and ''Sign/PartialSigVerify'' test vectors for messages longer than 32 bytes. +* '''0.7.1''' (2022-08-10): Extract test vectors into separate JSON file. +* '''0.7.0''' (2022-07-31): Change ''NonceGen'' such that output when message is not present is different from when message is present but has length 0. +* '''0.6.0''' (2022-07-31): Allow variable length messages, change serialization of the message in the ''NonceGen'' hash function, and add test vectors +* '''0.5.2''' (2022-06-26): Fix ''aggpk'' in ''NonceGen'' test vectors. +* '''0.5.1''' (2022-06-22): Rename "ordinary" tweaking to "plain" tweaking. +* '''0.5.0''' (2022-06-21): Separate ApplyTweak from KeyAgg and introduce KeyGen Context. +* '''0.4.0''' (2022-06-20): Allow the output of NonceAgg to be infinity and add test vectors +* '''0.3.2''' (2022-06-02): Add a lot of test vectors and improve handling of invalid contributions in reference code. +* '''0.3.1''' (2022-05-24): Add ''NonceGen'' test vectors +* '''0.3.0''' (2022-05-24): Hash ''i - 1'' instead of ''i'' in ''NonceGen'' +* '''0.2.0''' (2022-05-19): Change order of arguments in ''NonceGen'' hash function +* '''0.1.0''' (2022-05-19): Publication of draft BIP on the bitcoin-dev mailing list + +== Footnotes == + + + +== Acknowledgements == + +We thank Brandon Black, Riccardo Casatta, Sivaram Dhakshinamoorthy, Lloyd Fournier, Russell O'Connor, and Pieter Wuille for their contributions to this document. diff --git a/packages/wasm-utxo/cli/src/parse/node.rs b/packages/wasm-utxo/cli/src/parse/node.rs index 9182e5b8..8a189c0f 100644 --- a/packages/wasm-utxo/cli/src/parse/node.rs +++ b/packages/wasm-utxo/cli/src/parse/node.rs @@ -4,8 +4,8 @@ use bitcoin::hashes::Hash; use bitcoin::psbt::Psbt; use bitcoin::{Network, ScriptBuf, Transaction}; use wasm_utxo::bitgo_psbt::{ - BitGoKeyValue, Musig2PartialSig, Musig2Participants, Musig2PubNonce, ProprietaryKeySubtype, - BITGO, + p2tr_musig2_input::{Musig2PartialSig, Musig2Participants, Musig2PubNonce}, + BitGoKeyValue, ProprietaryKeySubtype, BITGO, }; pub use crate::node::{Node, Primitive}; diff --git a/packages/wasm-utxo/src/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/bitgo_psbt/mod.rs index 850519c8..4e3b4181 100644 --- a/packages/wasm-utxo/src/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/bitgo_psbt/mod.rs @@ -3,21 +3,19 @@ //! This module provides PSBT deserialization that works across different //! bitcoin-like networks, including those with non-standard transaction formats. -mod p2tr_musig2_input; +pub mod p2tr_musig2_input; +#[cfg(test)] +mod p2tr_musig2_input_utxolib; mod propkv; mod sighash; mod zcash_psbt; -pub use p2tr_musig2_input::{ - parse_musig2_nonces, parse_musig2_partial_sigs, parse_musig2_participants, Musig2Error, - Musig2Input, Musig2PartialSig, Musig2Participants, Musig2PubNonce, -}; +use crate::{bitgo_psbt::zcash_psbt::ZcashPsbt, networks::Network}; + +use miniscript::bitcoin::{psbt::Psbt, secp256k1, CompressedPublicKey}; pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, BITGO}; pub use sighash::validate_sighash_type; -use crate::{bitgo_psbt::zcash_psbt::ZcashPsbt, networks::Network}; -use miniscript::bitcoin::{psbt::Psbt, secp256k1}; - #[derive(Debug)] pub enum DeserializeError { /// Standard bitcoin consensus decoding error @@ -133,6 +131,13 @@ impl BitGoPsbt { } } + pub fn network(&self) -> Network { + match self { + BitGoPsbt::BitcoinLike(_, network) => *network, + BitGoPsbt::Zcash(_, network) => *network, + } + } + /// Serialize the PSBT to bytes, using network-specific logic pub fn serialize(&self) -> Result, SerializeError> { match self { @@ -148,6 +153,28 @@ impl BitGoPsbt { } } + /// Get a reference to the underlying PSBT + /// + /// This works for both BitcoinLike and Zcash PSBTs, returning a reference + /// to the inner Bitcoin-compatible PSBT structure. + pub fn psbt(&self) -> &Psbt { + match self { + BitGoPsbt::BitcoinLike(ref psbt, _network) => psbt, + BitGoPsbt::Zcash(ref zcash_psbt, _network) => &zcash_psbt.psbt, + } + } + + /// Get a mutable reference to the underlying PSBT + /// + /// This works for both BitcoinLike and Zcash PSBTs, returning a reference + /// to the inner Bitcoin-compatible PSBT structure. + pub fn psbt_mut(&mut self) -> &mut Psbt { + match self { + BitGoPsbt::BitcoinLike(ref mut psbt, _network) => psbt, + BitGoPsbt::Zcash(ref mut zcash_psbt, _network) => &mut zcash_psbt.psbt, + } + } + pub fn finalize_input( &mut self, secp: &secp256k1::Secp256k1, @@ -158,9 +185,10 @@ impl BitGoPsbt { match self { BitGoPsbt::BitcoinLike(ref mut psbt, _network) => { // Use custom bitgo p2trMusig2 input finalization for MuSig2 inputs - if Musig2Input::is_musig2_input(&psbt.inputs[input_index]) { - Musig2Input::finalize_input(psbt, secp, input_index) + if p2tr_musig2_input::Musig2Input::is_musig2_input(&psbt.inputs[input_index]) { + let mut ctx = p2tr_musig2_input::Musig2Context::new(psbt, input_index) .map_err(|e| e.to_string())?; + ctx.finalize_input(secp).map_err(|e| e.to_string())?; return Ok(()); } // other inputs can be finalized using the standard miniscript::psbt::finalize_input @@ -188,10 +216,7 @@ impl BitGoPsbt { &mut self, secp: &secp256k1::Secp256k1, ) -> Result<(), Vec> { - let num_inputs = match self { - BitGoPsbt::BitcoinLike(psbt, _network) => psbt.inputs.len(), - BitGoPsbt::Zcash(zcash_psbt, _network) => zcash_psbt.psbt.inputs.len(), - }; + let num_inputs = self.psbt().inputs.len(); let mut errors = vec![]; for index in 0..num_inputs { @@ -230,6 +255,108 @@ impl BitGoPsbt { } } + /// Helper function to create a MuSig2 context for an input + /// + /// This validates that: + /// 1. The PSBT is BitcoinLike (not Zcash) + /// 2. The input index is valid + /// 3. The input is a MuSig2 input + /// + /// Returns a Musig2Context for the specified input + fn musig2_context<'a>( + &'a mut self, + input_index: usize, + ) -> Result, String> { + if self.network().mainnet() != Network::Bitcoin { + return Err("MuSig2 not supported for non-Bitcoin networks".to_string()); + } + + if matches!(self, BitGoPsbt::Zcash(_, _)) { + return Err("MuSig2 not supported for Zcash".to_string()); + } + + let psbt = self.psbt_mut(); + if input_index >= psbt.inputs.len() { + return Err(format!("Input index {} out of bounds", input_index)); + } + + // Validate this is a MuSig2 input + if !p2tr_musig2_input::Musig2Input::is_musig2_input(&psbt.inputs[input_index]) { + return Err(format!("Input {} is not a MuSig2 input", input_index)); + } + + // Create and return the context + p2tr_musig2_input::Musig2Context::new(psbt, input_index).map_err(|e| e.to_string()) + } + + /// Set the counterparty's (BitGo's) nonce in the PSBT + /// + /// # Arguments + /// * `input_index` - The index of the MuSig2 input + /// * `participant_pub_key` - The counterparty's public key + /// * `pub_nonce` - The counterparty's public nonce + pub fn set_counterparty_nonce( + &mut self, + input_index: usize, + participant_pub_key: CompressedPublicKey, + pub_nonce: musig2::PubNonce, + ) -> Result<(), String> { + let mut ctx = self.musig2_context(input_index)?; + let tap_output_key = ctx.musig2_input().participants.tap_output_key; + + // Set the nonce + ctx.set_nonce(participant_pub_key, tap_output_key, pub_nonce) + .map_err(|e| e.to_string()) + } + + /// Generate and set a user nonce for a MuSig2 input using State-Machine API + /// + /// This method uses the State-Machine API from the musig2 crate, which encapsulates + /// the SecNonce internally to prevent accidental reuse. This is the recommended + /// production API. + /// + /// # Arguments + /// * `input_index` - The index of the MuSig2 input + /// * `xpriv` - The user's extended private key (will be derived for the input) + /// * `session_id` - 32-byte session ID (use rand::thread_rng().gen() in production) + /// + /// # Returns + /// A tuple of (FirstRound, PubNonce) - keep FirstRound secret for signing later, + /// send PubNonce to the counterparty + pub fn generate_nonce_first_round( + &mut self, + input_index: usize, + xpriv: &miniscript::bitcoin::bip32::Xpriv, + session_id: [u8; 32], + ) -> Result<(musig2::FirstRound, musig2::PubNonce), String> { + let mut ctx = self.musig2_context(input_index)?; + ctx.generate_nonce_first_round(xpriv, session_id) + .map_err(|e| e.to_string()) + } + + /// Sign a MuSig2 input using State-Machine API + /// + /// This method uses the State-Machine API from the musig2 crate. The FirstRound + /// from nonce generation encapsulates the secret nonce, preventing reuse. + /// + /// # Arguments + /// * `input_index` - The index of the MuSig2 input + /// * `first_round` - The FirstRound from generate_nonce_first_round() + /// * `xpriv` - The user's extended private key + /// + /// # Returns + /// Ok(()) if the signature was successfully created and added to the PSBT + pub fn sign_with_first_round( + &mut self, + input_index: usize, + first_round: musig2::FirstRound, + xpriv: &miniscript::bitcoin::bip32::Xpriv, + ) -> Result<(), String> { + let mut ctx = self.musig2_context(input_index)?; + ctx.sign_with_first_round(first_round, xpriv) + .map_err(|e| e.to_string()) + } + /// Sign the PSBT with the provided key. /// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt. /// @@ -454,22 +581,36 @@ mod tests { script_type: fixtures::ScriptType, unsigned_bitgo_psbt: &BitGoPsbt, halfsigned_bitgo_psbt: &BitGoPsbt, - wallet_keys: &fixtures::XprvTriple, + xpriv_triple: &fixtures::XprvTriple, input_index: usize, ) -> Result<(), String> { - let user_key = wallet_keys.user_key(); + let user_xpriv = xpriv_triple.user_key(); // Clone the unsigned PSBT and sign with user key - let mut signed_psbt = unsigned_bitgo_psbt.clone(); + let mut unsigned_bitgo_psbt = unsigned_bitgo_psbt.clone(); let secp = secp256k1::Secp256k1::new(); + if script_type == fixtures::ScriptType::P2trMusig2TaprootKeypath { + // MuSig2 keypath: set nonces and sign with user key + p2tr_musig2_input::assert_set_nonce_and_sign_musig2_keypath( + xpriv_triple, + &mut unsigned_bitgo_psbt, + halfsigned_bitgo_psbt, + input_index, + )?; + + // MuSig2 inputs use proprietary key values for partial signatures, + // not standard PSBT partial_sigs, so we're done + return Ok(()); + } + // Sign with user key using the new sign method - signed_psbt - .sign(user_key, &secp) + unsigned_bitgo_psbt + .sign(user_xpriv, &secp) .map_err(|(_num_keys, errors)| format!("Failed to sign PSBT: {:?}", errors))?; // Extract partial signatures from the signed input - let signed_input = match &signed_psbt { + let signed_input = match &unsigned_bitgo_psbt { BitGoPsbt::BitcoinLike(psbt, _) => &psbt.inputs[input_index], BitGoPsbt::Zcash(_, _) => { return Err("Zcash signing not yet implemented".to_string()); @@ -631,21 +772,19 @@ mod tests { let psbt_input_stages = psbt_input_stages.unwrap(); - if script_type != fixtures::ScriptType::P2trMusig2TaprootKeypath { - assert_half_sign( - script_type, - &psbt_stages - .unsigned - .to_bitgo_psbt(network) - .expect("Failed to convert to BitGo PSBT"), - &psbt_stages - .halfsigned - .to_bitgo_psbt(network) - .expect("Failed to convert to BitGo PSBT"), - &psbt_input_stages.wallet_keys, - psbt_input_stages.input_index, - )?; - } + assert_half_sign( + script_type, + &psbt_stages + .unsigned + .to_bitgo_psbt(network) + .expect("Failed to convert to BitGo PSBT"), + &psbt_stages + .halfsigned + .to_bitgo_psbt(network) + .expect("Failed to convert to BitGo PSBT"), + &psbt_input_stages.wallet_keys, + psbt_input_stages.input_index, + )?; assert_full_signed_matches_wallet_scripts( network, diff --git a/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input.rs b/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input.rs index 83825a43..f2d05b09 100644 --- a/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input.rs +++ b/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input.rs @@ -5,13 +5,50 @@ //! https://gist.github.com/sanket1729/4b525c6049f4d9e034d27368c49f28a6 use crate::bitgo_psbt::propkv::{find_kv, is_musig2_key, BitGoKeyValue}; +use crate::fixed_script_wallet::bitgo_musig::key_agg_p2tr_musig2; use super::propkv::ProprietaryKeySubtype; use crate::bitcoin::{key::UntweakedPublicKey, CompressedPublicKey}; use miniscript::bitcoin::hashes::{hex, Hash}; -use miniscript::bitcoin::{psbt::Input, secp256k1, Psbt}; +use miniscript::bitcoin::{ + bip32::{KeySource, Xpriv, Xpub}, + psbt::Input, + secp256k1, Psbt, TapLeafHash, XOnlyPublicKey, +}; use musig2::PubNonce; +pub type TapKeyOrigins = std::collections::BTreeMap, KeySource)>; + +pub fn derive_xpriv_for_input_tap( + xpriv: &Xpriv, + tap_key_origins: &TapKeyOrigins, +) -> Result { + let secp = secp256k1::Secp256k1::new(); + for (_leaf_hashes, (fingerprint, path)) in tap_key_origins.values() { + if *fingerprint == xpriv.fingerprint(&secp) { + return xpriv + .derive_priv(&secp, path) + .map_err(|e| format!("Failed to derive xpriv: {}", e)); + } + } + Err("No xpriv found with fingerprint".to_string()) +} + +pub fn derive_xpub_for_input_tap( + xpub: &Xpub, + tap_key_origins: &TapKeyOrigins, +) -> Result { + let secp = secp256k1::Secp256k1::new(); + for (_leaf_hashes, (fingerprint, path)) in tap_key_origins.values() { + if *fingerprint == xpub.fingerprint() { + return xpub + .derive_pub(&secp, path) + .map_err(|e| format!("Failed to derive xpub: {}", e)); + } + } + Err("No xpub found with fingerprint".to_string()) +} + /// Error types for MuSig2 parsing #[derive(Debug, Clone, PartialEq, Eq)] pub enum Musig2Error { @@ -96,6 +133,26 @@ pub struct Musig2Participants { pub participant_pub_keys: [CompressedPublicKey; 2], } +impl Musig2Participants { + pub fn aggregate_pub_key(&self) -> Result { + CompressedPublicKey::from_slice( + &key_agg_p2tr_musig2(&[self.participant_pub_keys[0], self.participant_pub_keys[1]]) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!( + "Failed to aggregate public key: {}", + e + )) + })?, + ) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!( + "Failed to convert to compressed public key: {}", + e + )) + }) + } +} + /// MuSig2 public nonce data /// /// Maps: `` => `` @@ -420,11 +477,338 @@ pub struct Musig2Input { pub partial_sigs: Vec, } +/// Context for MuSig2 operations on a specific PSBT input +/// +/// This struct bundles together a mutable reference to a PSBT, the input index, +/// and the parsed MuSig2 data, providing a more ergonomic API that doesn't require +/// passing `(psbt, input_index)` to every method. +pub struct Musig2Context<'a> { + pub(super) psbt: &'a mut Psbt, + pub(super) input_index: usize, + pub(super) musig2_input: Musig2Input, +} + +impl<'a> Musig2Context<'a> { + /// Create a new Musig2Context for a specific PSBT input + pub fn new(psbt: &'a mut Psbt, input_index: usize) -> Result { + let musig2_input = Musig2Input::from_input(&psbt.inputs[input_index])?; + Ok(Self { + psbt, + input_index, + musig2_input, + }) + } + + /// Get a reference to the parsed Musig2Input + pub fn musig2_input(&self) -> &Musig2Input { + &self.musig2_input + } + + /// Finalize a MuSig2 PSBT input by aggregating signatures and delegating to miniscript + /// + /// This method: + /// 1. Parses MuSig2 proprietary data from the input + /// 2. Aggregates partial signatures into a single Schnorr signature + /// 3. Places the signature in the standard `tap_key_sig` field (BIP 371) + /// 4. Clears MuSig2 proprietary fields + /// 5. Delegates to miniscript's standard finalization to create the witness + /// + /// After aggregation, the MuSig2 signature is indistinguishable from a single-key + /// taproot signature, allowing us to reuse all standard finalization code. + pub fn finalize_input( + &mut self, + secp: &secp256k1::Secp256k1, + ) -> Result<(), Musig2Error> { + use crate::bitcoin::sighash::SighashCache; + use miniscript::psbt::PsbtExt; + + // Step 1: Collect all prevouts for sighash computation + let prevouts = collect_prevouts(self.psbt)?; + + // Get tap merkle root from input + use crate::bitcoin::taproot::TapNodeHash; + let tap_merkle_root = self.psbt.inputs[self.input_index] + .tap_merkle_root + .unwrap_or_else(|| TapNodeHash::from_byte_array([0u8; 32])); + + // Step 2: Aggregate signatures + let mut sighash_cache = SighashCache::new(&self.psbt.unsigned_tx); + let taproot_sig = self.musig2_input.aggregate_signature( + &mut sighash_cache, + &prevouts, + self.input_index, + &tap_merkle_root, + )?; + + // Step 3: Set tap_key_sig + self.psbt.inputs[self.input_index].tap_key_sig = Some(taproot_sig); + + // Step 4: Clear MuSig2 proprietary fields (they're no longer needed) + self.psbt.inputs[self.input_index] + .proprietary + .retain(|key, _| !is_musig2_key(key)); + + // Step 5: Use standard miniscript finalization for the rest! + self.psbt + .finalize_inp_mut(secp, self.input_index) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Finalization failed: {}", e)) + })?; + + Ok(()) + } + + /// Set a public nonce in the PSBT proprietary fields + /// + /// # Arguments + /// * `participant_pub_key` - The public key of the participant providing the nonce + /// * `tap_output_key` - The taproot output key (x-only tweaked aggregated key) + /// * `pub_nonce` - The public nonce to set + pub fn set_nonce( + &mut self, + participant_pub_key: CompressedPublicKey, + tap_output_key: crate::bitcoin::key::UntweakedPublicKey, + pub_nonce: PubNonce, + ) -> Result<(), Musig2Error> { + let musig2_nonce = Musig2PubNonce { + participant_pub_key, + tap_output_key, + pub_nonce, + }; + + let (key, val) = musig2_nonce.to_key_value().to_key_value(); + self.psbt.inputs[self.input_index] + .proprietary + .insert(key, val); + Ok(()) + } + + /// Set a partial signature in the PSBT proprietary fields + /// + /// # Arguments + /// * `participant_pub_key` - The public key of the participant providing the signature + /// * `tap_output_key` - The taproot output key (x-only tweaked aggregated key) + /// * `partial_sig` - The partial signature to set + pub fn set_partial_signature( + &mut self, + participant_pub_key: CompressedPublicKey, + tap_output_key: crate::bitcoin::key::UntweakedPublicKey, + partial_sig: musig2::PartialSignature, + ) -> Result<(), Musig2Error> { + let musig2_partial_sig = Musig2PartialSig { + participant_pub_key, + tap_output_key, + partial_sig: partial_sig.serialize().to_vec(), + }; + + let (key, val) = musig2_partial_sig.to_key_value().to_key_value(); + self.psbt.inputs[self.input_index] + .proprietary + .insert(key, val); + Ok(()) + } + + /// Generate and set a user nonce for a MuSig2 input using State-Machine API (FirstRound) + /// + /// This method uses the State-Machine API from the musig2 crate, which encapsulates + /// the SecNonce internally to prevent accidental reuse. This is the recommended + /// production API. + /// + /// This method: + /// 1. Derives the signer's key for this input from tap_key_origins + /// 2. Determines the signer's index in the participant list + /// 3. Creates a MuSig2 key aggregation context with taproot tweak + /// 4. Computes the taproot sighash for additional entropy + /// 5. Creates a FirstRound with the session_id as entropy + /// 6. Extracts and sets the public nonce in the PSBT proprietary fields + /// 7. Returns FirstRound (keep for signing) and PubNonce (send to counterparty) + /// + /// # Arguments + /// * `xpriv` - The signer's extended private key + /// * `session_id` - 32-byte session ID (use rand::thread_rng().gen() in production) + /// + /// # Returns + /// A tuple of (FirstRound, PubNonce) - keep FirstRound secret for signing later, + /// send PubNonce to the counterparty + pub fn generate_nonce_first_round( + &mut self, + xpriv: &Xpriv, + session_id: [u8; 32], + ) -> Result<(musig2::FirstRound, musig2::PubNonce), Musig2Error> { + use crate::bitcoin::bip32::Xpub; + use crate::bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + use crate::bitcoin::taproot::TapNodeHash; + use musig2::{KeyAggContext, SecNonceSpices}; + + // Derive the signer's key for this input + let tap_key_origins = &self.psbt.inputs[self.input_index].tap_key_origins; + let derived_xpriv = derive_xpriv_for_input_tap(xpriv, tap_key_origins).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to derive xpriv: {}", e)) + })?; + let secp = secp256k1::Secp256k1::new(); + let derived_xpub = Xpub::from_priv(&secp, &derived_xpriv); + let signer_pub_key = derived_xpub.to_pub(); + + // Determine signer index + let signer_index = self.musig2_input.get_signer_index(&signer_pub_key)?; + + // Get tap merkle root + let tap_merkle_root = self.psbt.inputs[self.input_index] + .tap_merkle_root + .unwrap_or_else(|| TapNodeHash::from_byte_array([0u8; 32])); + + // Create key aggregation context with taproot tweak + let participant_keys = self.musig2_input.get_participant_pubkeys()?; + let key_agg_ctx = KeyAggContext::new(participant_keys).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to create key agg context: {}", e)) + })?; + + let tap_tree_root_bytes = tap_merkle_root.to_byte_array(); + let key_agg_ctx = key_agg_ctx + .with_taproot_tweak(&tap_tree_root_bytes) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to apply taproot tweak: {}", e)) + })?; + + // Compute sighash for SecNonceSpices + let prevouts = collect_prevouts(self.psbt)?; + let mut sighash_cache = SighashCache::new(&self.psbt.unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + self.input_index, + &Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to compute sighash: {}", e)) + })?; + + // Convert secret key to scalar + let secret_scalar = + musig2::secp::Scalar::try_from(&derived_xpriv.private_key.secret_bytes()[..]).map_err( + |e| Musig2Error::SignatureAggregation(format!("Failed to parse secret key: {}", e)), + )?; + + // Create SecNonceSpices with message for additional entropy + let message = sighash.to_byte_array(); + let spices = SecNonceSpices::new() + .with_seckey(secret_scalar) + .with_message(&message); + + // Create FirstRound with session_id as the primary entropy source + let first_round = musig2::FirstRound::new(key_agg_ctx, session_id, signer_index, spices) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to create FirstRound: {}", e)) + })?; + + // Extract public nonce + let pub_nonce = first_round.our_public_nonce(); + + // Set the public nonce in the PSBT + let tap_output_key = self.musig2_input.participants.tap_output_key; + self.set_nonce(signer_pub_key, tap_output_key, pub_nonce.clone())?; + + // Return FirstRound (caller keeps for signing) and PubNonce (send to counterparty) + Ok((first_round, pub_nonce)) + } + + /// Sign a MuSig2 input using State-Machine API (SecondRound) + /// + /// This method uses the State-Machine API from the musig2 crate. The FirstRound + /// from nonce generation encapsulates the secret nonce, preventing reuse. + /// + /// This method: + /// 1. Derives the signer's key to identify which signature to set + /// 2. Receives nonces from all other participants into FirstRound + /// 3. Finalizes FirstRound with seckey and message → SecondRound + /// 4. Extracts the partial signature from SecondRound + /// 5. Sets the partial signature in the PSBT proprietary fields + /// + /// # Arguments + /// * `first_round` - The FirstRound from generate_nonce_first_round() + /// * `xpriv` - The signer's extended private key + /// + /// # Returns + /// Ok(()) if the signature was successfully created and set + pub fn sign_with_first_round( + &mut self, + mut first_round: musig2::FirstRound, + xpriv: &Xpriv, + ) -> Result<(), Musig2Error> { + use crate::bitcoin::bip32::Xpub; + use crate::bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + + // Derive the signer's key for this input + let tap_key_origins = &self.psbt.inputs[self.input_index].tap_key_origins; + let derived_xpriv = derive_xpriv_for_input_tap(xpriv, tap_key_origins).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to derive xpriv: {}", e)) + })?; + let secp = secp256k1::Secp256k1::new(); + let derived_xpub = Xpub::from_priv(&secp, &derived_xpriv); + let signer_pub_key = derived_xpub.to_pub(); + + // Get signer index to know which nonces to receive + let signer_index = self.musig2_input.get_signer_index(&signer_pub_key)?; + + // Receive nonces from all other participants + // We need to map each nonce to its correct signer index + for nonce_data in &self.musig2_input.nonces { + let nonce_signer_index = self + .musig2_input + .get_signer_index(&nonce_data.participant_pub_key)?; + + // Only receive nonces from other signers (not our own) + if nonce_signer_index != signer_index { + first_round + .receive_nonce(nonce_signer_index, nonce_data.pub_nonce.clone()) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!( + "Failed to receive nonce from signer {}: {}", + nonce_signer_index, e + )) + })?; + } + } + + // Compute sighash message (needed for finalize) + let prevouts = collect_prevouts(self.psbt)?; + let mut sighash_cache = SighashCache::new(&self.psbt.unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + self.input_index, + &Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to compute sighash: {}", e)) + })?; + let message = sighash.to_byte_array(); + + // Convert secret key to scalar + let secret_scalar = + musig2::secp::Scalar::try_from(&derived_xpriv.private_key.secret_bytes()[..]).map_err( + |e| Musig2Error::SignatureAggregation(format!("Failed to parse secret key: {}", e)), + )?; + + // Finalize FirstRound with seckey and message → SecondRound (signature created during finalization) + let second_round = first_round.finalize(secret_scalar, message).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to finalize FirstRound: {}", e)) + })?; + + // Extract our partial signature from SecondRound + let partial_sig: musig2::PartialSignature = second_round.our_signature(); + + // Set the partial signature in the PSBT + let tap_output_key = self.musig2_input.participants.tap_output_key; + self.set_partial_signature(signer_pub_key, tap_output_key, partial_sig) + } +} + /// Collect all prevouts (funding outputs) from PSBT inputs /// /// This helper extracts the TxOut for each input from either witness_utxo or non_witness_utxo. /// Required for computing sighashes in taproot transactions. -fn collect_prevouts(psbt: &Psbt) -> Result, Musig2Error> { +pub(crate) fn collect_prevouts(psbt: &Psbt) -> Result, Musig2Error> { let tx = &psbt.unsigned_tx; psbt.inputs .iter() @@ -475,62 +859,6 @@ impl Musig2Input { }) } - /// Finalize a MuSig2 PSBT input by aggregating signatures and delegating to miniscript - /// - /// This method: - /// 1. Parses MuSig2 proprietary data from the input - /// 2. Aggregates partial signatures into a single Schnorr signature - /// 3. Places the signature in the standard `tap_key_sig` field (BIP 371) - /// 4. Clears MuSig2 proprietary fields - /// 5. Delegates to miniscript's standard finalization to create the witness - /// - /// After aggregation, the MuSig2 signature is indistinguishable from a single-key - /// taproot signature, allowing us to reuse all standard finalization code. - pub fn finalize_input( - psbt: &mut Psbt, - secp: &secp256k1::Secp256k1, - input_index: usize, - ) -> Result<(), Musig2Error> { - use crate::bitcoin::sighash::SighashCache; - use miniscript::psbt::PsbtExt; - - // Step 1: Parse Musig2Input from PSBT input - let musig2_input = Self::from_input(&psbt.inputs[input_index])?; - - // Step 2: Collect all prevouts for sighash computation - let prevouts = collect_prevouts(psbt)?; - - // Get tap merkle root from input - use crate::bitcoin::taproot::TapNodeHash; - let tap_merkle_root = psbt.inputs[input_index] - .tap_merkle_root - .unwrap_or_else(|| TapNodeHash::from_byte_array([0u8; 32])); - - // Step 3: Aggregate signatures - let mut sighash_cache = SighashCache::new(&psbt.unsigned_tx); - let taproot_sig = musig2_input.aggregate_signature( - &mut sighash_cache, - &prevouts, - input_index, - &tap_merkle_root, - )?; - - // Step 4: Set tap_key_sig - psbt.inputs[input_index].tap_key_sig = Some(taproot_sig); - - // Step 5: Clear MuSig2 proprietary fields (they're no longer needed) - psbt.inputs[input_index] - .proprietary - .retain(|key, _| !is_musig2_key(key)); - - // Step 6: Use standard miniscript finalization for the rest! - psbt.finalize_inp_mut(secp, input_index).map_err(|e| { - Musig2Error::SignatureAggregation(format!("Finalization failed: {}", e)) - })?; - - Ok(()) - } - /// Get public nonces pub fn get_pub_nonces(&self) -> Vec { self.nonces.iter().map(|n| n.pub_nonce.clone()).collect() @@ -565,6 +893,31 @@ impl Musig2Input { .collect() } + /// Get the signer index (0 or 1) for a given public key + /// + /// This is needed for the State-Machine API (FirstRound::new) which requires + /// knowing which position in the participant list corresponds to the signer. + /// + /// # Arguments + /// * `signer_pub_key` - The public key to find in the participant list + /// + /// # Returns + /// The index (0 or 1) of the signer, or an error if the key is not found + pub fn get_signer_index( + &self, + signer_pub_key: &CompressedPublicKey, + ) -> Result { + for (index, participant_key) in self.participants.participant_pub_keys.iter().enumerate() { + if participant_key == signer_pub_key { + return Ok(index); + } + } + Err(Musig2Error::SignatureAggregation(format!( + "Signer public key {:?} not found in participants", + signer_pub_key + ))) + } + /// Aggregate MuSig2 partial signatures into a final Schnorr signature /// /// This method: @@ -664,6 +1017,143 @@ impl Musig2Input { } } +/// Set nonces and sign a MuSig2 keypath input with the user's key using BOTH APIs +/// +/// This is a comprehensive test function that validates both the Functional API (against +/// fixtures) and the State-Machine API (for internal consistency). It ensures both +/// implementations work correctly. +/// +/// # Arguments +/// * `xpriv_triple` - Triple of extended private keys (user, backup, BitGo) +/// * `unsigned_bitgo_psbt` - Unsigned PSBT (will be mutated to add nonces and signature) +/// * `halfsigned_bitgo_psbt` - Expected halfsigned PSBT for Functional API verification +/// * `input_index` - Index of the MuSig2 input +#[cfg(test)] +pub fn assert_set_nonce_and_sign_musig2_keypath( + xpriv_triple: &crate::fixed_script_wallet::test_utils::fixtures::XprvTriple, + unsigned_bitgo_psbt: &mut crate::bitgo_psbt::BitGoPsbt, + halfsigned_bitgo_psbt: &crate::bitgo_psbt::BitGoPsbt, + input_index: usize, +) -> Result<(), String> { + // Test 1: Functional API (utxolib-compatible, fixture-validated) + let mut functional_psbt = unsigned_bitgo_psbt.clone(); + super::p2tr_musig2_input_utxolib::assert_set_nonce_and_sign_musig2_keypath_utxolib( + xpriv_triple, + &mut functional_psbt, + halfsigned_bitgo_psbt, + input_index, + )?; + + // Test 2: State-Machine API (internal consistency validation, no fixture comparison) + let mut state_machine_psbt = unsigned_bitgo_psbt.clone(); + assert_set_nonce_and_sign_musig2_keypath_state_machine( + xpriv_triple, + &mut state_machine_psbt, + input_index, + )?; + + // Update the original PSBT with the functional API result for consistency + *unsigned_bitgo_psbt = functional_psbt; + + Ok(()) +} + +/// Test State-Machine API: set nonces and sign a MuSig2 keypath input using FirstRound/SecondRound +/// +/// This test helper validates that the State-Machine API works correctly by: +/// 1. Generating nonces for both participants using generate_nonce_first_round() +/// 2. Signing using sign_with_first_round() +/// 3. Finalizing and verifying the aggregated signature is valid +/// +/// Unlike the Functional API test which validates against fixtures, this test +/// validates internal consistency (signatures are valid but not deterministic). +/// +/// # Arguments +/// * `xpriv_triple` - Triple of extended private keys (user, backup, BitGo) +/// * `unsigned_bitgo_psbt` - Unsigned PSBT (will be mutated to add nonces and signature) +/// * `input_index` - Index of the MuSig2 input +#[cfg(test)] +pub fn assert_set_nonce_and_sign_musig2_keypath_state_machine( + xpriv_triple: &crate::fixed_script_wallet::test_utils::fixtures::XprvTriple, + unsigned_bitgo_psbt: &mut crate::bitgo_psbt::BitGoPsbt, + input_index: usize, +) -> Result<(), String> { + // Verify this is actually a MuSig2 input + let is_musig2 = Musig2Input::is_musig2_input(&unsigned_bitgo_psbt.psbt().inputs[input_index]); + + if !is_musig2 { + return Err(format!( + "Expected MuSig2 input at index {} but found non-MuSig2 taproot input", + input_index + )); + } + + // Generate random session IDs for production-like behavior + let user_session_id: [u8; 32] = [1u8; 32]; // Use fixed for reproducibility in tests + let bitgo_session_id: [u8; 32] = [2u8; 32]; + + // Step 1: Generate user nonce using State-Machine API + let mut user_ctx = Musig2Context::new(unsigned_bitgo_psbt.psbt_mut(), input_index) + .map_err(|e| e.to_string())?; + let (user_first_round, _user_pub_nonce) = user_ctx + .generate_nonce_first_round(xpriv_triple.user_key(), user_session_id) + .map_err(|e| e.to_string())?; + + // Step 2: Generate BitGo nonce using State-Machine API + let mut bitgo_ctx = Musig2Context::new(unsigned_bitgo_psbt.psbt_mut(), input_index) + .map_err(|e| e.to_string())?; + let (bitgo_first_round, _bitgo_pub_nonce) = bitgo_ctx + .generate_nonce_first_round(xpriv_triple.bitgo_key(), bitgo_session_id) + .map_err(|e| e.to_string())?; + + // Step 3: Verify both nonces are set + let musig2_input = Musig2Input::from_input(&unsigned_bitgo_psbt.psbt().inputs[input_index]) + .map_err(|e| e.to_string())?; + if musig2_input.nonces.len() != 2 { + return Err(format!( + "Expected 2 nonces after generation, got {}", + musig2_input.nonces.len() + )); + } + + // Step 4: Sign with user key using State-Machine API + let mut user_sign_ctx = Musig2Context::new(unsigned_bitgo_psbt.psbt_mut(), input_index) + .map_err(|e| e.to_string())?; + user_sign_ctx + .sign_with_first_round(user_first_round, xpriv_triple.user_key()) + .map_err(|e| e.to_string())?; + + // Step 5: Sign with BitGo key using State-Machine API + let mut bitgo_sign_ctx = Musig2Context::new(unsigned_bitgo_psbt.psbt_mut(), input_index) + .map_err(|e| e.to_string())?; + bitgo_sign_ctx + .sign_with_first_round(bitgo_first_round, xpriv_triple.bitgo_key()) + .map_err(|e| e.to_string())?; + + // Step 6: Verify both partial signatures are set + let musig2_input = Musig2Input::from_input(&unsigned_bitgo_psbt.psbt().inputs[input_index]) + .map_err(|e| e.to_string())?; + if musig2_input.partial_sigs.len() != 2 { + return Err(format!( + "Expected 2 partial signatures after signing, got {}", + musig2_input.partial_sigs.len() + )); + } + + // Step 7: Finalize the input to verify the signature aggregates correctly + let secp = miniscript::bitcoin::secp256k1::Secp256k1::new(); + unsigned_bitgo_psbt + .finalize_input(&secp, input_index) + .map_err(|e| format!("Finalization failed: {}", e))?; + + // Step 8: Extract transaction to verify signature is valid + let psbt = unsigned_bitgo_psbt.clone().into_psbt(); + psbt.extract_tx() + .map_err(|e| format!("Failed to extract transaction: {}", e))?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -832,4 +1322,87 @@ mod tests { fn test_musig2_keypath_matches_fixture_fullsigned() { test_musig2_keypath_matches_fixture(SignatureState::Fullsigned); } + + #[test] + fn test_state_machine_api_produces_valid_signature() { + // Load fixtures using PsbtStages to get wallet keys + let psbt_stages = fixtures::PsbtStages::load(crate::Network::Bitcoin, TxFormat::Psbt) + .expect("Failed to load PSBT stages"); + + // Find MuSig2 keypath input + let (input_index, _input_fixture) = psbt_stages + .unsigned + .find_input_with_script_type(ScriptType::P2trMusig2TaprootKeypath) + .expect("Failed to find taprootKeyPathSpend input"); + + // Get wallet keys from stages + let xpriv_triple = &psbt_stages.wallet_keys; + + // Convert to BitGoPsbt + let mut bitgo_psbt = psbt_stages + .unsigned + .to_bitgo_psbt(crate::Network::Bitcoin) + .expect("Failed to convert to BitGoPsbt"); + + // Test State-Machine API + assert_set_nonce_and_sign_musig2_keypath_state_machine( + xpriv_triple, + &mut bitgo_psbt, + input_index, + ) + .expect("State-Machine API test failed"); + + println!("✓ State-Machine API produced valid signature"); + } + + #[test] + fn test_both_apis_produce_valid_signatures() { + // This test validates that both APIs can produce valid signatures on the same input + + // Load fixtures using PsbtStages to get wallet keys + let psbt_stages = fixtures::PsbtStages::load(crate::Network::Bitcoin, TxFormat::Psbt) + .expect("Failed to load PSBT stages"); + + let (input_index, _) = psbt_stages + .unsigned + .find_input_with_script_type(ScriptType::P2trMusig2TaprootKeypath) + .expect("Failed to find MuSig2 keypath input"); + + let xpriv_triple = &psbt_stages.wallet_keys; + + // Test 1: Functional API (deterministic, fixture-validated) + let mut functional_psbt = psbt_stages + .unsigned + .to_bitgo_psbt(crate::Network::Bitcoin) + .expect("Failed to convert to BitGoPsbt"); + + let halfsigned_psbt = psbt_stages + .halfsigned + .to_bitgo_psbt(crate::Network::Bitcoin) + .expect("Failed to convert halfsigned to BitGoPsbt"); + + // Use Functional API + assert_set_nonce_and_sign_musig2_keypath( + xpriv_triple, + &mut functional_psbt, + &halfsigned_psbt, + input_index, + ) + .expect("Functional API test failed"); + + // Test 2: State-Machine API (non-deterministic, validity-checked) + let mut state_machine_psbt = psbt_stages + .unsigned + .to_bitgo_psbt(crate::Network::Bitcoin) + .expect("Failed to convert to BitGoPsbt"); + + assert_set_nonce_and_sign_musig2_keypath_state_machine( + xpriv_triple, + &mut state_machine_psbt, + input_index, + ) + .expect("State-Machine API test failed"); + + println!("✓ Both Functional and State-Machine APIs produced valid signatures"); + } } diff --git a/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input_utxolib.rs b/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input_utxolib.rs new file mode 100644 index 00000000..6b60e2b2 --- /dev/null +++ b/packages/wasm-utxo/src/bitgo_psbt/p2tr_musig2_input_utxolib.rs @@ -0,0 +1,714 @@ +//! MuSig2 Functional API for utxolib compatibility testing +//! +//! This module contains the Functional API methods that require manual SecNonce +//! handling. These methods are kept for utxolib compatibility tests and are only +//! compiled in test mode. +//! +//! For production use, prefer the State-Machine API in the parent module which +//! provides better protection against nonce reuse. + +use super::p2tr_musig2_input::{ + collect_prevouts, derive_xpriv_for_input_tap, derive_xpub_for_input_tap, Musig2Context, + Musig2Error, Musig2Input, Musig2PubNonce, +}; +use crate::bitcoin::{ + bip32::Xpriv, + hashes::Hash, + key::{TapTweak, UntweakedPublicKey}, + secp256k1::{self, Parity, PublicKey}, + sighash::TapSighash, + taproot::TapNodeHash, +}; +use crate::bitgo_psbt::BitGoPsbt; +use crate::fixed_script_wallet::RootWalletKeys; +use musig2::{secp::Point, PubNonce}; + +/// Helper function to create a MuSig2 context for an input (minimal validation) +/// +/// This is a lightweight helper for test utilities that: +/// 1. Checks the PSBT is BitcoinLike (not Zcash) +/// 2. Creates a Musig2Context +/// +/// # Note +/// This function performs minimal validation. It's designed for test utilities +/// where the PSBT structure is already known to be valid. +fn musig2_context_unchecked<'a>( + bitgo_psbt: &'a mut BitGoPsbt, + input_index: usize, +) -> Result, String> { + if matches!(bitgo_psbt, BitGoPsbt::Zcash(_, _)) { + return Err("MuSig2 not supported for Zcash".to_string()); + } + + let psbt = bitgo_psbt.psbt_mut(); + Musig2Context::new(psbt, input_index).map_err(|e| e.to_string()) +} + +/// Generate and set a user nonce for a MuSig2 input (Functional API) +/// +/// This method uses the Functional API from the musig2 crate, which requires +/// manual SecNonce handling. It's kept for utxolib compatibility tests. +/// +/// This method: +/// 1. Derives the signer's key for this input from tap_key_origins +/// 2. Computes the taproot sighash +/// 3. Generates a nonce using the provided session_id +/// 4. Sets the public nonce in the PSBT proprietary fields +/// 5. Returns both nonces (secret for signing, public for exchange with counterparty) +/// +/// # Arguments +/// * `ctx` - The Musig2Context for the input +/// * `xpriv` - The signer's extended private key +/// * `session_id` - 32-byte session ID (random in production, deterministic in tests) +/// +/// # Returns +/// A tuple of (SecNonce, PubNonce) - keep the SecNonce secret for signing later +pub fn generate_and_set_user_nonce( + ctx: &mut Musig2Context, + xpriv: &Xpriv, + session_id: [u8; 32], +) -> Result<(musig2::SecNonce, musig2::PubNonce), Musig2Error> { + use crate::bitcoin::bip32::Xpub; + use crate::bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + + // Derive the signer's key for this input + let tap_key_origins = &ctx.psbt.inputs[ctx.input_index].tap_key_origins; + let derived_xpriv = derive_xpriv_for_input_tap(xpriv, tap_key_origins) + .map_err(|e| Musig2Error::SignatureAggregation(format!("Failed to derive xpriv: {}", e)))?; + let secp = secp256k1::Secp256k1::new(); + let derived_xpub = Xpub::from_priv(&secp, &derived_xpriv); + let signer_pub_key = derived_xpub.to_pub(); + + // Compute sighash + let prevouts = collect_prevouts(ctx.psbt)?; + let mut sighash_cache = SighashCache::new(&ctx.psbt.unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + ctx.input_index, + &Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to compute sighash: {}", e)) + })?; + + // Get tap output key for nonce generation + let tap_output_key = ctx.musig2_input.participants.tap_output_key; + let mut tap_output_key_bytes = vec![0x02]; + tap_output_key_bytes.extend_from_slice(&tap_output_key.serialize()); + let agg_pk = musig2::secp::Point::try_from(tap_output_key_bytes.as_slice()).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to convert tap output key: {}", e)) + })?; + + // Convert secret key to scalar + let secret_scalar = musig2::secp::Scalar::try_from( + &derived_xpriv.private_key.secret_bytes()[..], + ) + .map_err(|e| Musig2Error::SignatureAggregation(format!("Failed to parse secret key: {}", e)))?; + + // Generate nonce + let (sec_nonce, pub_nonce) = + generate_user_nonce(secret_scalar, session_id, agg_pk, &sighash, &[]); + + // Set the public nonce in the PSBT + ctx.set_nonce(signer_pub_key, tap_output_key, pub_nonce.clone())?; + + // Return both nonces - caller keeps secret nonce for signing, sends public nonce to counterparty + Ok((sec_nonce, pub_nonce)) +} + +/// Sign a MuSig2 input and set the partial signature in the PSBT (Functional API) +/// +/// This method uses the Functional API from the musig2 crate, which requires +/// manual SecNonce handling. It's kept for utxolib compatibility tests. +/// +/// This method: +/// 1. Derives the signer's key for this input from tap_key_origins +/// 2. Computes the taproot sighash +/// 3. Aggregates public nonces from all participants +/// 4. Creates a MuSig2 key aggregation context with taproot tweak +/// 5. Creates a partial signature using the provided secret nonce +/// 6. Sets the partial signature in the PSBT proprietary fields +/// +/// # Arguments +/// * `ctx` - The Musig2Context for the input +/// * `xpriv` - The signer's extended private key +/// * `sec_nonce` - The secret nonce generated earlier for this signing session +/// +/// # Returns +/// Ok(()) if the signature was successfully created and set +pub fn sign_and_set_partial_signature( + ctx: &mut Musig2Context, + xpriv: &Xpriv, + sec_nonce: musig2::SecNonce, +) -> Result<(), Musig2Error> { + use crate::bitcoin::bip32::Xpub; + use crate::bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + use crate::bitcoin::taproot::TapNodeHash; + use musig2::AggNonce; + + // Derive the signer's key for this input + let tap_key_origins = &ctx.psbt.inputs[ctx.input_index].tap_key_origins; + let derived_xpriv = derive_xpriv_for_input_tap(xpriv, tap_key_origins) + .map_err(|e| Musig2Error::SignatureAggregation(format!("Failed to derive xpriv: {}", e)))?; + let secp = secp256k1::Secp256k1::new(); + let derived_xpub = Xpub::from_priv(&secp, &derived_xpriv); + let signer_pub_key = derived_xpub.to_pub(); + + // Compute sighash + let prevouts = collect_prevouts(ctx.psbt)?; + let tap_merkle_root = ctx.psbt.inputs[ctx.input_index] + .tap_merkle_root + .unwrap_or_else(|| TapNodeHash::from_byte_array([0u8; 32])); + let mut sighash_cache = SighashCache::new(&ctx.psbt.unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + ctx.input_index, + &Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to compute sighash: {}", e)) + })?; + let message = sighash.to_byte_array(); + + // Aggregate nonces + let pub_nonces = ctx.musig2_input.get_pub_nonces(); + let agg_nonce = AggNonce::sum(&pub_nonces); + + // Create key aggregation context + let participant_keys = ctx.musig2_input.get_participant_pubkeys()?; + let key_agg_ctx = musig2::KeyAggContext::new(participant_keys).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to create key agg context: {}", e)) + })?; + + // Apply taproot tweak + let tap_tree_root_bytes = tap_merkle_root.to_byte_array(); + let key_agg_ctx = key_agg_ctx + .with_taproot_tweak(&tap_tree_root_bytes) + .map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to apply taproot tweak: {}", e)) + })?; + + // Convert secret key to scalar + let secret_scalar = musig2::secp::Scalar::try_from( + &derived_xpriv.private_key.secret_bytes()[..], + ) + .map_err(|e| Musig2Error::SignatureAggregation(format!("Failed to parse secret key: {}", e)))?; + + // Create partial signature + let partial_sig = + create_partial_signature(&key_agg_ctx, secret_scalar, sec_nonce, &agg_nonce, message)?; + + // Set the partial signature in the PSBT + let tap_output_key = ctx.musig2_input.participants.tap_output_key; + ctx.set_partial_signature(signer_pub_key, tap_output_key, partial_sig) +} + +/// Generate and set a deterministic nonce for testing +/// +/// This is a test-only method that generates a deterministic nonce to match +/// the behavior of test fixtures. +/// +/// # Arguments +/// * `ctx` - The Musig2Context for the input +/// * `xpriv` - The signer's extended private key +/// * `counterparty_nonce` - The other party's public nonce +/// +/// # Returns +/// The generated deterministic public nonce +/// +/// # Note +/// This method should ONLY be used in tests. +pub fn generate_and_set_deterministic_nonce( + ctx: &mut Musig2Context, + xpriv: &Xpriv, + counterparty_nonce: &musig2::PubNonce, +) -> Result { + use crate::bitcoin::bip32::Xpub; + use crate::bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + use crate::bitcoin::taproot::TapNodeHash; + + // Derive the key for this input + let tap_key_origins = &ctx.psbt.inputs[ctx.input_index].tap_key_origins; + let derived_xpriv = derive_xpriv_for_input_tap(xpriv, tap_key_origins) + .map_err(|e| format!("Failed to derive xpriv: {}", e))?; + let secp = secp256k1::Secp256k1::new(); + let derived_xpub = Xpub::from_priv(&secp, &derived_xpriv); + let pub_key = derived_xpub.to_pub(); + + // Get tap_internal_key and tap_merkle_root + let internal_pub_key = ctx.psbt.inputs[ctx.input_index] + .tap_internal_key + .ok_or_else(|| "tap_internal_key is required".to_string())?; + let tap_merkle_root = ctx.psbt.inputs[ctx.input_index] + .tap_merkle_root + .unwrap_or_else(|| TapNodeHash::from_byte_array([0u8; 32])); + + // Compute sighash + let prevouts = collect_prevouts(ctx.psbt).map_err(|e| e.to_string())?; + let mut sighash_cache = SighashCache::new(&ctx.psbt.unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash( + ctx.input_index, + &Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|e| format!("Failed to compute sighash: {}", e))?; + + // Generate deterministic nonce + let pub_nonce = create_musig2_deterministic_nonce( + &derived_xpriv.private_key, + counterparty_nonce, + &internal_pub_key, + &tap_merkle_root, + &sighash, + ) + .map_err(|e| e.to_string())?; + + // Set the nonce in the PSBT + let tap_output_key = ctx.musig2_input.participants.tap_output_key; + ctx.set_nonce(pub_key, tap_output_key, pub_nonce.clone()) + .map_err(|e| e.to_string())?; + + Ok(pub_nonce) +} + +/// Generate a user nonce for MuSig2 signing (Functional API) +/// +/// This method uses the Functional API from the musig2 crate, which requires +/// manual SecNonce handling. It's kept for utxolib compatibility tests. +/// +/// This static method generates both a secret nonce (SecNonce) and public nonce (PubNonce) +/// using the provided session_id. In production, session_id should be securely random. +/// For deterministic tests, use a fixed session_id. +/// +/// # Arguments +/// * `secret_key` - The user's secret key as a scalar +/// * `session_id` - 32-byte session ID (random in production, deterministic in tests) +/// * `agg_pk` - The aggregated public key (tweaked taproot output key) +/// * `sighash` - The taproot sighash being signed +/// * `extra_input` - Optional extra randomness (use empty slice [] if not needed) +/// +/// # Returns +/// A tuple of (SecNonce, PubNonce) - the secret and public nonces +pub fn generate_user_nonce( + secret_key: musig2::secp::Scalar, + session_id: [u8; 32], + agg_pk: musig2::secp::Point, + sighash: &crate::bitcoin::sighash::TapSighash, + extra_input: &[u8], +) -> (musig2::SecNonce, PubNonce) { + use crate::bitcoin::hashes::Hash as _; + let message = sighash.to_byte_array(); + let sec_nonce = + musig2::SecNonce::generate(session_id, secret_key, agg_pk, message, extra_input); + let pub_nonce = sec_nonce.public_nonce(); + (sec_nonce, pub_nonce) +} + +/// Create a partial signature for MuSig2 (Functional API) +/// +/// This method uses the Functional API from the musig2 crate, which requires +/// manual SecNonce handling. It's kept for utxolib compatibility tests. +/// +/// This method creates a partial signature given the secret nonce and signing parameters. +/// Both nonces (from all participants) must have been exchanged before calling this. +/// +/// # Arguments +/// * `key_agg_ctx` - The key aggregation context (with taproot tweak applied) +/// * `secret_key` - The signer's secret key as a scalar +/// * `sec_nonce` - The secret nonce generated earlier +/// * `agg_nonce` - The aggregated public nonce (sum of all participants' public nonces) +/// * `message` - The message being signed (sighash) +/// +/// # Returns +/// The partial signature +pub fn create_partial_signature( + key_agg_ctx: &musig2::KeyAggContext, + secret_key: musig2::secp::Scalar, + sec_nonce: musig2::SecNonce, + agg_nonce: &musig2::AggNonce, + message: [u8; 32], +) -> Result { + musig2::sign_partial(key_agg_ctx, secret_key, sec_nonce, agg_nonce, message).map_err(|e| { + Musig2Error::SignatureAggregation(format!("Failed to create partial signature: {:?}", e)) + }) +} + +/// Creates a deterministic MuSig2 nonce for a cosigner. +/// +/// This implements the deterministic nonce generation algorithm from the BIP-327 +/// reference implementation (see `bips/bip-0327/reference.py` function `deterministic_sign` +/// and `det_nonce_hash`), as well as the musig-js library. +/// +/// This allows a cosigner to generate a nonce deterministically **after** seeing +/// other signers' nonces. This is useful for stateless cosigners that can derive +/// their nonce from the signing context without storing secret nonces. +/// +/// The algorithm uses the tag `'MuSig/deterministic/nonce'` and hashes: +/// `secretKey || aggOtherNonce || aggPubKey || msgLength || msg || i` +/// +/// **Security Note**: Unlike standard BIP-327 nonce generation, this approach +/// generates nonces after seeing other parties' nonces. This is safe because the +/// nonce is derived deterministically from all signing context, preventing nonce +/// reuse attacks. However, it requires that the other party has already committed +/// to their nonce before this function is called. +/// +/// # Arguments +/// * `secret_key` - The signer's private key +/// * `agg_other_nonce` - Aggregate of all other signers' public nonces +/// * `internal_pub_key` - The untweaked aggregated public key (x-only, 32 bytes) +/// * `tap_tree_root` - The merkle root of the tap tree +/// * `sighash` - The taproot sighash (message to sign) +/// +/// # Returns +/// The deterministic public nonce +/// +/// # Reference +/// - BIP-327 reference implementation: `bips/bip-0327/reference.py` +/// - Function: `deterministic_sign()` and `det_nonce_hash()` +/// - musig-js: `deterministicSign()` function +fn create_musig2_deterministic_nonce( + secret_key: &crate::bitcoin::secp256k1::SecretKey, + agg_other_nonce: &PubNonce, + internal_pub_key: &UntweakedPublicKey, + tap_tree_root: &TapNodeHash, + sighash: &TapSighash, +) -> Result { + use crate::bitcoin::hashes::{sha256, Hash as _, HashEngine}; + use musig2::secp::MaybeScalar; + + // BIP340-style tagged hash helper + fn tagged_hash(tag: &str, msg: &[u8]) -> [u8; 32] { + let tag_hash = sha256::Hash::hash(tag.as_bytes()); + let mut engine = sha256::Hash::engine(); + engine.input(tag_hash.as_ref()); + engine.input(tag_hash.as_ref()); + engine.input(msg); + sha256::Hash::from_engine(engine).to_byte_array() + } + + // Create tap output key (tweaked aggregated key) + // Uses BIP341 taproot tweaking: P' = P + t*G where t = tagged_hash("TapTweak", P || merkle_root) + let secp = secp256k1::Secp256k1::new(); + let (tweaked_key, _parity): (crate::bitcoin::key::TweakedPublicKey, Parity) = + internal_pub_key.tap_tweak(&secp, Some(*tap_tree_root)); + let tap_output_key = tweaked_key.to_inner().serialize(); + + // Serialize aggregate other nonce + let agg_other_nonce_bytes = agg_other_nonce.serialize(); + + // Get message bytes + let msg = sighash.to_byte_array(); + + // Get secret key bytes + let secret_key_bytes = secret_key.secret_bytes(); + + // Prepare message length prefix (8 bytes, big endian) + let msg_length = (msg.len() as u64).to_be_bytes(); + + // Generate two nonce pairs using the deterministic algorithm + // This follows the BIP-327 reference implementation's det_nonce_hash() function + let mut nonce_points = Vec::with_capacity(2); + + for i in 0u8..2 { + // Compute deterministic nonce hash: det_nonce_hash(sk_, aggothernonce, aggpk, msg, i) + // Tag: 'MuSig/deterministic/nonce' + // Input: secretKey || aggOtherNonce || aggPubKey || msgLength || msg || i + let mut hash_input = Vec::new(); + hash_input.extend_from_slice(&secret_key_bytes); + hash_input.extend_from_slice(&agg_other_nonce_bytes); + hash_input.extend_from_slice(&tap_output_key); + hash_input.extend_from_slice(&msg_length); + hash_input.extend_from_slice(&msg); + hash_input.push(i); + + let k_hash = tagged_hash("MuSig/deterministic/nonce", &hash_input); + + // Reduce hash to scalar mod n: k = det_nonce_hash(...) % n + let k_scalar = MaybeScalar::from_slice(&k_hash) + .map_err(|e| format!("Failed to create scalar from hash: {}", e))?; + + // Convert scalar to bytes for SecretKey + let k_bytes = k_scalar.serialize(); + + // Check for zero scalar (cannot occur except with negligible probability) + let zero_bytes = [0u8; 32]; + if k_bytes == zero_bytes { + return Err("Generated zero scalar for nonce".to_string()); + } + + // Compute public nonce point: R = k * G + let nonce_secret = crate::bitcoin::secp256k1::SecretKey::from_slice(&k_bytes) + .map_err(|e| format!("Failed to create secret key from scalar: {}", e))?; + + let nonce_public = PublicKey::from_secret_key(&secp, &nonce_secret); + + // Convert to Point for PubNonce + let nonce_point = Point::try_from(&nonce_public.serialize()[..]) + .map_err(|e| format!("Failed to convert nonce to Point: {}", e))?; + + nonce_points.push(nonce_point); + } + + // Create PubNonce from the two points + Ok(PubNonce::new(nonce_points[0], nonce_points[1])) +} + +/// Generate and set a deterministic nonce for testing (top-level wrapper) +/// +/// This is a test-only convenience function that creates a Musig2Context +/// and calls the generate_and_set_deterministic_nonce method. +/// +/// # Arguments +/// * `bitgo_psbt` - The PSBT to modify +/// * `input_index` - The index of the MuSig2 input +/// * `xpriv` - The signer's extended private key +/// * `counterparty_nonce` - The other party's public nonce +/// +/// # Returns +/// The generated deterministic public nonce +/// +/// # Note +/// This function should ONLY be used in tests. +pub fn generate_and_set_deterministic_nonce_from_psbt( + bitgo_psbt: &mut BitGoPsbt, + input_index: usize, + xpriv: &crate::bitcoin::bip32::Xpriv, + counterparty_nonce: &musig2::PubNonce, +) -> Result { + let mut ctx = musig2_context_unchecked(bitgo_psbt, input_index)?; + generate_and_set_deterministic_nonce(&mut ctx, xpriv, counterparty_nonce) + .map_err(|e| e.to_string()) +} + +/// Set nonces for both participants (user and BitGo) in a MuSig2 transaction, +/// simulating a round trip with the BitGo HSM. +/// +/// This function generates and sets nonces for both the user and BitGo signers, +/// then verifies they match the expected fixture nonces. +/// +/// # Arguments +/// * `user_xpriv` - User's extended private key +/// * `bitgo_xpriv` - BitGo's extended private key +/// * `wallet_keys` - Root wallet keys for derivation +/// * `unsigned_bitgo_psbt` - Unsigned PSBT (will be mutated to add nonces) +/// * `expected_nonces_fixture` - Expected nonces from test fixture for verification +/// * `input_index` - Index of the MuSig2 input +/// +/// # Returns +/// The user's secret nonce (needed for signing later) +pub fn set_nonce_musig2( + user_xpriv: &crate::bitcoin::bip32::Xpriv, + bitgo_xpriv: &crate::bitcoin::bip32::Xpriv, + wallet_keys: &RootWalletKeys, + unsigned_bitgo_psbt: &mut BitGoPsbt, + expected_nonces_fixture: &[Musig2PubNonce], + input_index: usize, +) -> Result { + // Get derived public keys for verification + let psbt = unsigned_bitgo_psbt.clone().into_psbt(); + let tap_key_origins = &psbt.inputs[input_index].tap_key_origins; + let derived_user_pub_key = + derive_xpub_for_input_tap(wallet_keys.user_key(), tap_key_origins)?.to_pub(); + let derived_bitgo_pub_key = + derive_xpub_for_input_tap(wallet_keys.bitgo_key(), tap_key_origins)?.to_pub(); + + // Step 1: Generate and set user nonce using Functional API + // Use deterministic session_id for reproducible tests (production should use random) + let deterministic_session_id = [0u8; 32]; + let (user_sec_nonce, user_pub_nonce) = generate_and_set_user_nonce_from_psbt( + unsigned_bitgo_psbt, + input_index, + user_xpriv, + deterministic_session_id, + )?; + + // Step 2: Generate and set BitGo nonce using deterministic test utility + // This matches the behavior of the TypeScript test fixtures + let bitgo_pub_nonce = generate_and_set_deterministic_nonce_from_psbt( + unsigned_bitgo_psbt, + input_index, + bitgo_xpriv, + &user_pub_nonce, + )?; + + // Verify nonces match the expected fixture + assert_eq!( + expected_nonces_fixture.len(), + 2, + "Expected 2 nonces in fixture" + ); + + let user_fixture_nonce = expected_nonces_fixture + .iter() + .find(|n| n.participant_pub_key == derived_user_pub_key) + .expect("Failed to find user nonce in fixture"); + let bitgo_fixture_nonce = expected_nonces_fixture + .iter() + .find(|n| n.participant_pub_key == derived_bitgo_pub_key) + .expect("Failed to find BitGo nonce in fixture"); + + assert_eq!( + user_pub_nonce.serialize(), + user_fixture_nonce.pub_nonce.serialize(), + "User nonce mismatch" + ); + assert_eq!( + bitgo_pub_nonce.serialize(), + bitgo_fixture_nonce.pub_nonce.serialize(), + "BitGo nonce mismatch" + ); + + Ok(user_sec_nonce) +} + +/// Sign a MuSig2 input with the user's key and verify against expected signature +/// +/// This function signs a MuSig2 input that already has nonces set, then verifies +/// the generated signature matches the expected fixture signature. +/// +/// # Arguments +/// * `user_xpriv` - User's extended private key +/// * `user_sec_nonce` - User's secret nonce (from nonce generation step) +/// * `wallet_keys` - Root wallet keys for derivation +/// * `nonce_set_psbt` - PSBT with nonces already set (will be mutated to add signature) +/// * `expected_halfsigned_input` - Expected input state after user signing +/// * `input_index` - Index of the MuSig2 input +pub fn assert_half_sign_musig2( + user_xpriv: &crate::bitcoin::bip32::Xpriv, + user_sec_nonce: musig2::SecNonce, + wallet_keys: &RootWalletKeys, + nonce_set_psbt: &mut BitGoPsbt, + expected_halfsigned_input: &Musig2Input, + input_index: usize, +) -> Result<(), String> { + // Get derived user public key for verification + let psbt = nonce_set_psbt.clone().into_psbt(); + let tap_key_origins = &psbt.inputs[input_index].tap_key_origins; + let derived_user_pub_key = + derive_xpub_for_input_tap(wallet_keys.user_key(), tap_key_origins)?.to_pub(); + + // Sign with user key using Functional API + sign_musig2_input_from_psbt(nonce_set_psbt, input_index, user_xpriv, user_sec_nonce)?; + + // Verify the signed PSBT matches the expected halfsigned fixture + let signed_psbt = nonce_set_psbt.clone().into_psbt(); + let signed_musig2_input = Musig2Input::from_input(&signed_psbt.inputs[input_index]) + .expect("Failed to parse signed Musig2 input"); + + // Check that we have the user's partial signature and it matches the fixture + let actual_user_partial_sig = signed_musig2_input + .partial_sigs + .iter() + .find(|s| s.participant_pub_key == derived_user_pub_key) + .expect("Failed to find user partial signature"); + + let expected_user_partial_sig = expected_halfsigned_input + .partial_sigs + .iter() + .find(|s| s.participant_pub_key == derived_user_pub_key) + .expect("Failed to find expected user partial signature"); + + assert_eq!( + actual_user_partial_sig.partial_sig, expected_user_partial_sig.partial_sig, + "User partial signature mismatch" + ); + + Ok(()) +} + +/// Set nonces and sign a MuSig2 keypath input with the user's key using Functional API +/// +/// This is the utxolib-compatible test variant that uses the Functional API and validates +/// against fixtures. It combines nonce generation for both participants and user signing +/// in one call, verifying results match expected fixtures. +/// +/// # Arguments +/// * `xpriv_triple` - Triple of extended private keys (user, backup, BitGo) +/// * `unsigned_bitgo_psbt` - Unsigned PSBT (will be mutated to add nonces and signature) +/// * `halfsigned_bitgo_psbt` - Expected halfsigned PSBT for verification +/// * `input_index` - Index of the MuSig2 input +pub fn assert_set_nonce_and_sign_musig2_keypath_utxolib( + xpriv_triple: &crate::fixed_script_wallet::test_utils::fixtures::XprvTriple, + unsigned_bitgo_psbt: &mut BitGoPsbt, + halfsigned_bitgo_psbt: &BitGoPsbt, + input_index: usize, +) -> Result<(), String> { + // Verify this is actually a MuSig2 input by checking for proprietary keys + let is_musig2 = match &unsigned_bitgo_psbt { + BitGoPsbt::BitcoinLike(psbt, _) => Musig2Input::is_musig2_input(&psbt.inputs[input_index]), + BitGoPsbt::Zcash(_, _) => false, + }; + + if !is_musig2 { + return Err(format!( + "Expected MuSig2 input at index {} but found non-MuSig2 taproot input", + input_index + )); + } + + // Parse expected fixture data + let halfsigned_psbt = halfsigned_bitgo_psbt.clone().into_psbt(); + let expected_halfsigned_input = Musig2Input::from_input(&halfsigned_psbt.inputs[input_index]) + .expect("Failed to parse half-signed Musig2 input"); + + let expected_nonces = &expected_halfsigned_input.nonces; + let wallet_keys = &xpriv_triple.to_root_wallet_keys(); + + // Step 1: Set nonces for both participants (Functional API) + let user_sec_nonce = set_nonce_musig2( + xpriv_triple.user_key(), + xpriv_triple.bitgo_key(), + wallet_keys, + unsigned_bitgo_psbt, + expected_nonces, + input_index, + )?; + + // Step 2: Sign with user key (Functional API) + assert_half_sign_musig2( + xpriv_triple.user_key(), + user_sec_nonce, + wallet_keys, + unsigned_bitgo_psbt, + &expected_halfsigned_input, + input_index, + )?; + + Ok(()) +} + +// Helper wrapper functions for BitGoPsbt + +/// Generate and set a user nonce for a MuSig2 input (Functional API, BitGoPsbt wrapper) +pub fn generate_and_set_user_nonce_from_psbt( + bitgo_psbt: &mut BitGoPsbt, + input_index: usize, + xpriv: &Xpriv, + session_id: [u8; 32], +) -> Result<(musig2::SecNonce, musig2::PubNonce), String> { + let mut ctx = musig2_context_unchecked(bitgo_psbt, input_index)?; + generate_and_set_user_nonce(&mut ctx, xpriv, session_id).map_err(|e| e.to_string()) +} + +/// Sign a MuSig2 input with the user's key (Functional API, BitGoPsbt wrapper) +pub fn sign_musig2_input_from_psbt( + bitgo_psbt: &mut BitGoPsbt, + input_index: usize, + xpriv: &Xpriv, + sec_nonce: musig2::SecNonce, +) -> Result<(), String> { + let mut ctx = musig2_context_unchecked(bitgo_psbt, input_index)?; + + // Check that we have both nonces + if ctx.musig2_input.nonces.len() < 2 { + return Err(format!( + "Need 2 nonces to sign, but only have {}", + ctx.musig2_input.nonces.len() + )); + } + + sign_and_set_partial_signature(&mut ctx, xpriv, sec_nonce).map_err(|e| e.to_string()) +} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/test_utils/fixtures.rs b/packages/wasm-utxo/src/fixed_script_wallet/test_utils/fixtures.rs index e87b2c0a..c30d661b 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/test_utils/fixtures.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/test_utils/fixtures.rs @@ -40,7 +40,9 @@ use std::str::FromStr; -use crate::{bitcoin::bip32::Xpriv, fixed_script_wallet::RootWalletKeys}; +use crate::{ + bitcoin::bip32::Xpriv, bitgo_psbt::p2tr_musig2_input, fixed_script_wallet::RootWalletKeys, +}; use miniscript::bitcoin::bip32::Xpub; use serde::{Deserialize, Serialize}; @@ -175,7 +177,7 @@ impl Musig2Participants { /// Validates that the parsed Musig2Participants matches this fixture pub fn assert_matches_parsed( &self, - parsed: &crate::bitgo_psbt::Musig2Participants, + parsed: &p2tr_musig2_input::Musig2Participants, ) -> Result<(), String> { // Compare tap_output_key let parsed_output_key_hex = hex::encode(parsed.tap_output_key.serialize()); @@ -227,7 +229,7 @@ impl Musig2Nonce { /// Validates that the parsed Musig2PubNonce matches this fixture pub fn assert_matches_parsed( &self, - parsed: &crate::bitgo_psbt::Musig2PubNonce, + parsed: &p2tr_musig2_input::Musig2PubNonce, ) -> Result<(), String> { // Compare participant pub key let parsed_participant_key_hex = hex::encode(parsed.participant_pub_key.to_bytes()); @@ -265,7 +267,7 @@ impl Musig2PartialSig { /// Validates that the parsed Musig2PartialSig matches this fixture pub fn assert_matches_parsed( &self, - parsed: &crate::bitgo_psbt::Musig2PartialSig, + parsed: &p2tr_musig2_input::Musig2PartialSig, ) -> Result<(), String> { // Compare participant pub key let parsed_participant_key_hex = hex::encode(parsed.participant_pub_key.to_bytes()); @@ -1067,7 +1069,7 @@ impl P2trMusig2KeyPathInput { /// Validates that the parsed Musig2 input data matches this fixture pub fn assert_matches_musig2_input( &self, - musig2_input: &crate::bitgo_psbt::Musig2Input, + musig2_input: &p2tr_musig2_input::Musig2Input, ) -> Result<(), String> { // Validate participants let fixture_participants = self @@ -1143,11 +1145,8 @@ pub enum ScriptType { P2sh, P2shP2wsh, P2wsh, - // Chain 30 and 31 - we only support script path spending for these P2trLegacyScriptPath, - // Chain 40 and 41 - script path spend P2trMusig2ScriptPath, - // Chain 40 and 41 - keypath spend P2trMusig2TaprootKeypath, } diff --git a/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs b/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs index 2f2d5512..4e77bb00 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs @@ -40,8 +40,8 @@ pub fn derivation_path(prefix: &DerivationPath, chain: u32, index: u32) -> Deriv #[derive(Debug, Clone)] pub struct RootWalletKeys { - xpubs: XpubTriple, - derivation_prefixes: [DerivationPath; 3], + pub xpubs: XpubTriple, + pub derivation_prefixes: [DerivationPath; 3], } impl RootWalletKeys { @@ -55,6 +55,18 @@ impl RootWalletKeys { } } + pub fn user_key(&self) -> &Xpub { + &self.xpubs[0] + } + + pub fn backup_key(&self) -> &Xpub { + &self.xpubs[1] + } + + pub fn bitgo_key(&self) -> &Xpub { + &self.xpubs[2] + } + pub fn new(xpubs: XpubTriple) -> Self { Self::new_with_derivation_prefixes( xpubs,