diff --git a/README.mediawiki b/README.mediawiki index 98540b7ba5..108b7604c0 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1451,6 +1451,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Specification | Draft |- +| [[bip-0445.md|445]] +| +| FROST Signing Protocol for BIP340 Signatures +| Sivaram Dhakshinamoorthy +| Specification +| Draft +|- | [[bip-0446.md|446]] | Consensus (soft fork) | OP_TEMPLATEHASH diff --git a/bip-0445.md b/bip-0445.md new file mode 100644 index 0000000000..2adbf7b631 --- /dev/null +++ b/bip-0445.md @@ -0,0 +1,857 @@ +``` + BIP: 445 + Title: FROST Signing Protocol for BIP340 Signatures + Authors: Sivaram Dhakshinamoorthy + Status: Draft + Type: Specification + Assigned: 2026-01-30 + License: CC0-1.0 + Discussion: 2024-07-31: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ + Requires: 340 +``` + +## Abstract + +This document proposes a standard for the Flexible Round-Optimized Schnorr Threshold (FROST) signing protocol. The standard is compatible with [BIP340][bip340] public keys and signatures. It supports *tweaking*, which allows deriving [BIP32][bip32] child keys from the threshold public key and creating [BIP341][bip341] Taproot outputs with key and script paths. + +## Copyright + +This document is made available under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). +The accompanying source code is licensed under the [MIT license](https://opensource.org/license/mit). + +## Motivation + +The FROST signature scheme enables threshold Schnorr signatures. In a *t-of-n* threshold configuration, any *t*[^t-edge-cases] participants can cooperatively produce a Schnorr signature that is indistinguishable from a signature produced by a single signer. FROST signatures are unforgeable as long as fewer than *t* participants are corrupted. The signing protocol remains functional provided that at least *t* honest participants retain access to their secret key shares. + +[^t-edge-cases]: While *t = n* and *t = 1* are in principle supported, simpler alternatives are available in these cases. In the case *t = n*, using a dedicated *n-of-n* multi-signature scheme such as MuSig2 (see [BIP327][bip327]) instead of FROST avoids the need for an interactive DKG. The case *t = 1* can be realized by letting one signer generate an ordinary [BIP340][bip340] key pair and transmitting the key pair to every other signer, who can check its consistency and then simply use the ordinary [BIP340][bip340] signing algorithm. Signers still need to ensure that they agree on a key pair. + +The IRTF has published [RFC 9591][rfc9591], which specifies the FROST signing protocol for several elliptic curve and hash function combinations, including secp256k1 with SHA-256, the cryptographic primitives used in Bitcoin. However, the signatures produced by RFC 9591 are incompatible with BIP340 Schnorr signatures due to the X-only public keys introduced in BIP340. Additionally, RFC 9591 does not specify key tweaking mechanisms, which are essential for Bitcoin applications such as [BIP32][bip32] key derivation and [BIP341][bip341] Taproot. This document addresses these limitations by specifying a BIP340-compatible variant of FROST signing protocol that supports key tweaking. + +Following the initial publication of the FROST protocol[[KG20][frost1]], several optimized variants have been proposed to improve computational efficiency and bandwidth optimization: FROST2[[CKM21][frost2]], FROST2-BTZ[[BTZ21][stronger-security-frost]], and FROST3[[RRJSS][roast], [CGRS23][olaf]]. Among these variants, FROST3 is the most efficient variant to date. + +This document specifies the FROST3 variant[^frost3-security]. The FROST3 signing protocol shares substantial similarities with the MuSig2 signing protocol specified in [BIP327][bip327]. Accordingly, this specification adopts several design principles from BIP327, including support for key tweaking, partial signature verification, and identifiable abort mechanisms. We note that significant portions of this document have been directly adapted from BIP327 due to the similarities in the signing protocols. Key generation for FROST signing is out of scope for this document. + +[^frost3-security]: The FROST3 signing scheme has been proven existentially unforgeable for both trusted dealer and distributed key generation setups. When using a trusted dealer for key generation, security reduces to the standard One-More Discrete Logarithm (OMDL) assumption. When instantiated with a distributed key generation protocol such as SimplPedPoP, security reduces to the Algebraic One-More Discrete Logarithm (AOMDL) assumption. + +The on-chain footprint of a FROST Taproot output is essentially a single BIP340 public key, and a transaction spending the output only requires a single signature cooperatively produced by at least *t* 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 a *t-of-n* policy implemented using `OP_CHECKSIGADD` as introduced in [BIP342][bip342]. +As a side effect, the number *n* of signers is not limited by any consensus rules when using FROST. + +Moreover, FROST offers a **higher level of privacy** than `OP_CHECKSIGADD`: FROST 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 the threshold public key, the shared Taproot output can have script spending paths that are hidden unless used. + +## 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). + +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. + +### Key Material and Setup + +A FROST key generation protocol configures a group of *n* participants with a *threshold public key* (representing a *t-of-n* threshold policy). +The corresponding *threshold secret key* is Shamir secret-shared among all *n* participants, where each participant holds a distinct long-term *secret share*. +This ensures that any subset of at least *t* participants can jointly run the FROST signing protocol to produce a signature under the *threshold secret key*. + +Key generation for FROST signing is out of scope for this document. Implementations can use either a trusted dealer setup, as specified in [Appendix C of RFC 9591](https://www.rfc-editor.org/rfc/rfc9591.html#name-trusted-dealer-key-generati), or a distributed key generation (DKG) protocol such as [ChillDKG](https://github.com/BlockstreamResearch/bip-frost-dkg). The appropriate choice depends on the implementation's trust model and operational requirements. + +This protocol distinguishes between two public key formats: *plain public keys* are 33-byte compressed public keys traditionally used in Bitcoin, while *X-only public keys* are 32-byte keys defined in [BIP340][bip340]. +Key generation protocols produce *public shares* and *threshold public keys* in the plain format. During signing, we conditionally negate *secret shares* to ensure the resulting threshold-signature verifies under the corresponding *X-only threshold public key*. + +> [!WARNING] +> Key generation protocols must commit the *threshold public key* to an unspendable script path as recommended in [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23). This prevents a malicious party from embedding a hidden script path during key generation that would allow them to bypass the *t-of-n* threshold policy. + +#### Protocol Parties and Network Setup + +There are *u* (where *t <= u <= n < 2^32*)[^n-bound] participants and one coordinator initiating the FROST signing protocol. +Each participant has a point-to-point communication link to the coordinator (but participants do not have direct communication links to each other). + +[^n-bound]: This bound on *n* comes from the identifier encoding. A participant identifier is serialized as a 4-byte big-endian integer and fed into the tagged hash function that binds the nonces to the signer set, so it must fit in 32 bits. No realistic threshold setup approaches 2^32 participants, so the bound doesn't limit practical implementations. + +If there is no dedicated coordinator, one of the participants can act as the coordinator. Alternatively, the protocol can be run without any coordinator, with each signer sending its contributions to every other signer, as described in [BIP327][bip327]. + +This document is written from the coordinator's perspective because the key generation methods compatible with this BIP, a trusted dealer setup and ChillDKG, also involve a central third party. Implementations are therefore likely to reuse the same setup for signing. + +#### Signing Inputs and Outputs + +Each signing session requires two inputs: a participant's long-term *secret share* (individual to each participant, not shared with the coordinator) and a [Signers Context](#signers-context)[^signers-ctx-struct] data structure (common to all participants and the coordinator). + +[^signers-ctx-struct]: The Signers Context represents the public data of signing participants: their identifiers (*id1..u*) and public shares (*pubshare1..u*). +Implementations may represent this as simply as two separate lists passed to signing APIs. +The threshold public key *thresh_pk* can be stored for efficiency or recomputed when needed using *DeriveThreshPubkey*. +Similarly, the values *n* and *t* are used only for validation, and can be omitted if validation is not performed. + +This signing protocol is compatible with any key generation protocol that produces valid FROST keys. +Valid keys satisfy: (1) each *secret share* is a Shamir share of the *threshold secret key*, and (2) each *public share* equals the scalar multiplication *secshare \* G*. +Before signing, the signers context must pass *ValidateSignersCtx*, which rejects duplicate identifiers and confirms the key material reproduces the threshold public key. The signing algorithms (*Sign*, *PartialSigVerify*, and *PartialSigAgg*) include this check for clarity, so an implementation can instead validate a context once and skip the repeated checks in later calls. +For comprehensive validation of the entire key material, *ValidateSignersCtx* can be run on all possible *u* signing participants. + +> [!IMPORTANT] +> Passing *ValidateSignersCtx* ensures functional compatibility with the signing protocol but does not guarantee the security of the key generation protocol itself. + +The output of the FROST signing protocol is a BIP340 Schnorr signature that verifies under the *X-only threshold public key* as if it were produced by a single signer using the *threshold secret key*. + +### General Signing Flow + +The coordinator and signing participants must be determined before initiating the signing protocol. +The signing participants information is stored in a [Signers Context](#signers-context) data structure. +The *threshold public key* may optionally be tweaked by initializing a [Tweak Context](#tweak-context) at this stage. + +Whenever the signing participants want to sign a message, the basic order of operations to create a threshold-signature is as follows: + +**First communication round:** +Signers begin the signing session by running *NonceGen* to compute their *secnonce* and *pubnonce*.[^nonce-serialization-detail] +Each signer sends their *pubnonce* to the coordinator, who aggregates them using *NonceAgg* to produce an aggregate nonce and sends it back to all signers. + +[^nonce-serialization-detail]: 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 [FROST paper][olaf]. +However, serialization is a technical detail that is irrelevant for users of FROST interfaces. + +**Second communication 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 their long-term *secret share*, *secnonce* and the session context. +Then, each signer sends their partial signature to the coordinator, who runs *PartialSigAgg* to produce the final signature. +If all parties behaved honestly, the result passes [BIP340][bip340] verification. + +![Frost signing flow](./bip-0445/docs/frost-signing-flow.png) + +A malicious coordinator can cause the signing session to fail but cannot compromise the unforgeability of the scheme. Even when colluding with up to *t-1* signers, a malicious coordinator cannot forge a signature. + +> [!WARNING] +> 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. + +> [!WARNING] +> The computation of *GetSessionValues* and storage of the result must be protected against modification from an untrusted third party. +> Such a party would have complete control over the aggregate public key and message to be signed. + +### Nonce Generation + +*NonceGen* must have access to a high-quality random generator to draw an unbiased, uniformly random value *rand'*. + +> [!WARNING] +> In contrast to BIP340 signing, the values *k1* and *k2* **must not be derived deterministically** from the session parameters, because deterministic nonces enable a complete key-recovery attack in multi-party discrete-logarithm signatures[[MPSW18][musig]].[^det-nonce] + +The optional arguments to *NonceGen* enable a defense-in-depth mechanism that may prevent secret share 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 *secshare*, *pubshare*, *thresh_pk*, 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, the coordinator may enable preprocessing of nonce generation to reduce signing latency. +Participants run *NonceGen* to generate a batch of *pubnonce* values before the message or Signers Context[^preprocess-round1] is known, which are stored with the coordinator (e.g., on a centralized server). +During this preprocessing phase, only the available arguments are provided to *NonceGen*. +When a signing session begins, the coordinator selects and aggregates *pubnonces* of the signing participants, enabling them to run *Sign* immediately once the message 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. +Generating the nonces ahead of time in this manner does not affect the unforgeability of the scheme. + +[^det-nonce]: A signer's partial signature has the form *s = k1 + b k2 + c λ d*, where *(k1, k2)* is the secret nonce, *d* is the secret share, and the coefficients *b* (nonce binding), *c* (challenge), and *λ* (Lagrange interpolation over the signer set) are public. With deterministic nonces, an honest signer reproduces the identical *(k1, k2)* on every signing attempt for a given message. A malicious co-signer exploits this by replaying the session three times on that message and contributing a different nonce each time, which changes the aggregate nonce and therefore both *b* and *c*, while *λ* stays fixed because the signer set is unchanged. The three resulting partial signatures form three linear equations in the unknowns *(k1, k2, d)*, which the co-signer solves to recover the victim's secret share *d*. This adapts the derandomization attack from Section 3.2 ("Derandomized Signing") of [[MPSW18][musig]], and a similar replay attack is noted for the stateless deterministic signing case in [^det-signer-set]. + +[^preprocess-round1]: When preprocessing *NonceGen* round, the Signers Context can be extended to include the *pubnonces* of the signing participants, as these are generated and stored before the signing session begins. + +FROST signers are typically stateful: they generate *secnonce*, store it, and later use it to produce a partial signature after receiving the aggregated nonce. +However, stateless signing is possible when one signer receives the aggregate nonce of all OTHER signers before generating their own nonce. +In coordinator-based setups, the coordinator facilitates this by collecting pubnonces from the other signers, computing their aggregate (*aggothernonce*), and providing it to the stateless signer. +The stateless signer then runs *NonceGen*, *NonceAgg*, and *Sign* in sequence, sending its *pubnonce* and partial signature simultaneously to the coordinator, who computes the final aggregate nonce for all OTHER signers. +In coordinator-less setups, any one signer can achieve stateless operation by generating their nonce after seeing all other signers' *pubnonces*. +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 coordinator is trusted to aggregate the *pubnonces* correctly). +- The partial signatures received from all signers are verified using the algorithm *PartialSigVerify*. + +If these conditions hold and an honest party (signer or coordinator) 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 (within the input list) of exactly one malicious signer. +Additionally, whenever more than one honest party runs an aborting algorithm on the same contributions, they all identify the same malicious signer. + +In the coordinator setup assumed by this BIP, a signer receives only the aggregate nonce from the coordinator and never the individual *pubnonces* of the other signers, so it cannot recompute the aggregation to confirm it was done honestly and must trust the coordinator for the second condition. Because *PartialSigVerify* requires the full list of *pubnonces* and partial signatures, the coordinator (or a signer acting as the coordinator) is the natural party to run it and assign blame, as it is the only party that receives every signer's contribution.[^coordinator-less] + +[^coordinator-less]: In coordinator-less setups (see the [Protocol Parties and Network Setup](#protocol-parties-and-network-setup) section), each signer broadcasts its contributions to every other signer, so every honest signer holds the full set of *pubnonces* and partial signatures and can run *PartialSigVerify* to assign blame on its own. + +#### Further Remarks + +Some of the algorithms specified below may also assign blame to a malicious coordinator. +While this is possible for some particular misbehavior of the coordinator, it is not guaranteed that a malicious coordinator can be identified. +More specifically, a malicious coordinator (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 share for that particular participant public share.[^partialsig-forgery] +However, if *PartialSigVerify* succeeds for all partial signatures then *PartialSigAgg* will return a valid Schnorr signature. + +[^partialsig-forgery]: Assume a malicious participant intends to forge a partial signature for the participant with public share *P*. It participates in the signing session pretending to be two distinct signers: one with the public share *P* and the other with its own public share. The adversary then sets the nonce for the second signer in such a way that allows it to generate a partial signature for *P*. As a side effect, it cannot generate a valid partial signature for its own public share. An explanation of the steps required to create a partial signature forgery can be found in [this document](https://gist.github.com/siv2r/0eab97bae9b7186ef2a4919e49d3b426). + +### Tweaking the Threshold Public Key + +The threshold 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 Tweak Context output by *TweakCtxInit* 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 Tweak Context can be used to apply another tweak with *ApplyTweak* or obtain the threshold 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 FROST signing algorithms take arbitrary tweaks as input but accepting arbitrary tweaks may negatively affect the security of the scheme.[^arbitrary-tweaks] +Instead, signers should obtain the tweaks according to other specifications. +This typically involves deriving the tweaks from a hash of the threshold public key and some other information. +Depending on the specific scheme that is used for tweaking, either the plain or the X-only threshold public key is required. +For example, to do [BIP32][bip32] derivation, you call *GetPlainPubkey* to be able to compute the tweak, whereas [BIP341][bip341] TapTweaks require X-only public keys that are obtained with *GetXonlyPubkey*. + +[^arbitrary-tweaks]: It is an open question whether allowing *arbitrary* tweaks from an adversary affects the unforgeability of FROST. The standard tweaking used for [BIP32][bip32] derivation and [BIP341][bip341] Taproot corresponds to re-randomizing the threshold public key with an additive tweak, which has been proven to preserve unforgeability under the same AOMDL assumption (in the random oracle model) that underlies plain FROST[[GK24][rerandomized-frost]]. + +The tweak mode provided to *ApplyTweak* depends on the application: +Plain tweaking can be used to derive child public keys from a threshold public key using [BIP32][bip32]. +On the other hand, X-only tweaking is required for Taproot tweaking per [BIP341][bip341]. +A Taproot-tweaked public key commits to a *script path*, allowing users to create transaction outputs that are spendable either with a FROST threshold-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(tweak_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 algorithms are defined over the **[secp256k1](https://www.secg.org/sec2-v2.pdf) group and its associated scalar field**. We note that adapting this proposal to other elliptic curves is not straightforward and can result in an insecure scheme. + +#### Cryptographic Types and Operations + +We rely on the following types and conventions throughout this document: + +- **Types:** Points on the curve are represented by the object *GE*, and scalars are represented by *Scalar*. +- **Naming:** Points are denoted using uppercase letters (e.g., *P*, *Q*), while scalars are denoted using lowercase letters (e.g., *r*, *s*). +- **Mathematical Context:** Points are group elements under elliptic curve addition. The group includes all points on the secp256k1 curve plus the point at infinity (the identity element). +- **Arithmetic:** The operators +, -, and · are overloaded depending on their operands: + - **Scalar Arithmetic:** When applied to two *Scalar* operands, +, -, and · denote integer addition, subtraction, and multiplication modulo the group order. + - **Point Addition:** When applied to two *GE* operands, + denotes the elliptic curve [group addition operation](https://en.wikipedia.org/wiki/Elliptic_curve#The_group_law). + - **Scalar Multiplication:** The notation r · P denotes [scalar multiplication](https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication) (the repeated addition of point P, r times). + +The reference code vendors the secp256k1lab library to handle underlying arithmetic, serialization, deserialization, and auxiliary functions. To improve the readability of this specification, we utilize simplified notation aliases for the library's internal methods, as mapped below: + + +| Notation | secp256k1lab | Description | +| --- | --- | --- | +| *p* | *FE.SIZE* | Field element size | +| *ord* | *GE.ORDER*, *Scalar.SIZE* | Group order | +| *G* | *G* | The secp256k1 generator point | +| *inf_point* | *GE()* | The infinity point | +| *is_infinity(P)* | *P.infinity* | Returns whether *P* is the point at infinity | +| *x(P)* | *P.x* | Returns the x-coordinate of a non-infinity point *P*, in the range *[0, p−1]* | +| *y(P)* | *P.y* | Returns the y-coordinate of a non-infinity point *P*, in the range *[0, p-1]* | +| *has_even_y(P)* | *P.has_even_y()* | Returns whether *P* has an even y-coordinate | +| *with_even_y(P)* | - | Returns the version of point *P* that has an even y-coordinate. If *P* already has an even y-coordinate (or is infinity), it is returned unchanged. Otherwise, its negation *-P* is returned | +| *xbytes(P)* | *P.to_bytes_xonly()* | Returns the 32-byte x-only serialization of a non-infinity point *P* | +| *cbytes(P)* | *P.to_bytes_compressed()* | Returns the 33-byte compressed serialization of a non-infinity point *P* | +| *cbytes_ext(P)* | *P.to_bytes_compressed
_with_infinity()* | Returns the 33-byte compressed serialization of a point *P*. If *P* is the point at infinity, it is encoded as a 33-byte array of zeros. | +| *lift_x(x)*[^liftx-soln] | *GE.lift_x(x)* | Decodes a 32-byte x-only serialization *x* into a non-infinity point P. The resulting point always has an even y-coordinate. | +| *cpoint(b)* | *GE.from_bytes_compressed(b)* | Decodes a 33-byte compressed serialization *b* into a non-infinity point | +| *cpoint_ext(b)* | *GE.from_bytes_compressed
_with_infinity(b)* | Decodes a 33-byte compressed serialization *b* into a point. If *b* is a 33-byte array of zeros, it returns the point at infinity | +| *scalar_to_bytes(s)* | *s.to_bytes()* | Returns the 32-byte serialization of a scalar *s* | +| *scalar_from_bytes_checked(b)* | *Scalar.from_bytes_checked(b)* | Deserializes a 32-byte array *b* to a scalar, fails if the value is ≥ *ord* | +| *scalar_from_bytes
_nonzero_checked(b)* | *Scalar.from_bytes
_nonzero_checked(b)* | Deserializes a 32-byte array *b* to a scalar, fails if the value is zero or ≥ *ord* | +| *scalar_from_bytes_wrapping(b)* | *Scalar.from_bytes_wrapping(b)* | Deserializes a 32-byte array *b* to a scalar, reducing the value modulo *ord* | +| *hashtag(x)* | *tagged_hash(tag, x)* | Computes a 32-byte domain-separated hash of the byte array *x*. The output is *SHA256(SHA256(tag) \|\| SHA256(tag) \|\| x)*, where *tag* is UTF-8 encoded string unique to the context | +| *random_bytes(n)* | - | Returns *n* bytes, sampled uniformly at random using a cryptographically secure pseudorandom number generator (CSPRNG) | +| *xor_bytes(a, b)* | *xor_bytes(a, b)* | Returns byte-wise xor of *a* and *b* | + + +[^liftx-soln]: 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 [Quadratic residue](https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus)) if they exist, which can be checked by squaring and comparing with *c*. + +#### Auxiliary and Byte-string Operations + +The following helper functions and notation are used for operations on standard integers and byte arrays, independent of curve arithmetic. Note that like Scalars, these variables are denoted by lowercase letters (e.g., *x*, *n*); the intended type is implied by context. + +| Notation | Description | +| --- | --- | +| *\|\|* | Refers to byte array concatenation | +| *len(x)* | Returns the length of the byte array *x* in bytes | +| *x[i:j]* | Returns the sub-array of the byte array *x* starting at index *i* (inclusive) and ending at *j* (exclusive). The result has length *j - i* | +| *empty_bytestring* | A constant representing an empty byte array where length is 0 | +| *bytes(n, x)* | Returns the big-endian *n*-byte encoding of the integer *x* | +| *count(x, lst)* | Returns the number of times the element *x* occurs in the list *lst* | +| *has_duplicates(lst)* | Returns *True* if any element in *lst* appears more than once, *False* otherwise | +| *sorted(lst)* | Returns a new list containing the elements of *lst* arranged in ascending order | +| *(a, b, ...)* | Refers to a tuple containing the listed elements | + +> [!NOTE] +> In the following algorithms, all scalar arithmetic is understood to be modulo the group order. For example, *a · b* implicitly means *a · b mod order* + +### Key Material and Setup + +#### Signers Context + +The Signers Context is a data structure consisting of the following elements: + +- The total number *n* of participants involved in key generation: an integer with *2 ≤ n < 232* +- The threshold number *t* of participants required to issue a signature: an integer with *1 ≤ t ≤ n* +- The number *u* of signing participants: an integer with *t ≤ u ≤ n* +- The list of participant identifiers *id1..u*: *u* distinct integers, each with *0 ≤ idi ≤ n - 1* +- The list of participant public shares *pubshare1..u*: *u* 33-byte arrays, each a compressed serialized point +- The threshold public key *thresh_pk*: a 33-byte array, compressed serialized point + +We write "Let *(n, t, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx*" to assign names to the elements of Signers Context. + +Algorithm *ValidateSignersCtx(signers_ctx)*: + +- Inputs: + - The *signers_ctx*: a [Signers Context](#signers-context) data structure +- *(n, t, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx* +- Fail if not *1 ≤ t ≤ n* +- Fail if not *t ≤ u ≤ n* +- For *i = 1 .. u*: + - Fail if not *0 ≤ idi ≤ n - 1* + - Let *Pi = cpoint(pubsharei)*; fail if that fails +- Fail if *has_duplicates(id1..u)* +- Fail if *DeriveThreshPubkey(id1..u, P1..u) ≠ thresh_pk* +- No return + +Internal Algorithm *DeriveThreshPubkey(id1..u, P1..u)*[^derive-thresh-no-validate-inputs] + +- *Q = inf_point* +- For *i = 1..u*: + - *λ = DeriveInterpolatingValue(id1..u, idi)* + - *Q = Q + λ · Pi* +- Return *cbytes(Q)* + +[^derive-thresh-no-validate-inputs]: *DeriveThreshPubkey* does not validate its inputs. Its only caller, *ValidateSignersCtx*, deserializes the public shares into points and validates them (and the identifiers) beforehand. + +Internal Algorithm *DeriveInterpolatingValue(id1..u, my_id):* + +- Fail if *my_id* not in *id1..u* +- Fail if *has_duplicates(id1..u)* +- Let *num = Scalar(1)* +- Let *deno = Scalar(1)* +- For *i = 1..u*: + - If *idi ≠ my_id*: + - Let *num = num · Scalar(idi + 1)[^lagrange-shift]  (mod ord)* + - Let *deno = deno · Scalar(idi - my_id)  (mod ord)* +- *λ = num · deno-1  (mod ord)* +- Return *λ* + +[^lagrange-shift]: The standard Lagrange interpolation coefficient uses the formula *idi / (idi - my_id)* for each term in the product, where ids are in the range *1..n*. However, since participant identifiers in this protocol are zero-indexed (range *0..n-1*), we shift them by adding 1. This transforms each term to *(idi+1) / (idi - my_id)*. + +### Tweaking the Threshold Public Key + +#### Tweak Context + +The Tweak Context is a data structure consisting of the following elements: + +- The point *Q* representing the potentially tweaked threshold public key: a *GE* +- The value *gacc*: *Scalar(1)* or *Scalar(-1)* +- The accumulated tweak *tacc*: a *Scalar* + +We write "Let *(Q, gacc, tacc) = tweak_ctx*" to assign names to the elements of a Tweak Context. + +> [!TIP] +> The Tweak Context is identical to the *KeyAgg Context* defined in [BIP327][bip327]. Implementations with existing BIP327 code can reuse it to implement this data structure and its algorithms. + +Algorithm *TweakCtxInit(thresh_pk):* + +- Input: + - The threshold public key *thresh_pk*: a 33-byte array, compressed serialized point +- Let *Q = cpoint(thresh_pk)*; fail if that fails +- Fail if *is_infinity(Q)* +- Let *gacc = Scalar(1)* +- Let *tacc = Scalar(0)* +- Return *tweak_ctx = (Q, gacc, tacc)* + +Algorithm *GetXonlyPubkey(tweak_ctx)*: + +- Inputs: + - The *tweak_ctx*: a [Tweak Context](#tweak-context) data structure +- Let *(Q, _, _) = tweak_ctx* +- Return *xbytes(Q)* + +Algorithm *GetPlainPubkey(tweak_ctx)*: + +- Inputs: + - The *tweak_ctx*: a [Tweak Context](#tweak-context) data structure +- Let *(Q, _, _) = tweak_ctx* +- Return *cbytes(Q)* + +#### Applying Tweaks + +Algorithm *ApplyTweak(tweak_ctx, tweak, is_xonly_t)*: + +- Inputs: + - The *tweak_ctx*: a [Tweak Context](#tweak-context) data structure + - The *tweak*: a 32-byte array, serialized scalar + - The tweak mode *is_xonly_t*: a boolean +- Let *(Q, gacc, tacc) = tweak_ctx* +- If *is_xonly_t* and not *has_even_y(Q)*: + - Let *g = Scalar(-1)* +- Else: + - Let *g = Scalar(1)* +- Let *t = scalar_from_bytes_checked(tweak)*; fail if that fails +- Let *Q' = g · Q + t · G* + - Fail if *is_infinity(Q')* +- Let *gacc' = g · gacc  (mod ord)* +- Let *tacc' = t + g · tacc  (mod ord)* +- Return *tweak_ctx' = (Q', gacc', tacc')* + +### Nonce Generation + +Algorithm *NonceGen(secshare, pubshare, thresh_pk, m, extra_in)*: + +- Inputs: + - The participant secret signing share *secshare*: a 32-byte array, serialized scalar (optional argument) + - The participant public share *pubshare*: a 33-byte array, compressed serialized point (optional argument) + - The x-only threshold public key *thresh_pk*: a 32-byte array, X-only serialized point (optional argument). When tweaks have been applied, this is ideally the tweaked threshold public key from *GetXonlyPubkey(tweak_ctx)*. + - The message *m*: a byte array (optional argument)[^max-msg-len] + - The auxiliary input *extra_in*: a byte array with *0 ≤ len(extra_in) ≤ 232-1* (optional argument) +- Let *rand' = random_bytes(32)* +- If the optional argument *secshare* is present: + - Let *rand = xor_bytes(secshare, hashBIP0445/aux(rand'))*[^sk-xor-rand] +- Else: + - Let *rand = rand'* +- If the optional argument *pubshare* is not present: + - Let *pubshare* = *empty_bytestring* +- If the optional argument *thresh_pk* is not present: + - Let *thresh_pk* = *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 = scalar_from_bytes_wrapping(hashBIP0445/nonce(rand || bytes(1, len(pubshare)) || pubshare || bytes(1, len(thresh_pk)) || thresh_pk || m_prefixed || bytes(4, len(extra_in)) || extra_in || bytes(1, i - 1)))* for *i = 1,2* +- Fail if *k1 = Scalar(0)* or *k2 = Scalar(0)*[^negligible-zero-scalar] +- Let *R\*,1 = k1 · G*, *R\*,2 = k2 · G* +- Let *pubnonce = cbytes(R\*,1) || cbytes(R\*,2)* +- Let *secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2)*[^secnonce-ser] +- Return *(secnonce, pubnonce)* + +[^sk-xor-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 share itself. It is xored with the secret share (rather than combined with it in a hash) to reduce the number of operations exposed to the actual secret share. + +[^secnonce-ser]: The algorithms as specified here assume that the *secnonce* is stored as a 64-byte array using the serialization *secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2)*. 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 pair *(k1, k2)*, 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. + +[^max-msg-len]: 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). + +[^negligible-zero-scalar]: These are unreachable errors, included for completeness: such a value equals *Scalar(0)* only with negligible probability. The reference implementation checks the condition with an assertion. + +### Nonce Aggregation + +Algorithm *NonceAgg(pubnonce1..u)*: + +- Inputs: + - The number *u* of signing participants: an integer with *t ≤ u ≤ n* + - The list of participant public nonces *pubnonce1..u*: *u* 66-byte array, each an output of *NonceGen* +- 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 at index *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 *signers_ctx*: a [Signers Context](#signers-context) data structure +- The aggregate public nonce *aggnonce*: a 66-byte array, output of *NonceAgg* +- The number *v* of tweaks with *0 ≤ v < 2^32* +- The list of tweaks *tweak1..v*: *v* 32-byte arrays, each a serialized scalar +- The list of tweak modes *is_xonly_t1..v* : *v* booleans +- The message *m*: a byte array[^max-msg-len] + +We write "Let *(signers_ctx, aggnonce, 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 *(signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m) = session_ctx* +- *ValidateSignersCtx(signers_ctx)*; fail if that fails +- Let *(_, _, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx* +- Let *tweak_ctx0 = TweakCtxInit(thresh_pk)*; fail if that fails +- For *i = 1 .. v*: + - Let *tweak_ctxi = ApplyTweak(tweak_ctxi-1, tweaki, is_xonly_ti)*; fail if that fails +- Let *(Q, gacc, tacc) = tweak_ctxv* +- Let *ser_ids* = *SerializeIds(id1..u)*[^canonical-ids-det-sign] +- Let *b* = *scalar_from_bytes_wrapping(hashBIP0445/noncecoef(ser_ids || aggnonce || xbytes(Q) || m))* +- Fail if *b = Scalar(0)*[^negligible-zero-scalar] +- Let *R1 = cpoint_ext(aggnonce[0:33]), R2 = cpoint_ext(aggnonce[33:66])*; fail if that fails and blame the coordinator for invalid *aggnonce*. +- Let *R' = R1 + b · R2* +- If *is_infinity(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 = scalar_from_bytes_wrapping(hashBIP0340/challenge((xbytes(R) || xbytes(Q) || m)))* +- Fail if *e = Scalar(0)*[^negligible-zero-scalar] +- Return (Q, gacc, tacc, id1..u, pubshare1..u, b, R, e) + +Internal Algorithm *SerializeIds(id1..u)*: + +- *res = empty_bytestring* +- For *id* in *sorted(id1..u)*: + - *res = res || bytes(4, id)* +- Return *res* + +[^canonical-ids-det-sign]: The identifiers are sorted so that *b* commits to the signer *set*, not the order they appear in. This matters for *DeterministicSign*, where a signer reproduces the same secret nonce *(k1, k2)* whenever its inputs are unchanged. Suppose an implementation sorts the identifiers when deriving this nonce but not when deriving *b*. A malicious coordinator can then replay one signing session under three orderings of the same signer set: the victim returns the same nonce each time, but *b* and the challenge *e* change with the order. The three partial signatures *s = k1 + b k2 + e λ d* form a system of three linear equations in *(k1, k2, d)*, which the coordinator solves to recover the secret share *d*. Sorting prevents this. It is the order analog of the attack in [^det-signer-set]. + +### Signing + +Algorithm *Sign(secnonce, secshare, my_id, session_ctx)*: + +- Inputs: + - The secret nonce *secnonce* that has never been used as input to *Sign* before: a 64-byte array[^secnonce-ser] + - The participant secret signing share *secshare*: a 32-byte array, serialized scalar + - The participant identifier *my_id*: an integer with *0 ≤ my_id ≤ n-1* + - The *session_ctx*: a [Session Context](#session-context) data structure +- Let *(Q, gacc, _, id1..u, pubshare1..u, b, R, e) = GetSessionValues(session_ctx)*; fail if that fails +- Let *k1' = scalar_from_bytes_nonzero_checked(secnonce[0:32])*; fail if that fails +- Let *k2' = scalar_from_bytes_nonzero_checked(secnonce[32:64])*; fail if that fails +- Let *k1 = k1', k2 = k2'* if *has_even_y(R)*, otherwise let *k1 = -k1', k2 = -k2'* +- Let *d' = scalar_from_bytes_nonzero_checked(secshare)*; fail if that fails +- Let *pubshare = cbytes(d' · G)* +- Fail if *pubshare* not in *pubshare1..u* +- Fail if *my_id* not in *id1..u* +- Let *λ = DeriveInterpolatingValue(id1..u, my_id)*; fail if that fails +- Let *g = Scalar(1)* if *has_even_y(Q)*, otherwise let *g = Scalar(-1)* +- Let *d = g · gacc · d'  (mod ord)* (See [Negation of Secret Share When Signing](#negation-of-the-secret-share-when-signing)) +- Let *s = k1 + b · k2 + e · λ · d  (mod ord)* +- Let *psig = scalar_to_bytes(s)* +- Let *pubnonce = cbytes(k1' · G) || cbytes(k2' · G)* +- If *PartialSigVerifyInternal(psig, my_id, pubnonce, pubshare, session_ctx)* (see below) returns failure, fail[^why-verify-partialsig] +- Return partial signature *psig* + +[^why-verify-partialsig]: Verifying 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 share. It is recommended but can be omitted if the computation cost is prohibitive. + +### Partial Signature Verification + +Algorithm *PartialSigVerify(psig, pubnonce1..u, signers_ctx, tweak1..v, is_xonly_t1..v, m, i)*: + +- Inputs: + - The partial signature *psig*: a 32-byte array, serialized scalar + - The list of public nonces *pubnonce1..u*: *u* 66-byte arrays, each an output of *NonceGen* + - The *signers_ctx*: a [Signers Context](#signers-context) data structure + - The number *v* of tweaks with *0 ≤ v < 2^32* + - The list of tweaks *tweak1..v*: *v* 32-byte arrays, each a serialized scalar + - The list of tweak modes *is_xonly_t1..v* : *v* booleans + - The message *m*: a byte array[^max-msg-len] + - The index *i* of the signer in the list of public nonces where *0 ≤ i ≤ u - 1* +- ValidateSignersCtx(signers_ctx); fail if that fails +- Let *(_, _, u, id1..u, pubshare1..u, _) = signers_ctx* +- Let *aggnonce = NonceAgg(pubnonce1..u)*; fail if that fails +- Let *session_ctx = (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m)* +- Run *PartialSigVerifyInternal(psig, idi, pubnoncei, pubsharei, session_ctx)* +- Return success iff no failure occurred before reaching this point. + +Internal Algorithm *PartialSigVerifyInternal(psig, my_id, pubnonce, pubshare, session_ctx)*: + +- Let *(Q, gacc, _, id1..u, pubshare1..u, b, R, e) = GetSessionValues(session_ctx)*; fail if that fails +- Let *s = scalar_from_bytes_checked(psig)*; fail if that fails +- Fail if *pubshare* not in *pubshare1..u* +- Fail if *my_id* not in *id1..u* +- Let *R\*,1 = cpoint(pubnonce[0:33]), R\*,2 = cpoint(pubnonce[33:66])*; fail if either fails +- Let *Re\*' = R\*,1 + b · R\*,2* +- Let effective nonce *Re\* = Re\*'* if *has_even_y(R)*, otherwise let *Re\* = -Re\*'* +- Let *P = cpoint(pubshare)*; fail if that fails +- Let *λ = DeriveInterpolatingValue(id1..u, my_id)*[^lambda-cant-fail] +- Let *g = Scalar(1)* if *has_even_y(Q)*, otherwise let *g = Scalar(-1)* +- Let *g' = g · gacc  (mod ord)* (See [Negation of Pubshare When Partially Verifying](#negation-of-the-pubshare-when-partially-verifying)) +- Fail if *s · G ≠ Re\* + e · λ · g' · P* +- Return success iff no failure occurred before reaching this point. + +[^lambda-cant-fail]: *DeriveInterpolatingValue(id1..u, my_id)* cannot fail when called from *PartialSigVerifyInternal* as *PartialSigVerify* picks *my_id* from *id1..u* + +### Partial Signature Aggregation + +Algorithm *PartialSigAgg(psig1..u, session_ctx)*: + +- Inputs: + - The number *u* of signatures with *t ≤ u ≤ n* + - The list of partial signatures *psig1..u*: *u* 32-byte arrays, each an output of *Sign* + - 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 = scalar_from_bytes_checked(psigi)*; fail if that fails and blame signer at index *i* for invalid partial signature. +- Let *g = Scalar(1)* if *has_even_y(Q)*, otherwise let *g = Scalar(-1)* +- Let *s = s1 + ... + su + e · g · tacc  (mod ord)* +- Return *sig = xbytes(R) || scalar_to_bytes(s)* + +### Test Vectors & Reference Code + +We provide a naive, highly inefficient, and non-constant time [pure Python 3 reference implementation of the threshold public key tweaking, nonce generation, partial signing, and partial signature verification algorithms](./bip-0445/python/frost_ref/). + +Standalone JSON test vectors are also available in the [same directory](./bip-0445/python/vectors/), to facilitate porting the test vectors into other implementations. + +> [!CAUTION] +> The reference implementation is for demonstration purposes only and not to be used in production environments. + +## Remarks on Security and Correctness + +### 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. + +| | 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 share *secshare* 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 *secshare*. + +Second, if there is a unique signer who generates their nonce last (i.e., after receiving the aggregate nonce from all other signers), 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. +It has two optional arguments: *rand*, which can be omitted if randomness is entirely unavailable, and *aggothernonce*, which is omitted by a sole signer (*u = 1*) who has no other signers' nonces to aggregate. +When present, *aggothernonce* 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, or for a sole signer who is the only participant signing, and it makes the signer stateless, similar to the stateless signer described in the [Nonce Generation](#nonce-generation) section. +In FROST, the deterministic nonce must also bind to the the signer subset *id1..u*; otherwise a malicious coordinator can recover the victim's secret share via replayed sessions with varying signer subsets.[^det-signer-set] + +#### Deterministic and Stateless Signing for a Single Signer + +Algorithm *DeterministicSign(secshare, my_id, aggothernonce, signers_ctx, tweak1..v, is_xonly_t1..v, m, rand)*: + +- Inputs: + - The participant secret signing share *secshare*: a 32-byte array, serialized scalar + - The participant identifier *my_id*: an integer with *0 ≤ my_id ≤ n-1* + - The aggregate public nonce *aggothernonce* (see [above](#modifications-to-nonce-generation)): a 66-byte array, output of *NonceAgg* (optional argument)[^det-threshold-one] + - The *signers_ctx*: a [Signers Context](#signers-context) data structure + - The number *v* of tweaks with *0 ≤ v < 2^32* + - The list of tweaks *tweak1..v*: *v* 32-byte arrays, each a serialized scalar + - The list of tweak methods *is_xonly_t1..v*: *v* booleans + - The message *m*: a byte array[^max-msg-len] + - The auxiliary randomness *rand*: a 32-byte array (optional argument) +- If the optional argument *rand* is present: + - Let *secshare' = xor_bytes(secshare, hashBIP0445/aux(rand))* +- Else: + - Let *secshare' = secshare* +- If the optional argument *aggothernonce* is present: + - Let *aggothernonce' = aggothernonce* +- Else: + - Let *aggothernonce' = empty byte string* +- Let *(_, _, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx* +- Let *tweak_ctx0 = TweakCtxInit(thresh_pk)*; fail if that fails +- For *i = 1 .. v*: + - Let *tweak_ctxi = ApplyTweak(tweak_ctxi-1, tweaki, is_xonly_ti)*; fail if that fails +- Let *tweaked_tpk = GetXonlyPubkey(tweak_ctxv)* +- Let *ki = scalar_from_bytes_wrapping(hashBIP0445/deterministic/nonce(secshare' || bytes(4, my_id) || bytes(4, u) || SerializeIds(id1..u) || aggothernonce' || tweaked_tpk || bytes(8, len(m)) || m || bytes(1, i - 1)))* for *i = 1,2* +- Fail if *k1 = Scalar(0)* or *k2 = Scalar(0)*[^negligible-zero-scalar] +- Let *R\*,1 = k1 · G, R\*,2 = k2 · G* +- Let *pubnonce = cbytes(R\*,1) || cbytes(R\*,2)* +- Let *secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2)* +- If the optional argument *aggothernonce* is present: + - Let *aggnonce = NonceAgg((pubnonce, aggothernonce))*; fail if that fails and blame coordinator for invalid *aggothernonce*. +- Else: + - Let *aggnonce = pubnonce* +- Let *session_ctx = (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m)* +- Return (pubnonce, Sign(secnonce, secshare, my_id, session_ctx)) + +[^det-signer-set]: Without binding to the signer subset, a malicious coordinator can replay the same *aggothernonce* to the last signer across three sessions while varying *id1..u*. The victim produces byte-identical secret nonces *(k1, k2)* across sessions, but because the Lagrange interpolating coefficient *λ* and nonce coefficient *b* depend on the signer subset, the three partial signatures form a system of three linear equations in *(k1, k2, d)* where *d* is the victim's secret share — enough to recover *d* by solving the system. This consideration does not apply to MuSig2's *DeterministicSign* because MuSig2 is always *n*-of-*n* and the signer subset is fixed by the protocol. + +[^det-threshold-one]: The threshold *t = 1* is a special case. In a *1-of-n* setup, every participant's *secret share* equals the *threshold secret key* itself, so any single participant can produce a signature alone (*u = 1*). The lone signer calls *DeterministicSign* without the *aggothernonce* argument, which makes the derived nonce fully deterministic, just as in ordinary single-signer [BIP340][bip340] signing. The signer may instead run *Sign*, but that path still draws fresh randomness through *NonceGen*. Simplest of all, because a *1-of-n* group is effectively one secret key held by everyone, the participant can skip the FROST algorithms and sign with the ordinary [BIP340][bip340] signing algorithm[^t-edge-cases]. + +### Tweaking Definition + +Two modes of tweaking the threshold public key are supported. They correspond to the following algorithms: + +Algorithm *ApplyPlainTweak(P, t)*: + +- Inputs: + - *P*: a point + - The tweak *t*: a scalar +- Return *P + t · G* + +Algorithm *ApplyXonlyTweak(P, t)*: + +- Inputs: + - *P*: a point + - The tweak *t*: a scalar +- Return *with_even_y(P) + t · G* + +### Negation of the Secret Share when Signing + +In the following equations, all scalar arithmetic is understood to be modulo the group order, as specified in the [Notation](#notation) section. + +During the signing process, the *[Sign](#signing)* algorithm might have to negate the secret share in order to produce a partial signature for an X-only threshold public key, which may be tweaked *v* times (X-only or plain). + +The following elliptic curve points arise as intermediate steps when creating a signature: + +- The values *Pi* (pubshare), *di'* (secret share), and *Q0* (threshold public key) are produced by a FROST key generation protocol. We have +
+    Pi = di'·G
+    Q0 = λid1·P1 + λid2·P2 + ... + λidu·Pu
+  
+ Here, *λidi* denotes the interpolating value for the *i*-th signing participant in the [Signers Context](#signers-context). + +- *Qi* is the tweaked threshold 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(Q*v*)* is the final result of the threshold public key tweaking operations. It corresponds to the output of *GetXonlyPubkey* applied on the final Tweak Context. + +The signer's goal is to produce a partial signature corresponding to the final result of threshold pubkey derivation 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* equals *Scalar(-1)* 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 TweakCtxInit and ApplyTweak as follows: +
+  tacc0 = 0  (mod ord)
+  tacci = ti + gi-1·tacci-1  (mod ord) for i=1..v
+
+ for which it holds that +
+  gv·taccv = sumi=1..v ti · prodj=i..v gj  (mod ord)
+
+ +*TweakCtxInit* and *ApplyTweak* compute +
+  gacc0 = 1  (mod ord)
+  gacci = gi-1 · gacci-1  (mod ord) for i=1..v
+
+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·(λid1·P1 + ... + λidu·Pu)
+    = gv·gaccv·(λid1·d1'·G + ... + λidu·du'·G)
+    = sumj=1..u(gv·gaccv·λidj·dj')·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, participant *i* multiplies their secret share *di'* with *gv·gaccv* in the [*Sign*](#signing) algorithm. + +#### Negation of the Pubshare when Partially Verifying + +As explained in [Negation Of The Secret Share When Signing](#negation-of-the-secret-share-when-signing) the signer uses a possibly negated secret share +
+  d = gv·gaccv·d'  (mod ord)
+
+when producing a partial signature to ensure that the aggregate signature will correspond to a threshold public key with even Y coordinate. + +The [*PartialSigVerifyInternal*](#partial-signature-verification) algorithm is supposed to check +
+  s·G = Re* + e·λ·d·G
+
+ +The verifier doesn't have access to *d · G* but can construct it using the participant *pubshare* as follows: +
+d·G
+  = gv · gaccv · d' · G
+  = gv · gaccv · cpoint(pubshare)
+
+Note that the threshold 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 coordinator provides *aggnonce = bytes(33,0) || bytes(33,0)*, either the coordinator 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 [FROST3 signing][roast] 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.) + +## Backwards Compatibility + +This document proposes a standard for the FROST threshold signature scheme that is compatible with [BIP340][bip340]. FROST is *not* compatible with ECDSA signatures traditionally used in Bitcoin. + +## Changelog + +- *0.6.0* (2026-06-09): Expand test vectors to cover multiple (t, n) configurations (1-of-3, 2-of-3, 3-of-3, and 3-of-5) instead of the single 2-of-3 setup. +- *0.5.2* (2026-06-03): Make *aggothernonce* an optional argument in *DeterministicSign*, allowing a sole signer in a *1-of-n* setup to sign without aggregating other signers' nonces. +- *0.5.1* (2026-06-02): Add a footnote explaining why the signer identifiers are sorted when deriving the nonce coefficient *b*, which prevents a secret share recovery attack in *DeterministicSign*. +- *0.5.0* (2026-05-21): Introduces the following changes: + - Restructure test vectors to use indexed pools, rename per-case *signer_index* to *my_id*, and rewrite per-case comments. + - Fix mismatches between the BIP spec and the reference implementation. + - Move pubshare deserialization from *DeriveThreshPubkey* into *ValidateSignersCtx*, which now blames the signer on an invalid pubshare. +- *0.4.3* (2026-05-13): Expand test vector coverage for signing and tweaking error cases, and add a test vectors summary document. +- *0.4.2* (2026-04-14): Bind *my_id* and the signer identifiers list into the *DeterministicSign* nonce hash to prevent a secret share recovery attack via replayed signing sessions. +- *0.4.1* (2026-03-03): Assign blame to signer index (of the input list) instead of their identifier value +- *0.4.0* (2026-01-30): Number 445 was assigned to this BIP. +- *0.3.6* (2026-01-28): Add MIT license file for reference code and other auxiliary files. +- *0.3.5* (2026-01-25): Update secp256k1lab to latest version, remove stub file, and fix formatting in the BIP text. +- *0.3.4* (2026-01-01): Add an example file to the reference code. +- *0.3.3* (2025-12-29): Replace the lengthy Introduction section with a concise Motivation section. +- *0.3.2* (2025-12-20): Use 2-of-3 keys in test vectors. +- *0.3.1* (2025-12-17): Update the Algorithms section to use secp256k1lab methods and types. +- *0.3.0* (2025-12-15): Introduces the following changes: + - Introduce *SignersContext* and define key material compatibility with *ValidateSignersCtx*. + - Rewrite the signing protocol assuming a coordinator, add sequence diagram, and warn key generation protocols to output Taproot-safe *threshold public key*. + - Remove *GetSessionInterpolatingValue*, *SessionHasSignerPubshare*, *ValidatePubshares*, and *ValidateThreshPubkey* algorithms + - Revert back to initializing *TweakCtxInit* with threshold public key instead of *pubshares* +- *0.2.3* (2025-11-25): Sync terminologies with the ChillDKG BIP. +- *0.2.2* (2025-11-11): Remove key generation test vectors as key generation is out of scope for this specification. +- *0.2.1* (2025-11-10): Vendor secp256k1lab library to provide *Scalar* and *GE* primitives. Restructure reference implementation into a Python package layout. +- *0.2.0* (2025-04-11): Includes minor fixes and the following major changes: + - Initialize *TweakCtxInit* using individual *pubshares* instead of the threshold public key. + - Add Python script to automate generation of test vectors. + - Represent participant identifiers as 4-byte integers in the range *0..n - 1* (inclusive). +- *0.1.0* (2024-07-31): Publication of draft BIP on the bitcoin-dev mailing list + +## Acknowledgments + +We thank Jonas Nick, Tim Ruffing, Jesse Posner, Sebastian Falbesoner, Chris Stewart, Illia Melnyk, Mariia Zhvanko, and Fadi Barbàra for their comments and contributions to this document. + + +[bip32]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +[bip340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +[bip341]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki +[bip342]: https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki +[bip327]: https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki +[musig]: https://eprint.iacr.org/2018/068 +[frost1]: https://eprint.iacr.org/2020/852 +[frost2]: https://eprint.iacr.org/2021/1375 +[stronger-security-frost]: https://eprint.iacr.org/2022/833 +[olaf]: https://eprint.iacr.org/2023/899 +[roast]: https://eprint.iacr.org/2022/550 +[rerandomized-frost]: https://eprint.iacr.org/2024/436 + +[rfc9591]: https://www.rfc-editor.org/rfc/rfc9591.html diff --git a/bip-0445/.markdownlint.json b/bip-0445/.markdownlint.json new file mode 100644 index 0000000000..2c21fe5555 --- /dev/null +++ b/bip-0445/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "first-line-heading": false, + "line-length": false, + "no-inline-html": { "allowed_elements": ["sub", "sup", "pre"] }, + "no-duplicate-heading": { "siblings_only": true }, + "emphasis-style": { "style": "asterisk" }, + "strong-style": { "style": "asterisk" }, + "ul-style": { "style": "dash" }, + "fenced-code-language": false +} \ No newline at end of file diff --git a/bip-0445/COPYING b/bip-0445/COPYING new file mode 100644 index 0000000000..e4998218e3 --- /dev/null +++ b/bip-0445/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024-2026 Sivaram Dhakshinamoorthy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/bip-0445/all.sh b/bip-0445/all.sh new file mode 100755 index 0000000000..ae3bd43e0a --- /dev/null +++ b/bip-0445/all.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -euo pipefail + +cd "$(dirname "$0")" || exit 1 + +check_availability() { + command -v "$1" > /dev/null 2>&1 || { + echo >&2 "$1 is required but it's not installed. Aborting."; + exit 1; + } +} + +check_availability markdownlint-cli2 +check_availability typos + +markdownlint-cli2 ../bip-0445.md --config ./.markdownlint.json || true +typos ../bip-0445.md . || true + +cd python || exit 1 +./tests.sh +./example.py \ No newline at end of file diff --git a/bip-0445/docs/frost-signing-flow.png b/bip-0445/docs/frost-signing-flow.png new file mode 100644 index 0000000000..f0fb210930 Binary files /dev/null and b/bip-0445/docs/frost-signing-flow.png differ diff --git a/bip-0445/python/.ruff.toml b/bip-0445/python/.ruff.toml new file mode 100644 index 0000000000..d8cd9cc98f --- /dev/null +++ b/bip-0445/python/.ruff.toml @@ -0,0 +1,3 @@ +[format] +# Exclude vendored package. +exclude = ["secp256k1lab/*"] \ No newline at end of file diff --git a/bip-0445/python/example.py b/bip-0445/python/example.py new file mode 100755 index 0000000000..f6188ddef4 --- /dev/null +++ b/bip-0445/python/example.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +"""Example of a full FROST signing session.""" + +from typing import List, Tuple +import asyncio +import argparse +import secrets + +# Import frost_ref first to set up secp256k1lab path +from frost_ref import ( + nonce_gen, + nonce_agg, + sign, + partial_sig_agg, + partial_sig_verify, + SignersContext, + SessionContext, + PlainPk, +) +from frost_ref.signing import ( + thresh_pubkey_and_tweak, + get_xonly_pk, + partial_sig_verify_internal, +) + +from secp256k1lab.bip340 import schnorr_verify +from trusted_dealer import trusted_dealer_keygen + + +# +# Network mocks to simulate full FROST signing sessions +# + + +class CoordinatorChannels: + def __init__(self, n): + self.n = n + self.queues = [asyncio.Queue() for _ in range(n)] + self.participant_queues = None + + def set_participant_queues(self, participant_queues): + self.participant_queues = participant_queues + + def send_to(self, i, m): + assert self.participant_queues is not None + self.participant_queues[i].put_nowait(m) + + def send_all(self, m): + assert self.participant_queues is not None + for i in range(self.n): + self.participant_queues[i].put_nowait(m) + + async def receive_from(self, i: int) -> bytes: + return await self.queues[i].get() + + +class ParticipantChannel: + def __init__(self, coord_queue): + self.queue = asyncio.Queue() + self.coord_queue = coord_queue + + def send(self, m): + self.coord_queue.put_nowait(m) + + async def receive(self): + return await self.queue.get() + + +# +# Helper functions +# + + +def generate_frost_keys( + n: int, t: int +) -> Tuple[PlainPk, List[int], List[bytes], List[PlainPk]]: + """Generate t-of-n FROST keys using trusted dealer. + + Returns: + thresh_pk: Threshold public key (33-byte compressed) + ids: List of signer IDs (0-indexed: 0, 1, ..., n-1) + secshares: List of secret shares (32-byte scalars) + pubshares: List of public shares (33-byte compressed) + """ + thresh_pk, secshares, pubshares = trusted_dealer_keygen( + secrets.token_bytes(32), n, t + ) + + assert len(secshares) == n + ids = list(range(len(secshares))) # ids are 0..n-1 + + return thresh_pk, ids, secshares, pubshares + + +# +# Protocol parties +# + + +async def participant( + chan: ParticipantChannel, + secshare: bytes, + pubshare: PlainPk, + my_id: int, + signers_ctx: SignersContext, + tweaks: List[bytes], + is_xonly: List[bool], + msg: bytes, +) -> Tuple[bytes, bytes]: + """ + Participant in FROST signing protocol. + + Returns: + (psig, final_sig): Partial signature and final BIP340 signature + """ + # Get tweaked threshold pubkey + tweak_ctx = thresh_pubkey_and_tweak(signers_ctx.thresh_pk, tweaks, is_xonly) + tweaked_thresh_pk = get_xonly_pk(tweak_ctx) + + # Round 1: Nonce generation + secnonce, pubnonce = nonce_gen(secshare, pubshare, tweaked_thresh_pk, msg, None) + chan.send(pubnonce) + aggnonce = await chan.receive() + + # Round 2: Signing + session_ctx = SessionContext(aggnonce, signers_ctx, tweaks, is_xonly, msg) + psig = sign(secnonce, secshare, my_id, session_ctx) + assert partial_sig_verify_internal(psig, my_id, pubnonce, pubshare, session_ctx), ( + "Partial signature verification failed" + ) + chan.send(psig) + + # Receive final signature + final_sig = await chan.receive() + return (psig, final_sig) + + +async def coordinator( + chans: CoordinatorChannels, + signers_ctx: SignersContext, + tweaks: List[bytes], + is_xonly: List[bool], + msg: bytes, +) -> bytes: + """ + Coordinator in FROST signing protocol. + + Returns: + final_sig: Final BIP340 signature (64 bytes) + """ + # Determine the signers + signer_ids = signers_ctx.ids + num_signers = len(signer_ids) + + # Round 1: Collect pubnonces + pubnonces = [] + for i in range(num_signers): + pubnonce = await chans.receive_from(i) + pubnonces.append(pubnonce) + + # Aggregate nonces + aggnonce = nonce_agg(pubnonces) + chans.send_all(aggnonce) + + # Round 2: Collect partial signatures + session_ctx = SessionContext(aggnonce, signers_ctx, tweaks, is_xonly, msg) + psigs = [] + for i in range(num_signers): + psig = await chans.receive_from(i) + assert partial_sig_verify( + psig, pubnonces, signers_ctx, tweaks, is_xonly, msg, i + ), f"Partial signature verification failed for singer {i}" + psigs.append(psig) + + # Aggregate partial signatures + final_sig = partial_sig_agg(psigs, session_ctx) + chans.send_all(final_sig) + + return final_sig + + +# +# Signing Session +# + + +def simulate_frost_signing( + secshares: List[bytes], + signers_ctx: SignersContext, + msg: bytes, + tweaks: List[bytes], + is_xonly: List[bool], +) -> Tuple[bytes, List[bytes]]: + """Run a full FROST signing session. + + Returns: + (final_sig, psigs): Final signature and list of partial signatures + """ + # Extract signer set from signers_ctx + signer_ids = signers_ctx.ids + pubshares = signers_ctx.pubshares + num_signers = len(signer_ids) + + async def session(): + # Set up channels + coord_chans = CoordinatorChannels(num_signers) + participant_chans = [ + ParticipantChannel(coord_chans.queues[i]) for i in range(num_signers) + ] + coord_chans.set_participant_queues( + [participant_chans[i].queue for i in range(num_signers)] + ) + + # Create coroutines + coroutines = [coordinator(coord_chans, signers_ctx, tweaks, is_xonly, msg)] + [ + participant( + participant_chans[i], + secshares[i], + pubshares[i], + signer_ids[i], + signers_ctx, + tweaks, + is_xonly, + msg, + ) + for i in range(num_signers) + ] + + return await asyncio.gather(*coroutines) + + results = asyncio.run(session()) + final_sig = results[0] + psigs = [r[0] for r in results[1:]] # Extract psigs from participant results + return final_sig, psigs + + +def main(): + parser = argparse.ArgumentParser(description="FROST Signing example") + parser.add_argument( + "t", nargs="?", type=int, default=2, help="Threshold [default=2]" + ) + parser.add_argument( + "n", nargs="?", type=int, default=3, help="Participants [default=3]" + ) + args = parser.parse_args() + + t, n = args.t, args.n + assert 2 <= t <= n, "Threshold t must satisfy 2 <= t <= n" + + print("====== FROST Signing example session ======") + print(f"Using n = {n} participants and a threshold of t = {t}.") + print() + + # 1. Generate FROST keys + thresh_pk, all_ids, all_secshares, all_pubshares = generate_frost_keys(n, t) + + print("=== Key Configuration ===") + print(f"Threshold public key: {thresh_pk.hex()}") + print() + print("=== Public shares ===") + for i, pubshare in enumerate(all_pubshares): + print(f" Participant {all_ids[i]}: {pubshare.hex()}") + print() + + # 2. Select first t signers + signer_indices = list(range(t)) + signer_ids = [all_ids[i] for i in signer_indices] + signer_secshares = [all_secshares[i] for i in signer_indices] + signer_pubshares = [all_pubshares[i] for i in signer_indices] + + # 3. Initialize the signers context + print("=== Signing Set ===") + print(f"Selected signers: {signer_ids}") + print() + signers_ctx = SignersContext(n, t, signer_ids, signer_pubshares, thresh_pk) + + # 4. Create message and tweaks + msg = secrets.token_bytes(32) + + # Apply both plain (BIP32-style) and xonly (BIP341-style) tweaks + tweaks = [secrets.token_bytes(32), secrets.token_bytes(32)] + is_xonly = [False, True] # First: plain (BIP32), Second: xonly (BIP341) + + tweak_ctx = thresh_pubkey_and_tweak(thresh_pk, tweaks, is_xonly) + tweaked_thresh_pk = get_xonly_pk(tweak_ctx) + + print("=== Message and Tweaks ===") + print(f"Message: {msg.hex()}") + print(f"Tweak 1 (plain/BIP32): {tweaks[0].hex()}") + print(f"Tweak 2 (xonly/BIP341): {tweaks[1].hex()}") + print(f"Tweaked threshold public key: {tweaked_thresh_pk.hex()}") + print() + + # 5. Run signing protocol + final_sig, psigs = simulate_frost_signing( + signer_secshares, + signers_ctx, + msg, + tweaks, + is_xonly, + ) + + print("=== Participants Partial Signatures ===") + for i, psig in enumerate(psigs): + print(f" Participant {signer_ids[i]}: {psig.hex()}") + print() + + print("=== Final Signature ===") + print(f"BIP340 signature: {final_sig.hex()}") + print() + + # 6. Verify signature + assert schnorr_verify(msg, tweaked_thresh_pk, final_sig) + print("=== Verification ===") + print("Signature verified successfully!") + + +if __name__ == "__main__": + main() diff --git a/bip-0445/python/frost_ref/__init__.py b/bip-0445/python/frost_ref/__init__.py new file mode 100644 index 0000000000..d6a00bb380 --- /dev/null +++ b/bip-0445/python/frost_ref/__init__.py @@ -0,0 +1,43 @@ +from pathlib import Path +import sys + +# Add the vendored copy of secp256k1lab to path. +sys.path.append(str(Path(__file__).parent / "../secp256k1lab/src")) + +from .signing import ( + # Functions + validate_signers_ctx, + nonce_gen, + nonce_agg, + sign, + deterministic_sign, + partial_sig_verify, + partial_sig_agg, + # Exceptions + InvalidContributionError, + # Types + PlainPk, + XonlyPk, + SignersContext, + TweakContext, + SessionContext, +) + +__all__ = [ + # Functions + "validate_signers_ctx", + "nonce_gen", + "nonce_agg", + "sign", + "deterministic_sign", + "partial_sig_verify", + "partial_sig_agg", + # Exceptions + "InvalidContributionError", + # Types + "PlainPk", + "XonlyPk", + "SignersContext", + "TweakContext", + "SessionContext", +] diff --git a/bip-0445/python/frost_ref/signing.py b/bip-0445/python/frost_ref/signing.py new file mode 100644 index 0000000000..88fadd7b8f --- /dev/null +++ b/bip-0445/python/frost_ref/signing.py @@ -0,0 +1,505 @@ +# BIP FROST Signing reference implementation +# +# It's worth noting that many functions, types, and exceptions were directly +# copied or modified from the MuSig2 (BIP 327) reference code, found at: +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py +# +# WARNING: This implementation is for demonstration purposes only and _not_ to +# be used in production environments. The code is vulnerable to timing attacks, +# for example. + +from typing import List, Optional, Tuple, NewType, NamedTuple, Literal +import secrets + +from secp256k1lab.secp256k1 import G, GE, Scalar +from secp256k1lab.util import tagged_hash, xor_bytes + +PlainPk = NewType("PlainPk", bytes) +XonlyPk = NewType("XonlyPk", bytes) +ContribKind = Literal["aggothernonce", "aggnonce", "psig", "pubnonce"] + +# Tagged hash domain-separation tags. The challenge tag is inherited from +# BIP340 for signature compatibility; the rest are specific to this BIP. +FROST_TAG_AUX = "BIP0445/aux" +FROST_TAG_NONCE = "BIP0445/nonce" +FROST_TAG_NONCECOEF = "BIP0445/noncecoef" +FROST_TAG_DETERMINISTIC_NONCE = "BIP0445/deterministic/nonce" +BIP340_TAG_CHALLENGE = "BIP0340/challenge" + +# There are two types of exceptions that can be raised by this implementation: +# - ValueError for indicating that an input doesn't conform to some function +# precondition (e.g. an input array is the wrong length, a serialized +# representation doesn't have the correct format). +# - InvalidContributionError for indicating that a signer (or the +# coordinator) is misbehaving in the protocol. +# +# Assertions are used to (1) satisfy the type-checking system, and (2) check for +# inconvenient events that can't happen except with negligible probability (e.g. +# output of a hash function is 0) and can't be manually triggered by any +# signer. + + +# This exception is raised if a party (signer or nonce coordinator) sends invalid +# values. Actual implementations should not crash when receiving invalid +# contributions. Instead, they should hold the offending party accountable. +class InvalidContributionError(Exception): + def __init__(self, signer_index: Optional[int], contrib: ContribKind) -> None: + # index of the signer who sent the invalid value, or None for coordinator + self.signer_index = signer_index + self.contrib = contrib + + +def has_duplicates(lst: List[int]) -> bool: + return len(set(lst)) != len(lst) + + +def derive_interpolating_value(ids: List[int], my_id: int) -> Scalar: + assert my_id in ids + assert 0 <= my_id < 2**32 + assert not has_duplicates(ids) + num = Scalar(1) + deno = Scalar(1) + for curr_id in ids: + if curr_id == my_id: + continue + num *= Scalar(curr_id + 1) + deno *= Scalar(curr_id - my_id) + return num / deno + + +def derive_thresh_pubkey(ids: List[int], pubshares: List[GE]) -> PlainPk: + assert len(ids) == len(pubshares) + Q = GE() + for my_id, X_i in zip(ids, pubshares): + lam_i = derive_interpolating_value(ids, my_id) + Q += lam_i * X_i + # Q is not the point at infinity except with negligible probability. + assert not Q.infinity + return PlainPk(Q.to_bytes_compressed()) + + +class SignersContext(NamedTuple): + n: int + t: int + ids: List[int] + pubshares: List[PlainPk] + thresh_pk: PlainPk + + +def validate_signers_ctx(signers_ctx: SignersContext) -> None: + n, t, ids, pubshares, thresh_pk = signers_ctx + if t < 1 or t > n: + raise ValueError("The threshold must be 1 <= t <= n.") + if not t <= len(ids) <= n: + raise ValueError("The number of signers must be between t and n.") + if len(pubshares) != len(ids): + raise ValueError("The pubshares and ids arrays must have the same length.") + pubshare_points = [] + for idx, (i, pubshare) in enumerate(zip(ids, pubshares)): + if not 0 <= i <= n - 1: + raise ValueError( + f"The participant identifier at index {idx} is out of range." + ) + try: + P = GE.from_bytes_compressed(pubshare) + except ValueError: + # A malformed pubshare is invalid pre-protocol input, not a protocol + # contribution: the signers context is agreed upon before signing + # begins, so we signal it with ValueError rather than blaming a + # signer via InvalidContributionError. + raise ValueError(f"Invalid pubshare at index {idx}.") + pubshare_points.append(P) + if has_duplicates(ids): + raise ValueError("The participant identifier list contains duplicate elements.") + if derive_thresh_pubkey(ids, pubshare_points) != thresh_pk: + raise ValueError("The provided key material is incorrect.") + + +class TweakContext(NamedTuple): + Q: GE + gacc: Scalar + tacc: Scalar + + +def get_xonly_pk(tweak_ctx: TweakContext) -> XonlyPk: + Q, _, _ = tweak_ctx + return XonlyPk(Q.to_bytes_xonly()) + + +def get_plain_pk(tweak_ctx: TweakContext) -> PlainPk: + Q, _, _ = tweak_ctx + return PlainPk(Q.to_bytes_compressed()) + + +def tweak_ctx_init(thresh_pk: PlainPk) -> TweakContext: + Q = GE.from_bytes_compressed(thresh_pk) + gacc = Scalar(1) + tacc = Scalar(0) + return TweakContext(Q, gacc, tacc) + + +def apply_tweak(tweak_ctx: TweakContext, tweak: bytes, is_xonly: bool) -> TweakContext: + if len(tweak) != 32: + raise ValueError("The tweak must be a 32-byte array.") + Q, gacc, tacc = tweak_ctx + if is_xonly and not Q.has_even_y(): + g = Scalar(-1) + else: + g = Scalar(1) + try: + twk = Scalar.from_bytes_checked(tweak) + except ValueError: + raise ValueError("The tweak value is out of range.") + Q_ = g * Q + twk * G + if Q_.infinity: + raise ValueError("The result of tweaking cannot be infinity.") + gacc_ = g * gacc + tacc_ = twk + g * tacc + return TweakContext(Q_, gacc_, tacc_) + + +def nonce_hash( + rand: bytes, + pubshare: PlainPk, + thresh_pk: XonlyPk, + i: int, + msg_prefixed: bytes, + extra_in: bytes, +) -> bytes: + buf = b"" + buf += rand + buf += len(pubshare).to_bytes(1, "big") + buf += pubshare + buf += len(thresh_pk).to_bytes(1, "big") + buf += thresh_pk + buf += msg_prefixed + buf += len(extra_in).to_bytes(4, "big") + buf += extra_in + buf += i.to_bytes(1, "big") + return tagged_hash(FROST_TAG_NONCE, buf) + + +def nonce_gen_internal( + rand_: bytes, + secshare: Optional[bytes], + pubshare: Optional[PlainPk], + thresh_pk: Optional[XonlyPk], + msg: Optional[bytes], + extra_in: Optional[bytes], +) -> Tuple[bytearray, bytes]: + if secshare is not None: + rand = xor_bytes(secshare, tagged_hash(FROST_TAG_AUX, rand_)) + else: + rand = rand_ + if pubshare is None: + pubshare = PlainPk(b"") + if thresh_pk is None: + thresh_pk = XonlyPk(b"") + if msg is None: + msg_prefixed = b"\x00" + else: + msg_prefixed = b"\x01" + msg_prefixed += len(msg).to_bytes(8, "big") + msg_prefixed += msg + if extra_in is None: + extra_in = b"" + k_1 = Scalar.from_bytes_wrapping( + nonce_hash(rand, pubshare, thresh_pk, 0, msg_prefixed, extra_in) + ) + k_2 = Scalar.from_bytes_wrapping( + nonce_hash(rand, pubshare, thresh_pk, 1, msg_prefixed, extra_in) + ) + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + R1_partial = k_1 * G + R2_partial = k_2 * G + assert not R1_partial.infinity + assert not R2_partial.infinity + pubnonce = R1_partial.to_bytes_compressed() + R2_partial.to_bytes_compressed() + # use mutable `bytearray` since secnonce need to be replaced with zeros during signing. + secnonce = bytearray(k_1.to_bytes() + k_2.to_bytes()) + return secnonce, pubnonce + + +def nonce_gen( + secshare: Optional[bytes], + pubshare: Optional[PlainPk], + thresh_pk: Optional[XonlyPk], + msg: Optional[bytes], + extra_in: Optional[bytes], +) -> Tuple[bytearray, bytes]: + if secshare is not None and len(secshare) != 32: + raise ValueError("The optional byte array secshare must have length 32.") + if pubshare is not None and len(pubshare) != 33: + raise ValueError("The optional byte array pubshare must have length 33.") + if thresh_pk is not None and len(thresh_pk) != 32: + raise ValueError("The optional byte array thresh_pk must have length 32.") + rand_ = secrets.token_bytes(32) + return nonce_gen_internal(rand_, secshare, pubshare, thresh_pk, msg, extra_in) + + +def nonce_agg(pubnonces: List[bytes]) -> bytes: + aggnonce = b"" + for j in (1, 2): + R_j = GE() + for idx, pubnonce in enumerate(pubnonces): + try: + R_ij = GE.from_bytes_compressed(pubnonce[(j - 1) * 33 : j * 33]) + except ValueError: + raise InvalidContributionError(idx, "pubnonce") + R_j += R_ij + aggnonce += R_j.to_bytes_compressed_with_infinity() + return aggnonce + + +class SessionContext(NamedTuple): + aggnonce: bytes + signers_ctx: SignersContext + tweaks: List[bytes] + is_xonly: List[bool] + msg: bytes + + +def thresh_pubkey_and_tweak( + thresh_pk: PlainPk, tweaks: List[bytes], is_xonly: List[bool] +) -> TweakContext: + if len(tweaks) != len(is_xonly): + raise ValueError("The tweaks and is_xonly arrays must have the same length.") + tweak_ctx = tweak_ctx_init(thresh_pk) + v = len(tweaks) + for i in range(v): + tweak_ctx = apply_tweak(tweak_ctx, tweaks[i], is_xonly[i]) + return tweak_ctx + + +def get_session_values( + session_ctx: SessionContext, +) -> Tuple[GE, Scalar, Scalar, List[int], List[PlainPk], Scalar, GE, Scalar]: + (aggnonce, signers_ctx, tweaks, is_xonly, msg) = session_ctx + validate_signers_ctx(signers_ctx) + _, _, ids, pubshares, thresh_pk = signers_ctx + Q, gacc, tacc = thresh_pubkey_and_tweak(thresh_pk, tweaks, is_xonly) + # sort the ids before serializing because ROAST paper considers them as a set + ser_ids = serialize_ids(ids) + b = Scalar.from_bytes_wrapping( + tagged_hash(FROST_TAG_NONCECOEF, ser_ids + aggnonce + Q.to_bytes_xonly() + msg) + ) + assert b != 0 + try: + R1 = GE.from_bytes_compressed_with_infinity(aggnonce[0:33]) + R2 = GE.from_bytes_compressed_with_infinity(aggnonce[33:66]) + except ValueError: + # coordinator sent invalid aggnonce + raise InvalidContributionError(None, "aggnonce") + R_ = R1 + b * R2 + R = R_ if not R_.infinity else G + assert not R.infinity + e = Scalar.from_bytes_wrapping( + tagged_hash(BIP340_TAG_CHALLENGE, R.to_bytes_xonly() + Q.to_bytes_xonly() + msg) + ) + assert e != 0 + return (Q, gacc, tacc, ids, pubshares, b, R, e) + + +def serialize_ids(ids: List[int]) -> bytes: + sorted_ids = sorted(ids) + ser_ids = b"".join(i.to_bytes(4, byteorder="big", signed=False) for i in sorted_ids) + return ser_ids + + +def sign( + secnonce: bytearray, secshare: bytes, my_id: int, session_ctx: SessionContext +) -> bytes: + (Q, gacc, _, ids, pubshares, b, R, e) = get_session_values( + session_ctx + ) # internally validates signers_ctx + try: + k_1_ = Scalar.from_bytes_nonzero_checked(bytes(secnonce[0:32])) + except ValueError: + raise ValueError("first secnonce value is out of range.") + try: + k_2_ = Scalar.from_bytes_nonzero_checked(bytes(secnonce[32:64])) + except ValueError: + raise ValueError("second secnonce value is out of range.") + # Overwrite the secnonce argument with zeros, so the subsequent calls of + # sign with the same secnonce raise a ValueError. + secnonce[:] = bytearray(b"\x00" * 64) + k_1 = k_1_ if R.has_even_y() else -k_1_ + k_2 = k_2_ if R.has_even_y() else -k_2_ + try: + d_ = Scalar.from_bytes_nonzero_checked(secshare) + except ValueError: + raise ValueError("The signer's secret share value is out of range.") + P = d_ * G + assert not P.infinity + my_pubshare = P.to_bytes_compressed() + if my_pubshare not in pubshares: + raise ValueError( + "The signer's pubshare must be included in the list of pubshares." + ) + if my_id not in ids: + raise ValueError( + "The signer's id must be present in the participant identifier list." + ) + a = derive_interpolating_value(ids, my_id) + g = Scalar(1) if Q.has_even_y() else Scalar(-1) + d = g * gacc * d_ + s = k_1 + b * k_2 + e * a * d + psig = s.to_bytes() + R1_partial = k_1_ * G + R2_partial = k_2_ * G + assert not R1_partial.infinity + assert not R2_partial.infinity + pubnonce = R1_partial.to_bytes_compressed() + R2_partial.to_bytes_compressed() + # Optional correctness check. The result of signing should pass signature verification. + assert partial_sig_verify_internal(psig, my_id, pubnonce, my_pubshare, session_ctx) + return psig + + +def det_nonce_hash( + secshare_: bytes, + my_id: int, + ids: List[int], + aggothernonce: bytes, + tweaked_tpk: bytes, + msg: bytes, + i: int, +) -> bytes: + buf = b"" + buf += secshare_ + buf += my_id.to_bytes(4, "big") + buf += len(ids).to_bytes(4, "big") + buf += serialize_ids(ids) + buf += aggothernonce + buf += tweaked_tpk + buf += len(msg).to_bytes(8, "big") + buf += msg + buf += i.to_bytes(1, "big") + return tagged_hash(FROST_TAG_DETERMINISTIC_NONCE, buf) + + +def deterministic_sign( + secshare: bytes, + my_id: int, + aggothernonce: Optional[bytes], + signers_ctx: SignersContext, + tweaks: List[bytes], + is_xonly: List[bool], + msg: bytes, + rand: Optional[bytes], +) -> Tuple[bytes, bytes]: + if rand is not None: + secshare_ = xor_bytes(secshare, tagged_hash(FROST_TAG_AUX, rand)) + else: + secshare_ = secshare + validate_signers_ctx(signers_ctx) + _, _, ids, _, thresh_pk = signers_ctx + tweaked_tpk = get_xonly_pk(thresh_pubkey_and_tweak(thresh_pk, tweaks, is_xonly)) + + # A sole signer (u = 1) has no other nonces to aggregate, so aggothernonce is + # omitted. Bind the empty byte string into the nonce hash and use the signer's + # own pubnonce as the aggregate nonce below. + if aggothernonce is None: + aggothernonce_ = b"" + else: + aggothernonce_ = aggothernonce + + k_1 = Scalar.from_bytes_wrapping( + det_nonce_hash(secshare_, my_id, ids, aggothernonce_, tweaked_tpk, msg, 0) + ) + k_2 = Scalar.from_bytes_wrapping( + det_nonce_hash(secshare_, my_id, ids, aggothernonce_, tweaked_tpk, msg, 1) + ) + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + + R1_partial = k_1 * G + R2_partial = k_2 * G + assert not R1_partial.infinity + assert not R2_partial.infinity + pubnonce = R1_partial.to_bytes_compressed() + R2_partial.to_bytes_compressed() + secnonce = bytearray(k_1.to_bytes() + k_2.to_bytes()) + if aggothernonce is None: + aggnonce = pubnonce + else: + try: + aggnonce = nonce_agg([pubnonce, aggothernonce]) + except InvalidContributionError: + # pubnonce is always valid, so any failure is due to aggothernonce. + raise InvalidContributionError(None, "aggothernonce") + session_ctx = SessionContext(aggnonce, signers_ctx, tweaks, is_xonly, msg) + psig = sign(secnonce, secshare, my_id, session_ctx) + return (pubnonce, psig) + + +def partial_sig_verify( + psig: bytes, + pubnonces: List[bytes], + signers_ctx: SignersContext, + tweaks: List[bytes], + is_xonly: List[bool], + msg: bytes, + i: int, +) -> bool: + validate_signers_ctx(signers_ctx) + _, _, ids, pubshares, _ = signers_ctx + if len(pubnonces) != len(ids): + raise ValueError("The pubnonces and ids arrays must have the same length.") + if len(tweaks) != len(is_xonly): + raise ValueError("The tweaks and is_xonly arrays must have the same length.") + aggnonce = nonce_agg(pubnonces) + session_ctx = SessionContext(aggnonce, signers_ctx, tweaks, is_xonly, msg) + return partial_sig_verify_internal( + psig, ids[i], pubnonces[i], pubshares[i], session_ctx + ) + + +def partial_sig_verify_internal( + psig: bytes, + my_id: int, + pubnonce: bytes, + pubshare: bytes, + session_ctx: SessionContext, +) -> bool: + (Q, gacc, _, ids, pubshares, b, R, e) = get_session_values(session_ctx) + try: + s = Scalar.from_bytes_checked(psig) + except ValueError: + return False + if pubshare not in pubshares: + return False + if my_id not in ids: + return False + try: + R1_partial = GE.from_bytes_compressed(pubnonce[0:33]) + R2_partial = GE.from_bytes_compressed(pubnonce[33:66]) + except ValueError: + return False + Re_s_ = R1_partial + b * R2_partial + Re_s = Re_s_ if R.has_even_y() else -Re_s_ + try: + P = GE.from_bytes_compressed(pubshare) + except ValueError: + return False + a = derive_interpolating_value(ids, my_id) + g = Scalar(1) if Q.has_even_y() else Scalar(-1) + g_ = g * gacc + return s * G == Re_s + (e * a * g_) * P + + +def partial_sig_agg(psigs: List[bytes], session_ctx: SessionContext) -> bytes: + (Q, _, tacc, ids, _, _, R, e) = get_session_values(session_ctx) + if len(psigs) != len(ids): # get_session_values asserts len(pubshares) == len(ids) + raise ValueError("The psigs and ids arrays must have the same length.") + s = Scalar(0) + for idx, psig in enumerate(psigs): + try: + s_i = Scalar.from_bytes_checked(psig) + except ValueError: + raise InvalidContributionError(idx, "psig") + s += s_i + g = Scalar(1) if Q.has_even_y() else Scalar(-1) + s += e * g * tacc + return R.to_bytes_xonly() + s.to_bytes() diff --git a/bip-0445/python/gen_vectors.py b/bip-0445/python/gen_vectors.py new file mode 100755 index 0000000000..851577e420 --- /dev/null +++ b/bip-0445/python/gen_vectors.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import glob +import os +import sys + +from generators.det_sign import generate_det_sign_vectors +from generators.nonce import ( + generate_nonce_agg_vectors, + generate_nonce_gen_vectors, +) +from generators.sig_agg import generate_sig_agg_vectors +from generators.sign_verify import generate_sign_verify_vectors +from generators.tweak import generate_tweak_vectors + + +def create_vectors_directory(): + os.makedirs("vectors", exist_ok=True) + for f in glob.glob("vectors/*.json"): + os.remove(f) + + +def run_gen_vectors(test_name, test_func): + max_len = 30 + test_name = test_name.ljust(max_len, ".") + print(f"Running {test_name}...", end="", flush=True) + try: + test_func() + print("Done!") + except Exception as e: + print(f"Failed :'(\nError: {e}") + + +def main(): + create_vectors_directory() + + run_gen_vectors("generate_nonce_gen_vectors", generate_nonce_gen_vectors) + run_gen_vectors("generate_nonce_agg_vectors", generate_nonce_agg_vectors) + run_gen_vectors("generate_sign_verify_vectors", generate_sign_verify_vectors) + run_gen_vectors("generate_tweak_vectors", generate_tweak_vectors) + run_gen_vectors("generate_det_sign_vectors", generate_det_sign_vectors) + run_gen_vectors("generate_sig_agg_vectors", generate_sig_agg_vectors) + print("Test vectors generated successfully") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bip-0445/python/generators/__init__.py b/bip-0445/python/generators/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0445/python/generators/common.py b/bip-0445/python/generators/common.py new file mode 100644 index 0000000000..e95508887b --- /dev/null +++ b/bip-0445/python/generators/common.py @@ -0,0 +1,299 @@ +import json +import os +import re +import secrets +from collections import namedtuple +from typing import Dict, List, Sequence, Union + +from frost_ref.signing import ( + PlainPk, + XonlyPk, + derive_interpolating_value, + nonce_agg, + nonce_gen_internal, +) +from secp256k1lab.secp256k1 import G, GE, Scalar +from secp256k1lab.keys import pubkey_gen_plain +from trusted_dealer import trusted_dealer_keygen + + +def bytes_to_hex(data: bytes) -> str: + return data.hex().upper() + + +def bytes_list_to_hex(lst: Sequence[bytes]) -> List[str]: + return [l_i.hex().upper() for l_i in lst] + + +def hex_list_to_bytes(lst: List[str]) -> List[bytes]: + return [bytes.fromhex(l_i) for l_i in lst] + + +ErrorInfo = Dict[str, Union[int, str, None, "ErrorInfo"]] + + +def exception_asdict(e: Exception) -> dict: + error_info: ErrorInfo = {"type": e.__class__.__name__} + + for key, value in e.__dict__.items(): + if isinstance(value, (str, int, type(None))): + error_info[key] = value + elif isinstance(value, bytes): + error_info[key] = bytes_to_hex(value) + else: + raise NotImplementedError( + f"Conversion for type {type(value).__name__} is not implemented" + ) + + # If the last argument is not found in the instance’s attributes and + # is a string, treat it as an extra message. + if e.args and isinstance(e.args[-1], str) and e.args[-1] not in e.__dict__.values(): + error_info.setdefault("message", e.args[-1]) + return error_info + + +def expect_exception(try_fn, expected_exception): + try: + try_fn() + except expected_exception as e: + return exception_asdict(e) + except Exception as e: + raise AssertionError(f"Wrong exception raised: {type(e).__name__}") + else: + raise AssertionError("Expected exception") + + +COMMON_RAND = bytes.fromhex( + "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F" +) + +COMMON_MSGS = [ + bytes.fromhex( + "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + ), # 32-byte message + bytes.fromhex(""), # Empty message + bytes.fromhex( + "2626262626262626262626262626262626262626262626262626262626262626262626262626" + ), # 38-byte message +] + +COMMON_TWEAKS = hex_list_to_bytes( + [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + ] +) + +# secp256k1 group order n: the single out-of-range boundary shared by the tweak and +# partial-signature generators +GROUP_ORDER = GE.ORDER.to_bytes(32, "big") +OUT_OF_RANGE_TWEAK = GROUP_ORDER +INVALID_PUBSHARE = bytes.fromhex( + "020000000000000000000000000000000000000000000000000000000000000007" +) + +# Public nonce whose first half is an off-curve compressed point (x=9 not on the +# curve) and whose second half is a valid point +INVALID_PUBNONCE = bytes.fromhex( + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480" +) + +AGGNONCE_WRONG_TAG = bytes.fromhex( + "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9" +) + + +_SCALAR_TOKEN = r"-?\d+|true|false|null" +_SCALAR_ARRAY_RE = re.compile( + rf"\[\s*(?:(?:{_SCALAR_TOKEN})(?:\s*,\s*(?:{_SCALAR_TOKEN}))*)?\s*\]" +) + + +def _inline_scalar_array(match): + tokens = re.findall(_SCALAR_TOKEN, match.group(0)) + return "[" + ", ".join(tokens) + "]" + + +def write_test_vectors(filename, vectors): + output_file = os.path.join("vectors", filename) + text = _SCALAR_ARRAY_RE.sub(_inline_scalar_array, json.dumps(vectors, indent=4)) + json.loads(text) # guard: inlining must keep the JSON parseable + with open(output_file, "w") as f: + f.write(text) + + +def generate_all_nonces(rand, secshares, pubshares, xonly_thresh_pk, msg=None): + secnonces = [] + pubnonces = [] + for i in range(len(secshares)): + sec, pub = nonce_gen_internal( + rand, secshares[i], pubshares[i], xonly_thresh_pk, msg, None + ) + secnonces.append(sec) + pubnonces.append(pub) + return secnonces, pubnonces + + +def reconstruct_thresh_sk(ids, secshares): + assert len(ids) == len(secshares) + result = Scalar(0) + for i, s in zip(ids, secshares): + result = result + derive_interpolating_value( + ids, i + ) * Scalar.from_bytes_checked(s) + return result + + +# Chosen so the threshold pubkey is odd-y (prefix 03) +SECKEY_1OF3 = bytes.fromhex( + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32" +) +# Chosen so the threshold pubkey is even-y (prefix 02) +SECKEY_2OF3 = bytes.fromhex( + "4C08C37F5B9A88FAE396A06E286BA41B654457BF5E35B4A693096ED9AB1491F5" +) +# Chosen so the threshold pubkey is even-y (prefix 02) +SECKEY_3OF3 = bytes.fromhex( + "70E90852E9541FE47552B738A14C2B9B5B38C0979D640BA8C7A5A5EEE1BDA405" +) +# Chosen so the threshold pubkey is odd-y (prefix 03) +SECKEY_3OF5 = bytes.fromhex( + "C97F278DAC5FC3214F4C2DD7551C84D4854DCA143887F54692735C61A16E902A" +) + + +def frost_keygen(seckey=None, n=3, t=2): + # NOTE: don't default `seckey` to secrets.token_bytes(32) in the signature, as that is evaluated once at import time and every no-arg call would reuse it. + if seckey is None: + seckey = secrets.token_bytes(32) + assert len(seckey) == 32 + assert 1 <= t <= n + thresh_pk, secshares, pubshares = trusted_dealer_keygen(seckey, n, t) + assert thresh_pk == pubkey_gen_plain(seckey) + return (n, t, thresh_pk, list(range(n)), secshares, pubshares) + + +# --- Multi-(t, n) config machinery (shared by all four signing generators) --- + +Config = namedtuple("Config", ["tg_id", "t", "n", "seckey"]) + +CONFIGS = [ + Config("2of3", 2, 3, SECKEY_2OF3), + Config("1of3", 1, 3, SECKEY_1OF3), + Config("3of3", 3, 3, SECKEY_3OF3), + Config("3of5", 3, 5, SECKEY_3OF5), +] + + +class SharedGroupInputs: + """The maximal per-test-group bundle: real key/nonce material plus the union pools + (real entries followed by appended fault slots) and the named offsets that index + those slots. Built once per test group; each generator slices what it needs.""" + + def __init__(self, cfg): + n, t, thresh_pk, _ids, secshares, pubshares = frost_keygen( + cfg.seckey, cfg.n, cfg.t + ) + self.n = n + self.t = t + self.thresh_pk = thresh_pk + self.xonly_thresh_pk = XonlyPk(thresh_pk[1:]) + self.secshares = secshares + self.pubshares = pubshares + self.secnonces, self.pubnonces = generate_all_nonces( + COMMON_RAND, secshares, pubshares, self.xonly_thresh_pk + ) + + # pubshares pool: off-curve point at slot n. + self.pool_pubshares = pubshares + [PlainPk(INVALID_PUBSHARE)] + # secshares pool: zero scalar at slot n. + self.pool_secshares = secshares + [b"\x00" * 32] + + # pubnonces pool: off-curve nonce at slot n, then the inverse nonce at slot + # n+1 (negation of the aggregate of the first n-1 real pubnonces). It only + # sums to infinity when paired with indices [0..n-2] plus INVERSE_PUBNONCE_IDX. + # Any other size n-1 subset yields a non-infinity aggregate. + tmp = nonce_agg(self.pubnonces[: n - 1]) + R1 = GE.from_bytes_compressed_with_infinity(tmp[0:33]) + R2 = GE.from_bytes_compressed_with_infinity(tmp[33:66]) + inverse_pubnonce = (-R1).to_bytes_compressed_with_infinity() + ( + -R2 + ).to_bytes_compressed_with_infinity() + self.pool_pubnonces = self.pubnonces + [INVALID_PUBNONCE, inverse_pubnonce] + + # secnonces pool: all-zero at slot n, nonzero-first/zero-second at slot n+1. + zero_second_secnonce = self.secnonces[0][0:32] + b"\x00" * 32 + assert Scalar.from_bytes_nonzero_checked(zero_second_secnonce[0:32]) + self.pool_secnonces = self.secnonces + [b"\x00" * 64, zero_second_secnonce] + + # tweaks pool: 4 common tweaks, out-of-range tweak, then the per-config + # infinity tweak (negation of the reconstructed threshold secret over the + # minimum set; degenerates to -secshares[0] at t=1). + infinity_tweak_scalar = -reconstruct_thresh_sk(list(range(t)), secshares[:t]) + assert ( + GE.from_bytes_compressed(self.thresh_pk) + infinity_tweak_scalar * G + ).infinity + infinity_tweak = infinity_tweak_scalar.to_bytes() + self.tweaks_pool = list(COMMON_TWEAKS) + [OUT_OF_RANGE_TWEAK, infinity_tweak] + + # named offsets into the pools, all derived from n + self.INVALID_PUBSHARE_IDX = n + self.SECSHARE_ZERO_IDX = n + self.INVALID_PUBNONCE_IDX = n + self.INVERSE_PUBNONCE_IDX = n + 1 + self.SECNONCE_ZERO_IDX = n + self.SECNONCE_ZERO_SECOND_IDX = n + 1 + self.OUT_OF_RANGE_ID = n + # tweaks-pool offsets, n-independent + self.OUT_OF_RANGE_TWEAK_IDX = len(COMMON_TWEAKS) + self.INFINITY_TWEAK_IDX = len(COMMON_TWEAKS) + 1 + + +def has_excl0_subset(t, n): + # The "excl0" subset (a size-t signer set that excludes participant 0) is + # usable only when t >= 2 and t < n. At t=n no size-t set can exclude id 0, + # and at t=1 all shares are identical, so excluding id 0 changes nothing. + return t >= 2 and t < n + + +def get_subset(cfg, strategy="min"): + match strategy: + case "min": # minimum threshold subset, the first t ids + return list(range(cfg.t)) + case "full": # all n participants + return list(range(cfg.n)) + case "alt": # id 0 + the last t-1 ids; collapses to [0] at t=1 (caller guards) + return [0] + list(range(cfg.n - cfg.t + 1, cfg.n)) + case "min2": # size-at-least-2 baseline; [0, 1] at t=1 + return list(range(max(cfg.t, 2))) + case "excl0": # t ids from 1, excludes id 0 (only valid when has_excl0_subset) + assert has_excl0_subset(cfg.t, cfg.n) + return list(range(1, cfg.t + 1)) + case _: + raise ValueError(f"Unknown subset strategy: {strategy}") + + +def swap_last_two(indices): + result = list(indices) + assert len(result) >= 2 + result[-1], result[-2] = result[-2], result[-1] + return result + + +def set_group_config(group, cfg, inputs): + group["tg_id"] = cfg.tg_id + group["t"] = cfg.t + group["n"] = cfg.n + group["thresh_pk"] = bytes_to_hex(inputs.thresh_pk) + + +def assign_tc_ids(groups): + tc_id = 1 + for group in groups: + for key, value in group.items(): + if isinstance(value, list) and value and isinstance(value[0], dict): + for i, case in enumerate(value): + group[key][i] = {"tc_id": tc_id, **case} + tc_id += 1 diff --git a/bip-0445/python/generators/det_sign.py b/bip-0445/python/generators/det_sign.py new file mode 100644 index 0000000000..4ad5f501e2 --- /dev/null +++ b/bip-0445/python/generators/det_sign.py @@ -0,0 +1,515 @@ +from typing import List, Optional + +from frost_ref import ( + InvalidContributionError, + SignersContext, + deterministic_sign, + nonce_agg, +) +from frost_ref.signing import nonce_gen_internal + +from generators.common import ( + COMMON_MSGS, + COMMON_TWEAKS, + CONFIGS, + AGGNONCE_WRONG_TAG, + OUT_OF_RANGE_TWEAK, + SharedGroupInputs, + assign_tc_ids, + bytes_list_to_hex, + bytes_to_hex, + expect_exception, + get_subset, + has_excl0_subset, + set_group_config, + swap_last_two, + write_test_vectors, +) + +# Aux-randomness pool: all-zeros, None (omitted), all-ones. +RANDS = [ + bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000"), + None, + bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), +] + +# Fault literals that are case payloads rather than pool material (config-independent, +# never indexed from a pool), so they stay local to this generator. +AGGOTHERNONCE_FIRST_HALF_ZERO = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480" +) +AGGOTHERNONCE_BAD_POINT = bytes.fromhex( + "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC020000000000000000000000000000000000000000000000000000000000000009" +) +AGGOTHERNONCE_EXCEEDS_FIELD = bytes.fromhex( + "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" +) + + +class DetSignGroupBuilder: + """Builds one (t, n) test group for det_sign_vectors.json. Shared inputs and + subsets live on self. Each add_* method appends its category to self.group. + + Index convention: valid cases use secshare_index == my_id (a signer signs with + its own share at pool position my_id).""" + + def __init__(self, cfg): + self.inputs = SharedGroupInputs(cfg) + self.t = self.inputs.t + self.n = self.inputs.n + self.thresh_pk = self.inputs.thresh_pk + + self.cfg = cfg + self.min_s = get_subset(cfg, "min") + self.full = get_subset(cfg, "full") + self.alt = get_subset(cfg, "alt") + self.min2 = get_subset(cfg, "min2") + + self.group = {} + set_group_config(self.group, cfg, self.inputs) + self.group["pubshares"] = bytes_list_to_hex(self.inputs.pool_pubshares) + self.group["secshares"] = bytes_list_to_hex(self.inputs.pool_secshares) + self.group["valid_tests"] = [] + self.group["error_tests"] = [] + + def _derive_aggothernonce( + self, + ids_set: List[int], + my_id: int, + msg: bytes, + rand: Optional[bytes], + ) -> Optional[bytes]: + """Return None when the signer is the sole participant (the set is exactly + [my_id]). Otherwise aggregate the other signers' public nonces.""" + if ids_set == [my_id]: + return None + tmp = b"" if rand is None else rand + other_pubnonces = [] + for pid in ids_set: + if pid == my_id: + continue + _, pub = nonce_gen_internal( + tmp, + self.inputs.pool_secshares[pid], + self.inputs.pool_pubshares[pid], + self.inputs.xonly_thresh_pk, + msg, + None, + ) + other_pubnonces.append(pub) + return nonce_agg(other_pubnonces) + + def _append_valid( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + rand: Optional[bytes], + msg: bytes, + tweaks: List[bytes], + is_xonly: List[bool], + comment: str, + ) -> None: + curr_aggothernonce = self._derive_aggothernonce(ids, my_id, msg, rand) + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + secshare = self.inputs.pool_secshares[my_id] + result = deterministic_sign( + secshare, my_id, curr_aggothernonce, signers, tweaks, is_xonly, msg, rand + ) + self.group["valid_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "secshare_index": my_id, + "aggothernonce": bytes_to_hex(curr_aggothernonce) + if curr_aggothernonce is not None + else None, + "rand": bytes_to_hex(rand) if rand is not None else None, + "msg": bytes_to_hex(msg), + "tweaks": bytes_list_to_hex(tweaks), + "is_xonly": is_xonly, + "expected": bytes_list_to_hex(list(result)), + } + ) + + def _append_error( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + secshare_index: int, + rand: Optional[bytes], + msg: bytes, + tweaks: List[bytes], + is_xonly: List[bool], + error: str, + comment: str, + aggothernonce: Optional[bytes] = None, + ) -> None: + # A caller may supply a crafted aggothernonce to exercise an error path. + curr_aggothernonce: Optional[bytes] + if aggothernonce is not None: + curr_aggothernonce = aggothernonce + else: + curr_aggothernonce = self._derive_aggothernonce(ids, my_id, msg, rand) + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + secshare = self.inputs.pool_secshares[secshare_index] + expected_exc = ValueError if error == "value" else InvalidContributionError + err = expect_exception( + lambda: deterministic_sign( + secshare, + my_id, + curr_aggothernonce, + signers, + tweaks, + is_xonly, + msg, + rand, + ), + expected_exc, + ) + self.group["error_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "secshare_index": secshare_index, + "aggothernonce": bytes_to_hex(curr_aggothernonce) + if curr_aggothernonce is not None + else None, + "rand": bytes_to_hex(rand) if rand is not None else None, + "msg": bytes_to_hex(msg), + "tweaks": bytes_list_to_hex(tweaks), + "is_xonly": is_xonly, + "error": err, + } + ) + + # --- Array A: valid_tests --- + + def add_valid_tests(self) -> None: + t, n = self.t, self.n + # minimum threshold subset. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "Minimum threshold subset of signers", + ) + # reordering (needs a set of size >= 2 to be meaningful, matching sign_verify). + if t >= 2: + rev = list(reversed(self.min_s)) + self._append_valid( + 0, + rev, + rev, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "Reordering the signer set leaves the deterministic output unchanged, because the identifiers are sorted before they are bound into the nonce derivation and the binding value", + ) + # a different threshold subset (needs t >= 2; at t=1 alt collapses to the + # minimum set, matching sign_verify). + if t >= 2 and t < n: + self._append_valid( + 0, + self.alt, + self.alt, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "A different threshold subset gives a different deterministic nonce, since the signer set is bound into the nonce derivation", + ) + # null randomness. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[1], + COMMON_MSGS[0], + [], + [], + "Auxiliary randomness omitted (null), which is not equivalent to all-zeros randomness", + ) + # all-ones randomness. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[2], + COMMON_MSGS[0], + [], + [], + "Auxiliary randomness is all ones, distinct from the all-zeros and omitted cases", + ) + # all signers, non-first member signs. + self._append_valid( + 1, + self.full, + self.full, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "All signers participate, signed by a non-first member of the signer set", + ) + # empty message. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[0], + COMMON_MSGS[1], + [], + [], + "Empty message", + ) + # non-standard message length. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[0], + COMMON_MSGS[2], + [], + [], + "Non-standard message length (38 bytes)", + ) + # single x-only tweak. + self._append_valid( + 0, + self.min_s, + self.min_s, + RANDS[0], + COMMON_MSGS[0], + [COMMON_TWEAKS[0]], + [True], + "Single x-only tweak applied", + ) + + # --- Array B: error_tests --- + + def add_error_tests(self) -> None: + t, n = self.t, self.n + # my_id is absent from the signer set (only when t < n). + if t < n: + self._append_error( + t, + self.min_s, + self.min_s, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Signer's identifier is absent from the signer set", + ) + # duplicate id in the signer set. + self._append_error( + 0, + [0, 1, 1], + [0, 1, 1], + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Signer set contains a duplicate id", + ) + # signer loads share 0 but the set excludes id 0 (needs t >= 2 and t < n). + if has_excl0_subset(t, n): + excl0 = get_subset(self.cfg, "excl0") + self._append_error( + 1, + excl0, + excl0, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Signer's public share is not in the public share list", + ) + # off-curve pubshare at position 1 (min2 forces size >= 2). + pubshare_indices_offcurve = [ + self.min2[0], + self.inputs.INVALID_PUBSHARE_IDX, + ] + self.min2[2:] + self._append_error( + 0, + self.min2, + pubshare_indices_offcurve, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "A public share is not a valid point", + ) + # A signer id equals n, outside the valid range. For t >= 2 an in-range + # member signs. At t=1 the lone id is out of range and the check fires + # first, so the self fields are inert. This assumes signer-id range + # validation runs before the pubshare/threshold-key checks; a different + # order would surface a different error. + if t >= 2: + ids_out_of_range = [self.inputs.OUT_OF_RANGE_ID] + list(range(1, t)) + self._append_error( + 1, + ids_out_of_range, + list(range(t)), + 1, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "A signer id is outside the valid range [0, n-1]", + ) + elif t == 1: + self._append_error( + 0, + [self.inputs.OUT_OF_RANGE_ID], + [0], + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "A signer id is outside the valid range [0, n-1]", + ) + # pubshares don't match the threshold public key. Needs t >= 2: at t=1 the lone + # pubshare must equal the threshold key, so there is no last-two pair to swap. The + # 1of3 config intentionally omits this case, matching sign_verify. + if t >= 2: + self._append_error( + 0, + self.min_s, + swap_last_two(self.min_s), + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Signer set's public shares do not match the threshold public key", + ) + # Bad aggothernonce literals passed directly to bypass the helper. + self._append_error( + 0, + self.min2, + self.min2, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "invalid_contrib", + "Aggregate of the other signers' nonces is invalid: first half has an unknown tag 0x04", + aggothernonce=AGGNONCE_WRONG_TAG, + ) + self._append_error( + 0, + self.min2, + self.min2, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "invalid_contrib", + "Aggregate of the other signers' nonces is invalid: first half is all zeros", + aggothernonce=AGGOTHERNONCE_FIRST_HALF_ZERO, + ) + self._append_error( + 0, + self.min2, + self.min2, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "invalid_contrib", + "Aggregate of the other signers' nonces is invalid: second half is not a point on the curve", + aggothernonce=AGGOTHERNONCE_BAD_POINT, + ) + self._append_error( + 0, + self.min2, + self.min2, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "invalid_contrib", + "Aggregate of the other signers' nonces is invalid: second half's x-coordinate exceeds the field size", + aggothernonce=AGGOTHERNONCE_EXCEEDS_FIELD, + ) + # tweak exceeds the group order. + self._append_error( + 0, + self.min_s, + self.min_s, + 0, + RANDS[0], + COMMON_MSGS[0], + [OUT_OF_RANGE_TWEAK], + [False], + "value", + "Tweak exceeds the group order", + ) + # Fewer signers than the threshold (empty set at t=1). + below = list(range(t - 1)) + self._append_error( + 0, + below, + below, + 0, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Fewer signers than the threshold t", + ) + # signing with a zero secret share + self._append_error( + 0, + self.min_s, + self.min_s, + self.inputs.SECSHARE_ZERO_IDX, + RANDS[0], + COMMON_MSGS[0], + [], + [], + "value", + "Secret share is out of range (zero)", + ) + + def build(self) -> dict: + self.add_valid_tests() + self.add_error_tests() + return self.group + + +def generate_det_sign_vectors() -> None: + groups = [DetSignGroupBuilder(cfg).build() for cfg in CONFIGS] + assign_tc_ids(groups) + write_test_vectors("det_sign_vectors.json", {"test_groups": groups}) diff --git a/bip-0445/python/generators/nonce.py b/bip-0445/python/generators/nonce.py new file mode 100644 index 0000000000..036d7ac2b9 --- /dev/null +++ b/bip-0445/python/generators/nonce.py @@ -0,0 +1,230 @@ +from frost_ref import InvalidContributionError, nonce_agg +from frost_ref.signing import nonce_gen_internal + +from generators.common import ( + COMMON_MSGS, + COMMON_RAND, + SECKEY_2OF3, + bytes_list_to_hex, + bytes_to_hex, + expect_exception, + frost_keygen, + hex_list_to_bytes, + write_test_vectors, +) + + +def generate_nonce_gen_vectors(): + vectors = {} + vectors["valid_tests"] = [] + tc_id = 1 + + _, _, thresh_pk, _, secshares, pubshares = frost_keygen(SECKEY_2OF3) + xonly_thresh_pk = thresh_pk[1:] + extra_in = bytes.fromhex( + "0808080808080808080808080808080808080808080808080808080808080808" + ) + + # --- Valid Test Case 1 --- + msg = bytes.fromhex( + "0101010101010101010101010101010101010101010101010101010101010101" + ) + secnonce, pubnonce = nonce_gen_internal( + COMMON_RAND, secshares[0], pubshares[0], xonly_thresh_pk, msg, extra_in + ) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "All optional defense-in-depth arguments present", + "rand_": bytes_to_hex(COMMON_RAND), + "secshare": bytes_to_hex(secshares[0]), + "pubshare": bytes_to_hex(pubshares[0]), + "thresh_pk": bytes_to_hex(xonly_thresh_pk), + "msg": bytes_to_hex(msg), + "extra_in": bytes_to_hex(extra_in), + "expected": [bytes_to_hex(secnonce), bytes_to_hex(pubnonce)], + } + ) + tc_id += 1 + # --- Valid Test Case 2 --- + secnonce, pubnonce = nonce_gen_internal( + COMMON_RAND, + secshares[0], + pubshares[0], + xonly_thresh_pk, + COMMON_MSGS[1], + extra_in, + ) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "Empty message", + "rand_": bytes_to_hex(COMMON_RAND), + "secshare": bytes_to_hex(secshares[0]), + "pubshare": bytes_to_hex(pubshares[0]), + "thresh_pk": bytes_to_hex(xonly_thresh_pk), + "msg": bytes_to_hex(COMMON_MSGS[1]), + "extra_in": bytes_to_hex(extra_in), + "expected": [bytes_to_hex(secnonce), bytes_to_hex(pubnonce)], + } + ) + tc_id += 1 + # --- Valid Test Case 3 --- + secnonce, pubnonce = nonce_gen_internal( + COMMON_RAND, + secshares[0], + pubshares[0], + xonly_thresh_pk, + COMMON_MSGS[2], + extra_in, + ) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "Non-standard message length (38 bytes)", + "rand_": bytes_to_hex(COMMON_RAND), + "secshare": bytes_to_hex(secshares[0]), + "pubshare": bytes_to_hex(pubshares[0]), + "thresh_pk": bytes_to_hex(xonly_thresh_pk), + "msg": bytes_to_hex(COMMON_MSGS[2]), + "extra_in": bytes_to_hex(extra_in), + "expected": [bytes_to_hex(secnonce), bytes_to_hex(pubnonce)], + } + ) + tc_id += 1 + # --- Valid Test Case 4 --- + secnonce, pubnonce = nonce_gen_internal(COMMON_RAND, None, None, None, None, None) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "All optional defense-in-depth arguments omitted", + "rand_": bytes_to_hex(COMMON_RAND), + "secshare": None, + "pubshare": None, + "thresh_pk": None, + "msg": None, + "extra_in": None, + "expected": [bytes_to_hex(secnonce), bytes_to_hex(pubnonce)], + } + ) + tc_id += 1 + # --- Valid Test Case 5 --- + secnonce, pubnonce = nonce_gen_internal( + COMMON_RAND, secshares[0], pubshares[0], xonly_thresh_pk, None, extra_in + ) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "Message omitted, other optional arguments present", + "rand_": bytes_to_hex(COMMON_RAND), + "secshare": bytes_to_hex(secshares[0]), + "pubshare": bytes_to_hex(pubshares[0]), + "thresh_pk": bytes_to_hex(xonly_thresh_pk), + "msg": None, + "extra_in": bytes_to_hex(extra_in), + "expected": [bytes_to_hex(secnonce), bytes_to_hex(pubnonce)], + } + ) + tc_id += 1 + + write_test_vectors("nonce_gen_vectors.json", vectors) + + +def generate_nonce_agg_vectors(): + vectors = {} + + # Special pubnonce indices for test cases + INVALID_TAG_IDX = 4 + INVALID_XCOORD_IDX = 5 + INVALID_EXCEEDS_FIELD_IDX = 6 + + pubnonces = hex_list_to_bytes( + [ + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + ] + ) + vectors["pubnonces"] = bytes_list_to_hex(pubnonces) + + tc_id = 1 + vectors["valid_tests"] = [] + # --- Valid Test Case 1 --- + pubnonce_indices = [0, 1] + curr_pubnonces = [pubnonces[i] for i in pubnonce_indices] + aggnonce = nonce_agg(curr_pubnonces) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "Two well-formed public nonces", + "pubnonce_indices": pubnonce_indices, + "expected": bytes_to_hex(aggnonce), + } + ) + tc_id += 1 + # --- Valid Test Case 2 --- + pubnonce_indices = [2, 3] + curr_pubnonces = [pubnonces[i] for i in pubnonce_indices] + aggnonce = nonce_agg(curr_pubnonces) + vectors["valid_tests"].append( + { + "tc_id": tc_id, + "comment": "Second halves sum to the point at infinity, which is serialized as the all-zero encoding", + "pubnonce_indices": pubnonce_indices, + "expected": bytes_to_hex(aggnonce), + } + ) + tc_id += 1 + + vectors["error_tests"] = [] + # --- Error Test Case 1 --- + pubnonce_indices = [0, INVALID_TAG_IDX] + curr_pubnonces = [pubnonces[i] for i in pubnonce_indices] + error = expect_exception( + lambda: nonce_agg(curr_pubnonces), InvalidContributionError + ) + vectors["error_tests"].append( + { + "tc_id": tc_id, + "comment": "Public nonce is invalid: first half has an unknown tag 0x04", + "pubnonce_indices": pubnonce_indices, + "error": error, + } + ) + tc_id += 1 + # --- Error Test Case 2 --- + pubnonce_indices = [INVALID_XCOORD_IDX, 1] + curr_pubnonces = [pubnonces[i] for i in pubnonce_indices] + error = expect_exception( + lambda: nonce_agg(curr_pubnonces), InvalidContributionError + ) + vectors["error_tests"].append( + { + "tc_id": tc_id, + "comment": "Public nonce is invalid: second half is not a point on the curve", + "pubnonce_indices": pubnonce_indices, + "error": error, + } + ) + tc_id += 1 + # --- Error Test Case 3 --- + pubnonce_indices = [INVALID_EXCEEDS_FIELD_IDX, 1] + curr_pubnonces = [pubnonces[i] for i in pubnonce_indices] + error = expect_exception( + lambda: nonce_agg(curr_pubnonces), InvalidContributionError + ) + vectors["error_tests"].append( + { + "tc_id": tc_id, + "comment": "Public nonce is invalid: second half's x-coordinate exceeds the field size", + "pubnonce_indices": pubnonce_indices, + "error": error, + } + ) + tc_id += 1 + + write_test_vectors("nonce_agg_vectors.json", vectors) diff --git a/bip-0445/python/generators/sig_agg.py b/bip-0445/python/generators/sig_agg.py new file mode 100644 index 0000000000..a435ae4ef4 --- /dev/null +++ b/bip-0445/python/generators/sig_agg.py @@ -0,0 +1,210 @@ +from typing import List + +from frost_ref import ( + InvalidContributionError, + SessionContext, + SignersContext, + nonce_agg, + partial_sig_agg, + partial_sig_verify, + sign, +) + +from generators.common import ( + COMMON_MSGS, + COMMON_TWEAKS, + CONFIGS, + GROUP_ORDER, + SharedGroupInputs, + assign_tc_ids, + bytes_list_to_hex, + bytes_to_hex, + expect_exception, + get_subset, + set_group_config, + write_test_vectors, +) + + +class SigAggGroupBuilder: + """Builds one (t, n) test group for sig_agg_vectors.json. Shared inputs and + subsets live on self. Each add_* method appends its category to self.group. + + Index convention: set_indices selects the signing subset from the pool (sig_agg + has no per-signer my_id material convention and no appended fault slots).""" + + def __init__(self, cfg): + self.inputs = SharedGroupInputs(cfg) + self.t = self.inputs.t + self.n = self.inputs.n + self.thresh_pk = self.inputs.thresh_pk + + self.min_s = get_subset(cfg, "min") + self.full = get_subset(cfg, "full") + + self.group = {} + set_group_config(self.group, cfg, self.inputs) + # sig_agg has no appended fault slots in any pool (both error faults are + # injected inline), so the group serializes and reads the plain n-length + # pubshares and the 4 common tweaks, not the SharedGroupInputs pool_* arrays. + self.group["pubshares"] = bytes_list_to_hex(self.inputs.pubshares) + self.group["tweaks"] = bytes_list_to_hex(COMMON_TWEAKS) + self.group["valid_tests"] = [] + self.group["error_tests"] = [] + + def _append_valid( + self, + set_indices: List[int], + tweak_indices: List[int], + is_xonly: List[bool], + msg: bytes, + comment: str, + ) -> None: + pubshares = [self.inputs.pubshares[i] for i in set_indices] + pubnonces = [self.inputs.pubnonces[i] for i in set_indices] + ids = list(set_indices) + aggnonce = nonce_agg(pubnonces) + tweaks = [COMMON_TWEAKS[i] for i in tweak_indices] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, tweaks, is_xonly, msg) + psigs = [] + for signer_index, my_id in enumerate(set_indices): + psig = sign( + bytearray(self.inputs.secnonces[my_id]), + self.inputs.secshares[my_id], + my_id, + session, + ) + psigs.append(psig) + assert partial_sig_verify( + psig, pubnonces, signers, tweaks, is_xonly, msg, signer_index + ) + expected = partial_sig_agg(psigs, session) + self.group["valid_tests"].append( + { + "comment": comment, + "ids": ids, + "pubshare_indices": list(set_indices), + "aggnonce": bytes_to_hex(aggnonce), + "tweak_indices": tweak_indices, + "is_xonly": is_xonly, + "psigs": bytes_list_to_hex(psigs), + "msg": bytes_to_hex(msg), + "expected": bytes_to_hex(expected), + } + ) + + def _append_error( + self, set_indices: List[int], fault: str, error: str, comment: str + ) -> None: + pubshares = [self.inputs.pubshares[i] for i in set_indices] + pubnonces = [self.inputs.pubnonces[i] for i in set_indices] + ids = list(set_indices) + aggnonce = nonce_agg(pubnonces) + msg = COMMON_MSGS[0] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, [], [], msg) + psigs = [] + for signer_index, my_id in enumerate(set_indices): + psig = sign( + bytearray(self.inputs.secnonces[my_id]), + self.inputs.secshares[my_id], + my_id, + session, + ) + psigs.append(psig) + assert partial_sig_verify( + psig, pubnonces, signers, [], [], msg, signer_index + ) + + if fault == "psig_out_of_range": + psigs[-1] = GROUP_ORDER + elif fault == "psig_count_mismatch": + psigs = psigs[:-1] + + expected_exc = ValueError if error == "value" else InvalidContributionError + err = expect_exception(lambda: partial_sig_agg(psigs, session), expected_exc) + self.group["error_tests"].append( + { + "comment": comment, + "ids": ids, + "pubshare_indices": list(set_indices), + "aggnonce": bytes_to_hex(aggnonce), + "tweak_indices": [], + "is_xonly": [], + "psigs": bytes_list_to_hex(psigs), + "msg": bytes_to_hex(msg), + "error": err, + } + ) + + # --- Array A: valid_tests --- + + def add_valid_tests(self) -> None: + t, n = self.t, self.n + # Minimum threshold subset. + self._append_valid( + self.min_s, + [], + [], + COMMON_MSGS[0], + "Minimum threshold subset of signers, no tweaks", + ) + # Order-invariance (needs a set of size >= 2 to be meaningful). + if t >= 2: + rev = list(reversed(self.min_s)) + self._append_valid( + rev, + [], + [], + COMMON_MSGS[0], + "Reordering the signer set leaves the aggregate signature unchanged, because the partial signatures are summed and the identifiers are sorted before they are bound into the binding value", + ) + # Three tweaks applied (one x-only, two plain). + self._append_valid( + self.min_s, + [0, 1, 2], + [True, False, False], + COMMON_MSGS[0], + "Aggregation with three tweaks applied (one x-only, two plain)", + ) + # All n signers participate (dropped when t == n, as the all-n set + # equals the minimum set and partial_sig_agg has no my_id field, so the + # aggregate signature would be byte-identical to the minimum-subset case). + if t < n: + self._append_valid( + self.full, + [], + [], + COMMON_MSGS[0], + "All signers participate, no tweaks", + ) + + # --- Array B: error_tests --- + + def add_error_tests(self) -> None: + # Partial signature equals the group order (out-of-range scalar). + self._append_error( + self.min_s, + "psig_out_of_range", + "invalid_contrib", + "Partial signature equals the group order, which is out of range", + ) + # Number of partial signatures does not match the number of signers. + self._append_error( + self.min_s, + "psig_count_mismatch", + "value", + "Number of partial signatures does not match the number of signers", + ) + + def build(self) -> dict: + self.add_valid_tests() + self.add_error_tests() + return self.group + + +def generate_sig_agg_vectors() -> None: + groups = [SigAggGroupBuilder(cfg).build() for cfg in CONFIGS] + assign_tc_ids(groups) + write_test_vectors("sig_agg_vectors.json", {"test_groups": groups}) diff --git a/bip-0445/python/generators/sign_verify.py b/bip-0445/python/generators/sign_verify.py new file mode 100644 index 0000000000..4a97813a44 --- /dev/null +++ b/bip-0445/python/generators/sign_verify.py @@ -0,0 +1,571 @@ +from typing import List + +from frost_ref import ( + InvalidContributionError, + SessionContext, + SignersContext, + nonce_agg, + partial_sig_verify, + sign, +) +from secp256k1lab.secp256k1 import Scalar + +from generators.common import ( + COMMON_MSGS, + CONFIGS, + GROUP_ORDER, + AGGNONCE_WRONG_TAG, + SharedGroupInputs, + assign_tc_ids, + bytes_list_to_hex, + bytes_to_hex, + expect_exception, + get_subset, + has_excl0_subset, + set_group_config, + swap_last_two, + write_test_vectors, +) + +# Fault literals that are case payloads rather than pool material (config-independent, +# never indexed from a pool), so they stay local to this generator. +AGGNONCE_BAD_XCOORD = bytes.fromhex( + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009" +) +AGGNONCE_EXCEEDS_FIELD = bytes.fromhex( + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" +) + + +class SignVerifyGroupBuilder: + """Builds one (t, n) test group for sign_verify_vectors.json. Shared inputs and + subsets live on self. Each add_* method appends its category to self.group. + + Index convention: valid cases use secshare_index == secnonce_index == my_id (a + signer signs with its own material at pool position my_id), and verify-side cases + use signer 0's material as the base partial signature.""" + + def __init__(self, cfg): + self.inputs = SharedGroupInputs(cfg) + self.t = self.inputs.t + self.n = self.inputs.n + self.thresh_pk = self.inputs.thresh_pk + + self.cfg = cfg + self.min_s = get_subset(cfg, "min") + self.full = get_subset(cfg, "full") + self.alt = get_subset(cfg, "alt") + self.min2 = get_subset(cfg, "min2") + self.aggnonce_min = self._agg(self.min_s) + + self.group = {} + set_group_config(self.group, cfg, self.inputs) + self.group["pubshares"] = bytes_list_to_hex(self.inputs.pool_pubshares) + self.group["pubnonces"] = bytes_list_to_hex(self.inputs.pool_pubnonces) + self.group["secshares"] = bytes_list_to_hex(self.inputs.pool_secshares) + self.group["secnonces"] = bytes_list_to_hex(self.inputs.pool_secnonces) + self.group["valid_tests"] = [] + self.group["sign_error_tests"] = [] + self.group["verify_fail_tests"] = [] + self.group["verify_error_tests"] = [] + + def _agg(self, pubnonce_indices: List[int]) -> bytes: + return nonce_agg([self.inputs.pool_pubnonces[i] for i in pubnonce_indices]) + + def _append_valid( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + pubnonce_indices: List[int], + aggnonce: bytes, + msg: bytes, + comment: str, + ) -> None: + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + pubnonces = [self.inputs.pool_pubnonces[i] for i in pubnonce_indices] + secnonce = bytearray(self.inputs.pool_secnonces[my_id]) + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, [], [], msg) + psig = sign(secnonce, self.inputs.pool_secshares[my_id], my_id, session) + assert partial_sig_verify( + psig, pubnonces, signers, [], [], msg, ids.index(my_id) + ) + self.group["valid_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "pubnonce_indices": pubnonce_indices, + "secshare_index": my_id, + "secnonce_index": my_id, + "aggnonce": bytes_to_hex(aggnonce), + "msg": bytes_to_hex(msg), + "expected": bytes_to_hex(psig), + } + ) + + def _append_sign_error( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + secshare_idx: int, + secnonce_idx: int, + aggnonce: bytes, + msg: bytes, + error: str, + comment: str, + ) -> None: + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + secshare = self.inputs.pool_secshares[secshare_idx] + secnonce = bytearray(self.inputs.pool_secnonces[secnonce_idx]) + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, [], [], msg) + expected_exc = ValueError if error == "value" else InvalidContributionError + err = expect_exception( + lambda: sign(secnonce, secshare, my_id, session), expected_exc + ) + self.group["sign_error_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "secshare_index": secshare_idx, + "secnonce_index": secnonce_idx, + "aggnonce": bytes_to_hex(aggnonce), + "msg": bytes_to_hex(msg), + "error": err, + } + ) + + def _append_verify_error( + self, + ids: List[int], + pubshare_indices: List[int], + pubnonce_indices: List[int], + signer_index: int, + psig: bytes, + error: str, + comment: str, + ) -> None: + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + pubnonces = [self.inputs.pool_pubnonces[i] for i in pubnonce_indices] + msg = COMMON_MSGS[0] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + expected_exc = ValueError if error == "value" else InvalidContributionError + err = expect_exception( + lambda: partial_sig_verify( + psig, pubnonces, signers, [], [], msg, signer_index + ), + expected_exc, + ) + self.group["verify_error_tests"].append( + { + "comment": comment, + "psig": bytes_to_hex(psig), + "ids": ids, + "pubshare_indices": pubshare_indices, + "pubnonce_indices": pubnonce_indices, + "signer_index": signer_index, + "msg": bytes_to_hex(msg), + "error": err, + } + ) + + def _append_verify_fail( + self, + ids: List[int], + pubshare_indices: List[int], + pubnonce_indices: List[int], + signer_index: int, + psig: bytes, + comment: str, + ) -> None: + pubshares = [self.inputs.pool_pubshares[i] for i in pubshare_indices] + pubnonces = [self.inputs.pool_pubnonces[i] for i in pubnonce_indices] + msg = COMMON_MSGS[0] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + assert not partial_sig_verify( + psig, pubnonces, signers, [], [], msg, signer_index + ) + self.group["verify_fail_tests"].append( + { + "comment": comment, + "psig": bytes_to_hex(psig), + "ids": ids, + "pubshare_indices": pubshare_indices, + "pubnonce_indices": pubnonce_indices, + "signer_index": signer_index, + "msg": bytes_to_hex(msg), + } + ) + + # --- Array A: valid_tests --- + + def add_valid_tests(self) -> None: + t, n = self.t, self.n + # Minimum threshold subset. + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + COMMON_MSGS[0], + "Minimum threshold subset of signers", + ) + # Order-invariance (needs a set of size >= 2 to be meaningful). + if t >= 2: + rev = list(reversed(self.min_s)) + self._append_valid( + 0, + rev, + rev, + rev, + self._agg(rev), + COMMON_MSGS[0], + "Reordering the signer set leaves the partial signature unchanged, because the identifiers are sorted before they are bound into the binding value", + ) + # A different threshold subset (needs t >= 2 and t < n, else the alt set + # collapses to the minimum set or is the only valid set). + if t >= 2 and t < n: + self._append_valid( + 0, + self.alt, + self.alt, + self.alt, + self._agg(self.alt), + COMMON_MSGS[0], + "A different threshold subset gives a different partial signature, since the Lagrange coefficients depend on the signer set", + ) + # All n signers, signed by a non-first member. + self._append_valid( + 1, + self.full, + self.full, + self.full, + self._agg(self.full), + COMMON_MSGS[0], + "All signers participate, signed by a non-first member of the signer set", + ) + # Aggregate nonce is the point at infinity. The inverse pubnonce cancels + # the first n-1 real pubnonces, so the aggregate over them is infinity. + inf_pubnonce_indices = list(range(n - 1)) + [self.inputs.INVERSE_PUBNONCE_IDX] + self._append_valid( + 0, + self.full, + self.full, + inf_pubnonce_indices, + self._agg(inf_pubnonce_indices), + COMMON_MSGS[0], + "Aggregate nonce is the point at infinity, so the final nonce point falls back to the generator G", + ) + # Message variations over the minimum set. + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + COMMON_MSGS[1], + "Empty message", + ) + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + COMMON_MSGS[2], + "Non-standard message length (38 bytes)", + ) + + # --- Array B: sign_error_tests --- + + def add_sign_error_tests(self) -> None: + t, n = self.t, self.n + # my_id is a valid participant absent from the set (needs t < n so a valid + # participant can sit outside the only valid set). + if t < n: + self._append_sign_error( + t, + self.min_s, + self.min_s, + 0, + 0, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Signer's identifier is absent from the signer set", + ) + # Duplicate id in the set (fixed [0, 1, 1], valid for every config). + self._append_sign_error( + 0, + [0, 1, 1], + [0, 1, 1], + 0, + 0, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Signer set contains a duplicate id", + ) + # Signer loads share 0 but the set excludes id 0, so its derived pubshare + # is absent (needs t >= 2 for distinct shares and t < n for a participant + # outside the set). + if has_excl0_subset(t, n): + excl0 = get_subset(self.cfg, "excl0") + self._append_sign_error( + 1, + excl0, + excl0, + 0, + 0, + self._agg(excl0), + COMMON_MSGS[0], + "value", + "Signer's public share is not in the public share list", + ) + # A listed public share is off-curve (at position 1, so min2 forces size 2). + pubshare_indices_offcurve = [ + self.min2[0], + self.inputs.INVALID_PUBSHARE_IDX, + ] + self.min2[2:] + self._append_sign_error( + 0, + self.min2, + pubshare_indices_offcurve, + 0, + 0, + self._agg(self.min2), + COMMON_MSGS[0], + "value", + "A public share is not a valid point", + ) + # A signer id equals n, outside the valid range. For t >= 2 an in-range + # member signs. At t=1 the lone id is out of range and the check fires + # first, so the self fields are inert. This assumes signer-id range + # validation runs before the pubshare/threshold-key checks; a different + # order would surface a different error. + if t >= 2: + ids_out_of_range = [self.inputs.OUT_OF_RANGE_ID] + list(range(1, t)) + self._append_sign_error( + 1, + ids_out_of_range, + list(range(t)), + 1, + 1, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "A signer id is outside the valid range [0, n-1]", + ) + else: + self._append_sign_error( + 0, + [self.inputs.OUT_OF_RANGE_ID], + [0], + 0, + 0, + self._agg([0]), + COMMON_MSGS[0], + "value", + "A signer id is outside the valid range [0, n-1]", + ) + # Public shares won't interpolate to the correct threshold key, since we're + # swapping the last two positions (needs t >= 2 for distinct shares). + if t >= 2: + self._append_sign_error( + 0, + self.min_s, + swap_last_two(self.min_s), + 0, + 0, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Signer set's public shares do not match the threshold public key", + ) + # Invalid aggregate nonce literals. + self._append_sign_error( + 0, + self.min_s, + self.min_s, + 0, + 0, + AGGNONCE_WRONG_TAG, + COMMON_MSGS[0], + "invalid_contrib", + "Aggregate nonce is invalid: first half has an unknown tag 0x04", + ) + self._append_sign_error( + 0, + self.min_s, + self.min_s, + 0, + 0, + AGGNONCE_BAD_XCOORD, + COMMON_MSGS[0], + "invalid_contrib", + "Aggregate nonce is invalid: second half is not a point on the curve", + ) + self._append_sign_error( + 0, + self.min_s, + self.min_s, + 0, + 0, + AGGNONCE_EXCEEDS_FIELD, + COMMON_MSGS[0], + "invalid_contrib", + "Aggregate nonce is invalid: second half's x-coordinate exceeds the field size", + ) + # All-zero secret nonce (first scalar out of range). + self._append_sign_error( + 0, + self.min_s, + self.min_s, + 0, + self.inputs.SECNONCE_ZERO_IDX, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Secret nonce's first half is out of range (all-zero nonce, which may indicate nonce reuse)", + ) + # Secret nonce with a zero second scalar. + self._append_sign_error( + 0, + self.min_s, + self.min_s, + 0, + self.inputs.SECNONCE_ZERO_SECOND_IDX, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Secret nonce's second half is out of range (zero)", + ) + # Fewer signers than the threshold (empty set at t=1). The aggnonce, secshare, + # and secnonce fields are inert placeholders: SignersContext rejects the + # sub-threshold set before sign() reads them. + below = list(range(t - 1)) + self._append_sign_error( + 0, + below, + below, + 0, + 0, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Fewer signers than the threshold t", + ) + # Zero secret share. + self._append_sign_error( + 0, + self.min_s, + self.min_s, + self.inputs.SECSHARE_ZERO_IDX, + 0, + self.aggnonce_min, + COMMON_MSGS[0], + "value", + "Secret share is out of range (zero)", + ) + + # --- Array C: verify_fail_tests --- + + def add_verify_fail_tests(self) -> None: + # Base partial signature: signer 0 over min2, the size-at-least-2 baseline set. + # min2 (not min_s) because the wrong-signer-index fail case below verifies at + # signer_index 1, which needs a 2-signer set. min2 == min_s for t >= 2, so this + # only differs at t=1. + secnonce = bytearray(self.inputs.pool_secnonces[0]) + signers = SignersContext( + self.n, + self.t, + self.min2, + [self.inputs.pool_pubshares[i] for i in self.min2], + self.thresh_pk, + ) + session = SessionContext(self._agg(self.min2), signers, [], [], COMMON_MSGS[0]) + psig = sign(secnonce, self.inputs.pool_secshares[0], 0, session) + neg_psig = (-Scalar.from_bytes_checked(psig)).to_bytes() + + self._append_verify_fail( + self.min2, + self.min2, + self.min2, + 0, + neg_psig, + "Negated partial signature fails the verification equation", + ) + self._append_verify_fail( + self.min2, + self.min2, + self.min2, + 1, + psig, + "A valid partial signature checked against the wrong signer fails the verification equation", + ) + self._append_verify_fail( + self.min2, + self.min2, + self.min2, + 0, + GROUP_ORDER, + "Partial signature equals the group order, which is out of range", + ) + + # --- Array D: verify_error_tests --- + + def add_verify_error_tests(self) -> None: + # Base partial signature: signer 0 over the minimum set. + secnonce = bytearray(self.inputs.pool_secnonces[0]) + signers = SignersContext( + self.n, + self.t, + self.min_s, + [self.inputs.pool_pubshares[i] for i in self.min_s], + self.thresh_pk, + ) + session = SessionContext(self.aggnonce_min, signers, [], [], COMMON_MSGS[0]) + psig_min = sign(secnonce, self.inputs.pool_secshares[0], 0, session) + + # Off-curve public nonce at position 0. + pubnonce_indices_offcurve = [self.inputs.INVALID_PUBNONCE_IDX] + self.min_s[1:] + self._append_verify_error( + self.min_s, + self.min_s, + pubnonce_indices_offcurve, + 0, + psig_min, + "invalid_contrib", + "Verification rejects an invalid public nonce, blaming the malicious signer", + ) + # Off-curve public share at position 0. + pubshare_indices_offcurve = [self.inputs.INVALID_PUBSHARE_IDX] + self.min_s[1:] + self._append_verify_error( + self.min_s, + pubshare_indices_offcurve, + self.min_s, + 0, + psig_min, + "value", + "A public share is not a valid point", + ) + + def build(self) -> dict: + self.add_valid_tests() + self.add_sign_error_tests() + self.add_verify_fail_tests() + self.add_verify_error_tests() + return self.group + + +def generate_sign_verify_vectors() -> None: + groups = [SignVerifyGroupBuilder(cfg).build() for cfg in CONFIGS] + assign_tc_ids(groups) + write_test_vectors("sign_verify_vectors.json", {"test_groups": groups}) diff --git a/bip-0445/python/generators/tweak.py b/bip-0445/python/generators/tweak.py new file mode 100644 index 0000000000..e32550cbbc --- /dev/null +++ b/bip-0445/python/generators/tweak.py @@ -0,0 +1,311 @@ +from typing import List + +from frost_ref import ( + SessionContext, + SignersContext, + nonce_agg, + partial_sig_verify, + sign, +) + +from generators.common import ( + COMMON_MSGS, + CONFIGS, + SharedGroupInputs, + assign_tc_ids, + bytes_list_to_hex, + bytes_to_hex, + expect_exception, + get_subset, + set_group_config, + write_test_vectors, +) + +# an invalid 33-byte tweak value, kept local to this generator rather than in common.py +INVALID_33_BYTE_TWEAK = bytes.fromhex( + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BBFF" +) + + +class TweakGroupBuilder: + """Builds one (t, n) test group for tweak_vectors.json. Shared inputs and + subsets live on self. Each add_* method appends its category to self.group. + + Index convention: valid cases use secshare_index == secnonce_index == my_id (a + signer signs with its own material at pool position my_id).""" + + def __init__(self, cfg): + self.inputs = SharedGroupInputs(cfg) + self.t = self.inputs.t + self.n = self.inputs.n + self.thresh_pk = self.inputs.thresh_pk + + self.min_s = get_subset(cfg, "min") + self.full = get_subset(cfg, "full") + self.aggnonce_min = self._agg(self.min_s) + + # Build the tweaks pool: self.inputs.tweaks_pool has 6 entries (indices 0-5). + # Append INVALID_33_BYTE_TWEAK at the next slot (len of the shared pool). + self.tweaks_pool = list(self.inputs.tweaks_pool) + [INVALID_33_BYTE_TWEAK] + + self.group = {} + set_group_config(self.group, cfg, self.inputs) + # Tweak error cases fault only the tweak value, so the group serializes the + # real (non-pool) pubshares, pubnonces, secshares, and secnonces. The tweaks + # array is the extended pool with the invalid 33-byte entry appended. Invalid + # pubshare, nonce, and secret-share faults are covered in + # sign_verify_vectors.json, not here. + self.group["pubshares"] = bytes_list_to_hex(self.inputs.pubshares) + self.group["pubnonces"] = bytes_list_to_hex(self.inputs.pubnonces) + self.group["secshares"] = bytes_list_to_hex(self.inputs.secshares) + self.group["secnonces"] = bytes_list_to_hex(self.inputs.secnonces) + self.group["tweaks"] = bytes_list_to_hex(self.tweaks_pool) + self.group["valid_tests"] = [] + self.group["error_tests"] = [] + + def _agg(self, pubnonce_indices: List[int]) -> bytes: + return nonce_agg([self.inputs.pubnonces[i] for i in pubnonce_indices]) + + def _append_valid( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + pubnonce_indices: List[int], + aggnonce: bytes, + msg: bytes, + tweak_indices: List[int], + is_xonly: List[bool], + comment: str, + ) -> None: + pubshares = [self.inputs.pubshares[i] for i in pubshare_indices] + tweaks = [self.tweaks_pool[i] for i in tweak_indices] + secnonce = bytearray(self.inputs.secnonces[my_id]) + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, tweaks, is_xonly, msg) + psig = sign(secnonce, self.inputs.secshares[my_id], my_id, session) + assert partial_sig_verify( + psig, + [self.inputs.pubnonces[i] for i in pubnonce_indices], + signers, + tweaks, + is_xonly, + msg, + ids.index(my_id), + ) + self.group["valid_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "pubnonce_indices": pubnonce_indices, + "secshare_index": my_id, + "secnonce_index": my_id, + "aggnonce": bytes_to_hex(aggnonce), + "msg": bytes_to_hex(msg), + "tweak_indices": tweak_indices, + "is_xonly": is_xonly, + "expected": bytes_to_hex(psig), + } + ) + + def _append_error( + self, + my_id: int, + ids: List[int], + pubshare_indices: List[int], + secshare_index: int, + secnonce_index: int, + aggnonce: bytes, + msg: bytes, + tweak_indices: List[int], + is_xonly: List[bool], + comment: str, + ) -> None: + pubshares = [self.inputs.pubshares[i] for i in pubshare_indices] + tweaks = [self.tweaks_pool[i] for i in tweak_indices] + secnonce = bytearray(self.inputs.secnonces[secnonce_index]) + secshare = self.inputs.secshares[secshare_index] + signers = SignersContext(self.n, self.t, ids, pubshares, self.thresh_pk) + session = SessionContext(aggnonce, signers, tweaks, is_xonly, msg) + err = expect_exception( + lambda: sign(secnonce, secshare, my_id, session), ValueError + ) + self.group["error_tests"].append( + { + "comment": comment, + "my_id": my_id, + "ids": ids, + "pubshare_indices": pubshare_indices, + "secshare_index": secshare_index, + "secnonce_index": secnonce_index, + "aggnonce": bytes_to_hex(aggnonce), + "msg": bytes_to_hex(msg), + "tweak_indices": tweak_indices, + "is_xonly": is_xonly, + "error": err, + } + ) + + # --- Array A: valid_tests --- + + def add_valid_tests(self) -> None: + msg = COMMON_MSGS[0] + aggnonce_full = self._agg(self.full) + + # No tweaks applied + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [], + [], + "No tweaks applied", + ) + # Single x-only tweak + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [0], + [True], + "Single x-only tweak (used for BIP341 Taproot)", + ) + # Single plain tweak + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [0], + [False], + "Single plain tweak (used for BIP32 derivation)", + ) + # Plain then x-only tweak + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [0, 1], + [False, True], + "A plain tweak followed by an x-only tweak", + ) + # Four tweaks alternating x-only and plain + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [0, 1, 2, 3], + [True, False, True, False], + "Four tweaks alternating x-only and plain", + ) + # Four tweaks: two plain then two x-only + self._append_valid( + 0, + self.min_s, + self.min_s, + self.min_s, + self.aggnonce_min, + msg, + [0, 1, 2, 3], + [False, False, True, True], + "Four tweaks: two plain followed by two x-only", + ) + # Same tweaks as the previous case but all n signers, signed by non-first member + self._append_valid( + 1, + self.full, + self.full, + self.full, + aggnonce_full, + msg, + [0, 1, 2, 3], + [False, False, True, True], + "Same tweaks as the previous case but with all signers participating, signed by a non-first member of the signer set", + ) + + # --- Array B: error_tests --- + + def add_error_tests(self) -> None: + msg = COMMON_MSGS[0] + my_id = 0 + + # Tweak exceeds the group order + self._append_error( + my_id, + self.min_s, + self.min_s, + 0, + 0, + self.aggnonce_min, + msg, + [self.inputs.OUT_OF_RANGE_TWEAK_IDX], + [False], + "Tweak exceeds the group order", + ) + # Infinity tweak + self._append_error( + my_id, + self.min_s, + self.min_s, + 0, + 0, + self.aggnonce_min, + msg, + [self.inputs.INFINITY_TWEAK_IDX], + [False], + "Plain tweak drives the tweaked threshold public key to the point at infinity", + ) + # Length mismatch between tweaks and is_xonly + self._append_error( + my_id, + self.min_s, + self.min_s, + 0, + 0, + self.aggnonce_min, + msg, + [0], + [], + "Number of tweaks does not match the number of tweak modes", + ) + # Invalid 33-byte tweak + self._append_error( + my_id, + self.min_s, + self.min_s, + 0, + 0, + self.aggnonce_min, + msg, + # index of the appended invalid 33-byte tweak (last slot of the pool) + [len(self.inputs.tweaks_pool)], + [False], + "Tweak is not a 32-byte array", + ) + + def build(self) -> dict: + self.add_valid_tests() + self.add_error_tests() + return self.group + + +def generate_tweak_vectors() -> None: + groups = [TweakGroupBuilder(cfg).build() for cfg in CONFIGS] + assign_tc_ids(groups) + write_test_vectors("tweak_vectors.json", {"test_groups": groups}) diff --git a/bip-0445/python/mypy.ini b/bip-0445/python/mypy.ini new file mode 100644 index 0000000000..08f1e086d1 --- /dev/null +++ b/bip-0445/python/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +# Include path to vendored copy of secp256k1lab, in order to +# avoid "import-not-found" errors in mypy's `--strict` mode +mypy_path = $MYPY_CONFIG_FILE_DIR/secp256k1lab/src \ No newline at end of file diff --git a/bip-0445/python/secp256k1lab/.github/workflows/main.yml b/bip-0445/python/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..fb05230b3c --- /dev/null +++ b/bip-0445/python/secp256k1lab/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Tests +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx ruff check . + mypy: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv, setup Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + - run: uvx mypy . + unittest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: python3 -m unittest diff --git a/bip-0445/python/secp256k1lab/.gitignore b/bip-0445/python/secp256k1lab/.gitignore new file mode 100644 index 0000000000..505a3b1ca2 --- /dev/null +++ b/bip-0445/python/secp256k1lab/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/bip-0445/python/secp256k1lab/.python-version b/bip-0445/python/secp256k1lab/.python-version new file mode 100644 index 0000000000..2c0733315e --- /dev/null +++ b/bip-0445/python/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/bip-0445/python/secp256k1lab/CHANGELOG.md b/bip-0445/python/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..4c756d3695 --- /dev/null +++ b/bip-0445/python/secp256k1lab/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +#### Added + - Added new methods `Scalar.from_int_nonzero_checked` and `Scalar.from_bytes_nonzero_checked` + that ensure a constructed scalar is in the range `0 < s < N` (i.e. is non-zero and within the + group order) and throw a `ValueError` otherwise. This is e.g. useful for ensuring that newly + generated secret keys or nonces are valid without having to do the non-zero check manually. + The already existing methods `Scalar.from_int_checked` and `Scalar.from_bytes_checked` error + on overflow, but not on zero, i.e. they only ensure `0 <= s < N`. + + - Added a new method `GE.from_bytes_compressed_with_infinity` to parse a compressed + public key (33 bytes) to a group element, where the all-zeros bytestring maps to the + point at infinity. This is the counterpart to the already existing serialization + method `GE.to_bytes_compressed_with_infinity`. + +## [1.0.0] - 2025-03-31 + +Initial release. diff --git a/bip-0445/python/secp256k1lab/COPYING b/bip-0445/python/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0445/python/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bip-0445/python/secp256k1lab/README.md b/bip-0445/python/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/bip-0445/python/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + +![Dependencies: None](https://img.shields.io/badge/dependencies-none-success) + +An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education. + +Features: +* Low-level secp256k1 field and group arithmetic. +* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). +* ECDH key exchange. + +WARNING: The code in this library is slow and trivially vulnerable to side channel attacks. diff --git a/bip-0445/python/secp256k1lab/pyproject.toml b/bip-0445/python/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..68b927b384 --- /dev/null +++ b/bip-0445/python/secp256k1lab/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "secp256k1lab" +version = "1.0.0" +description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education" +readme = "README.md" +authors = [ + { name = "Pieter Wuille", email = "pieter@wuille.net" }, + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +maintainers = [ + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +requires-python = ">=3.11" +license = "MIT" +license-files = ["COPYING"] +keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", +] +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0445/python/secp256k1lab/src/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0445/python/secp256k1lab/src/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/keys.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0445/python/secp256k1lab/src/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/py.typed b/bip-0445/python/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..0526878d91 --- /dev/null +++ b/bip-0445/python/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -0,0 +1,483 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +from __future__ import annotations +from typing import Self + +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE: int + + def __init__(self, a: int | Self = 0, b: int | Self = 1) -> None: + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, type(self)): + num = a._num + den = a._den + else: + assert isinstance(a, int) + num = a % self.SIZE + den = 1 + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE + else: + assert isinstance(b, int) + den = (den * b) % self.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num: int = num + self._den: int = den + + def __add__(self, a: int | Self) -> Self: + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented + + def __radd__(self, a: int) -> Self: + """Compute the sum of an integer and a field element.""" + return type(self)(a) + self + + @classmethod + def sum(cls, *es: Self) -> Self: + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) + + def __sub__(self, a: int | Self) -> Self: + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented + + def __rsub__(self, a: int) -> Self: + """Compute the difference of an integer and a field element.""" + return type(self)(a) - self + + def __mul__(self, a: int | Self) -> Self: + """Compute the product of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented + + def __rmul__(self, a: int) -> Self: + """Compute the product of an integer with a field element.""" + return type(self)(a) * self + + def __truediv__(self, a: int | Self) -> Self: + """Compute the ratio of two field elements (second may be int).""" + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented + + def __pow__(self, a: int) -> Self: + """Raise a field element to an integer power.""" + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) + + def __neg__(self) -> Self: + """Negate a field element.""" + return type(self)(-self._num, self._den) + + def __int__(self) -> int: + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE + self._den = 1 + return self._num + + def sqrt(self) -> Self | None: + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError + + def is_square(self) -> bool: + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self) -> bool: + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a: object) -> bool: + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + elif isinstance(a, int): + return (self._num - self._den * a) % self.SIZE == 0 + return False # for other types + + def to_bytes(self) -> bytes: + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @classmethod + def from_int_checked(cls, v: int) -> Self: + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v: int) -> Self: + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) + + def __str__(self) -> str: + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self) -> str: + """Get a string representation of this field element.""" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self) -> Self | None: + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + @classmethod + def from_int_nonzero_checked(cls, v: int) -> Self: + """Convert an integer to a scalar (no zero or overflow allowed).""" + if not (0 < v < cls.SIZE): + raise ValueError + return cls(v) + + @classmethod + def from_bytes_nonzero_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a scalar (BE byte order, no zero or overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_nonzero_checked(v) + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + GE objects are immutable. + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = Scalar.SIZE + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + @property + def infinity(self) -> bool: + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self) -> FE: + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self) -> FE: + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + + def __init__(self, x: int | FE | None = None, y: int | FE | None = None) -> None: + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self._infinity = True + else: + # Initialize as point on the curve (and check that it is). + assert x is not None + assert y is not None + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self._infinity = False + self._x = fx + self._y = fy + + def __add__(self, a: GE) -> GE: + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def sum(*ps: GE) -> GE: + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps: tuple[Scalar, GE]) -> GE: + """Compute a (batch) scalar group element multiplication. + + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(int(a), p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a: int | Scalar) -> GE: + """Multiply an integer or scalar with a group element.""" + if self == G: + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) + + def __neg__(self) -> GE: + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def __sub__(self, a: GE) -> GE: + """Subtract a group element from another.""" + return self + (-a) + + def __eq__(self, a: object) -> bool: + """Check if two group elements are equal.""" + if not isinstance(a, type(self)): + return False + return (self - a).infinity + + def has_even_y(self) -> bool: + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + + def to_bytes_compressed(self) -> bytes: + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_compressed_with_infinity(self) -> bytes: + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + + def to_bytes_uncompressed(self) -> bytes: + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self) -> bytes: + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x: int | FE) -> GE: + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + raise ValueError + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes_compressed(b: bytes) -> GE: + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_compressed_with_infinity(b: bytes) -> GE: + """Convert a compressed to a group element, mapping zeros to infinity.""" + if b == 33 * b"\x00": + return GE() + else: + return GE.from_bytes_compressed(b) + + @staticmethod + def from_bytes_uncompressed(b: bytes) -> GE: + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + + @staticmethod + def from_bytes(b: bytes) -> GE: + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + return GE.from_bytes_compressed(b) + else: + return GE.from_bytes_uncompressed(b) + + @staticmethod + def from_bytes_xonly(b: bytes) -> GE: + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r + + @staticmethod + def is_valid_x(x: int | FE) -> bool: + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self) -> str: + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self) -> str: + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + + def __hash__(self) -> int: + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p: GE) -> None: + self.table: list[GE] = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a: Scalar | int) -> GE: + result = GE() + a_ = int(a) + for bit in range(a_.bit_length()): + if a_ & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/bip-0445/python/secp256k1lab/src/secp256k1lab/util.py b/bip-0445/python/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0445/python/secp256k1lab/src/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() diff --git a/bip-0445/python/secp256k1lab/test/__init__.py b/bip-0445/python/secp256k1lab/test/__init__.py new file mode 100644 index 0000000000..862ed6e21c --- /dev/null +++ b/bip-0445/python/secp256k1lab/test/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path +import sys + +# Ensure secp256k1lab is found and can be imported directly +sys.path.insert(0, str(Path(__file__).parent / "../src/")) diff --git a/bip-0445/python/secp256k1lab/test/test_bip340.py b/bip-0445/python/secp256k1lab/test/test_bip340.py new file mode 100644 index 0000000000..7fafad54bd --- /dev/null +++ b/bip-0445/python/secp256k1lab/test/test_bip340.py @@ -0,0 +1,51 @@ +import csv +from pathlib import Path +from random import randbytes +import unittest + +from secp256k1lab.bip340 import pubkey_gen, schnorr_sign, schnorr_verify + + +class BIP340Tests(unittest.TestCase): + """Test schnorr signatures (BIP 340).""" + + def test_correctness(self): + seckey = randbytes(32) + pubkey_xonly = pubkey_gen(seckey) + aux_rand = randbytes(32) + message = b'this is some arbitrary message' + signature = schnorr_sign(message, seckey, aux_rand) + success = schnorr_verify(message, pubkey_xonly, signature) + self.assertTrue(success) + + def test_vectors(self): + # Test against vectors from the BIPs repository + # [https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv] + vectors_file = Path(__file__).parent / "vectors" / "bip340.csv" + with open(vectors_file, encoding='utf8') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + with self.subTest(i=int(row['index'])): + self.subtest_vectors_case(row) + + def subtest_vectors_case(self, row): + seckey = bytes.fromhex(row['secret key']) + pubkey_xonly = bytes.fromhex(row['public key']) + aux_rand = bytes.fromhex(row['aux_rand']) + msg = bytes.fromhex(row['message']) + sig = bytes.fromhex(row['signature']) + result_str = row['verification result'] + comment = row['comment'] + + result = result_str == 'TRUE' + assert result or result_str == 'FALSE' + if seckey != b'': + pubkey_xonly_actual = pubkey_gen(seckey) + self.assertEqual(pubkey_xonly.hex(), pubkey_xonly_actual.hex(), f"BIP340 test vector ({comment}): pubkey mismatch") + sig_actual = schnorr_sign(msg, seckey, aux_rand) + self.assertEqual(sig.hex(), sig_actual.hex(), f"BIP340 test vector ({comment}): sig mismatch") + result_actual = schnorr_verify(msg, pubkey_xonly, sig) + if result: + self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification failed unexpectedly") + else: + self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification succeeded unexpectedly") diff --git a/bip-0445/python/secp256k1lab/test/test_ecdh.py b/bip-0445/python/secp256k1lab/test/test_ecdh.py new file mode 100644 index 0000000000..63c9da7a1b --- /dev/null +++ b/bip-0445/python/secp256k1lab/test/test_ecdh.py @@ -0,0 +1,18 @@ +from random import randbytes +import unittest + +from secp256k1lab.ecdh import ecdh_libsecp256k1 +from secp256k1lab.keys import pubkey_gen_plain + + +class ECDHTests(unittest.TestCase): + """Test ECDH module.""" + + def test_correctness(self): + seckey_alice = randbytes(32) + pubkey_alice = pubkey_gen_plain(seckey_alice) + seckey_bob = randbytes(32) + pubkey_bob = pubkey_gen_plain(seckey_bob) + shared_secret1 = ecdh_libsecp256k1(seckey_alice, pubkey_bob) + shared_secret2 = ecdh_libsecp256k1(seckey_bob, pubkey_alice) + self.assertEqual(shared_secret1, shared_secret2) diff --git a/bip-0445/python/secp256k1lab/test/test_secp256k1.py b/bip-0445/python/secp256k1lab/test/test_secp256k1.py new file mode 100644 index 0000000000..c6aee19a0a --- /dev/null +++ b/bip-0445/python/secp256k1lab/test/test_secp256k1.py @@ -0,0 +1,180 @@ +"""Test low-level secp256k1 field and group arithmetic classes.""" +from random import randint +import unittest + +from secp256k1lab.secp256k1 import FE, G, GE, Scalar + + +class PrimeFieldTests(unittest.TestCase): + def test_fe_constructors(self): + P = FE.SIZE + random_fe_valid = randint(0, P-1) + random_fe_overflowing = randint(P, 2**256-1) + + # wrapping constructors + for init_value in [0, P-1, P, P+1, random_fe_valid, random_fe_overflowing]: + fe1 = FE(init_value) + fe2 = FE.from_int_wrapping(init_value) + fe3 = FE.from_bytes_wrapping(init_value.to_bytes(32, 'big')) + reduced_value = init_value % P + self.assertEqual(int(fe1), reduced_value) + self.assertEqual(int(fe1), int(fe2)) + self.assertEqual(int(fe2), int(fe3)) + + # checking constructors (should throw on overflow) + for valid_value in [0, P-1, random_fe_valid]: + fe1 = FE.from_int_checked(valid_value) + fe2 = FE.from_bytes_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(fe1), valid_value) + self.assertEqual(int(fe1), int(fe2)) + + for overflow_value in [P, P+1, random_fe_overflowing]: + with self.assertRaises(ValueError): + _ = FE.from_int_checked(overflow_value) + with self.assertRaises(ValueError): + _ = FE.from_bytes_checked(overflow_value.to_bytes(32, 'big')) + + def test_scalar_constructors(self): + N = Scalar.SIZE + random_scalar_valid = randint(0, N-1) + random_scalar_overflowing = randint(N, 2**256-1) + + # wrapping constructors + for init_value in [0, N-1, N, N+1, random_scalar_valid, random_scalar_overflowing]: + s1 = Scalar(init_value) + s2 = Scalar.from_int_wrapping(init_value) + s3 = Scalar.from_bytes_wrapping(init_value.to_bytes(32, 'big')) + reduced_value = init_value % N + self.assertEqual(int(s1), reduced_value) + self.assertEqual(int(s1), int(s2)) + self.assertEqual(int(s2), int(s3)) + + # checking constructors (should throw on overflow) + for valid_value in [0, N-1, random_scalar_valid]: + s1 = Scalar.from_int_checked(valid_value) + s2 = Scalar.from_bytes_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(s1), valid_value) + self.assertEqual(int(s1), int(s2)) + + for overflow_value in [N, N+1, random_scalar_overflowing]: + with self.assertRaises(ValueError): + _ = Scalar.from_int_checked(overflow_value) + with self.assertRaises(ValueError): + _ = Scalar.from_bytes_checked(overflow_value.to_bytes(32, 'big')) + + # non-zero checking constructors (should throw on zero or overflow, only for Scalar) + random_nonzero_scalar_valid = randint(1, N-1) + for valid_value in [1, N-1, random_nonzero_scalar_valid]: + s1 = Scalar.from_int_nonzero_checked(valid_value) + s2 = Scalar.from_bytes_nonzero_checked(valid_value.to_bytes(32, 'big')) + self.assertEqual(int(s1), valid_value) + self.assertEqual(int(s1), int(s2)) + + for invalid_value in [0, N, random_scalar_overflowing]: + with self.assertRaises(ValueError): + _ = Scalar.from_int_nonzero_checked(invalid_value) + with self.assertRaises(ValueError): + _ = Scalar.from_bytes_nonzero_checked(invalid_value.to_bytes(32, 'big')) + + +class GeSerializationTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.point_at_infinity = GE() + cls.group_elements_on_curve = [ + # generator point + G, + # Bitcoin genesis block public key + GE(0x678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6, + 0x49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f), + ] + # generate a few random points, to likely cover both even/odd y polarity + cls.group_elements_on_curve.extend([randint(1, Scalar.SIZE-1) * G for _ in range(8)]) + # generate x coordinates that don't have a valid point on the curve + # (note that ~50% of all x coordinates are valid, so finding one needs two loop iterations on average) + cls.x_coords_not_on_curve = [] + while len(cls.x_coords_not_on_curve) < 8: + x = randint(0, FE.SIZE-1) + if not GE.is_valid_x(x): + cls.x_coords_not_on_curve.append(x) + + cls.group_elements = [cls.point_at_infinity] + cls.group_elements_on_curve + + def test_infinity_raises(self): + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_uncompressed() + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_compressed() + with self.assertRaises(AssertionError): + _ = self.point_at_infinity.to_bytes_xonly() + + def test_not_on_curve_raises(self): + # for compressed and x-only GE deserialization, test with invalid x coordinate + for x in self.x_coords_not_on_curve: + x_bytes = x.to_bytes(32, 'big') + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed(b'\x02' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed(b'\x03' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed_with_infinity(b'\x02' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_compressed_with_infinity(b'\x03' + x_bytes) + with self.assertRaises(ValueError): + _ = GE.from_bytes_xonly(x_bytes) + + # for uncompressed GE serialization, test by invalidating either coordinate + for ge in self.group_elements_on_curve: + valid_x = ge.x + valid_y = ge.y + invalid_x = ge.x + 1 + invalid_y = ge.y + 1 + + # valid cases (if point (x,y) is on the curve, then point(x,-y) is on the curve as well) + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + valid_y.to_bytes()) + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + (-valid_y).to_bytes()) + # invalid cases (curve equation y**2 = x**3 + 7 doesn't hold) + self.assertNotEqual(invalid_y**2, valid_x**3 + 7) + with self.assertRaises(ValueError): + _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + invalid_y.to_bytes()) + self.assertNotEqual(valid_y**2, invalid_x**3 + 7) + with self.assertRaises(ValueError): + _ = GE.from_bytes_uncompressed(b'\x04' + invalid_x.to_bytes() + valid_y.to_bytes()) + + def test_affine(self): + # GE serialization and parsing round-trip (variants that only support serializing points on the curve) + for ge_orig in self.group_elements_on_curve: + # uncompressed serialization: 65 bytes, starts with 0x04 + ge_ser = ge_orig.to_bytes_uncompressed() + self.assertEqual(len(ge_ser), 65) + self.assertEqual(ge_ser[0], 0x04) + ge_deser = GE.from_bytes_uncompressed(ge_ser) + self.assertEqual(ge_deser, ge_orig) + + # compressed serialization: 33 bytes, starts with 0x02 (if y is even) or 0x03 (if y is odd) + ge_ser = ge_orig.to_bytes_compressed() + self.assertEqual(len(ge_ser), 33) + self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03) + ge_deser = GE.from_bytes_compressed(ge_ser) + self.assertEqual(ge_deser, ge_orig) + + # x-only serialization: 32 bytes + ge_ser = ge_orig.to_bytes_xonly() + self.assertEqual(len(ge_ser), 32) + ge_deser = GE.from_bytes_xonly(ge_ser) + if not ge_orig.has_even_y(): # x-only implies even y, so flip if necessary + ge_deser = -ge_deser + self.assertEqual(ge_deser, ge_orig) + + def test_affine_with_infinity(self): + # GE serialization and parsing round-trip (variants that also support serializing the point at infinity) + for ge_orig in self.group_elements: + # compressed serialization: 33 bytes, all-zeros for point at infinity + ge_ser = ge_orig.to_bytes_compressed_with_infinity() + self.assertEqual(len(ge_ser), 33) + if ge_orig.infinity: + self.assertEqual(ge_ser, b'\x00'*33) + else: + self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03) + ge_deser = GE.from_bytes_compressed_with_infinity(ge_ser) + self.assertEqual(ge_deser, ge_orig) diff --git a/bip-0445/python/secp256k1lab/test/vectors/bip340.csv b/bip-0445/python/secp256k1lab/test/vectors/bip340.csv new file mode 100644 index 0000000000..aa317a3b3d --- /dev/null +++ b/bip-0445/python/secp256k1lab/test/vectors/bip340.csv @@ -0,0 +1,20 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/bip-0445/python/tests.py b/bip-0445/python/tests.py new file mode 100755 index 0000000000..a27b25ed64 --- /dev/null +++ b/bip-0445/python/tests.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python3 + +import json +import os +import secrets +import sys +import time +from typing import List, Optional, Tuple + +from frost_ref.signing import ( + InvalidContributionError, + PlainPk, + SessionContext, + SignersContext, + XonlyPk, + deterministic_sign, + get_xonly_pk, + thresh_pubkey_and_tweak, + nonce_agg, + nonce_gen, + nonce_gen_internal, + partial_sig_agg, + partial_sig_verify, + partial_sig_verify_internal, + sign, +) +from secp256k1lab.keys import pubkey_gen_plain +from secp256k1lab.secp256k1 import G +from secp256k1lab.bip340 import schnorr_verify +from secp256k1lab.util import int_from_bytes +from trusted_dealer import trusted_dealer_keygen + + +def fromhex_all(hex_values): + return [bytes.fromhex(value) for value in hex_values] + + +# Check that calling `try_fn` raises a `exception`. If `exception` is raised, +# examine it with `except_fn`. +def assert_raises(exception, try_fn, except_fn): + raised = False + try: + try_fn() + except exception as e: + raised = True + assert except_fn(e) + except BaseException: + raise AssertionError("Wrong exception raised in a test.") + if not raised: + raise AssertionError( + "Exception was _not_ raised in a test where it was required." + ) + + +def get_error_details(test_case): + error = test_case["error"] + if error["type"] == "InvalidContributionError": + exception = InvalidContributionError + if "contrib" in error: + + def except_fn(e): + return ( + e.signer_index == error["signer_index"] + and e.contrib == error["contrib"] + ) + else: + + def except_fn(e): + return e.signer_index == error["signer_index"] + elif error["type"] == "ValueError": + exception = ValueError + + def except_fn(e): + return str(e) == error["message"] + else: + raise RuntimeError(f"Invalid error type: {error['type']}") + return exception, except_fn + + +def generate_frost_keys( + n: int, t: int +) -> Tuple[PlainPk, List[int], List[bytes], List[PlainPk]]: + if not (2 <= t <= n): + raise ValueError("values must satisfy: 2 <= t <= n") + + thresh_pk, secshares, pubshares = trusted_dealer_keygen( + secrets.token_bytes(32), n, t + ) + + # IDs are 0-indexed: the index in the list IS the participant ID + assert len(secshares) == n + identifiers = list(range(len(secshares))) + + return (thresh_pk, identifiers, secshares, pubshares) + + +def test_nonce_gen_vectors(): + with open(os.path.join(sys.path[0], "vectors", "nonce_gen_vectors.json")) as f: + test_data = json.load(f) + + for test_case in test_data["valid_tests"]: + + def get_value(key) -> bytes: + return bytes.fromhex(test_case[key]) + + def get_value_maybe(key) -> Optional[bytes]: + if test_case[key] is not None: + return get_value(key) + else: + return None + + rand_ = get_value("rand_") + secshare = get_value_maybe("secshare") + pubshare = get_value_maybe("pubshare") + if pubshare is not None: + pubshare = PlainPk(pubshare) + thresh_pk = get_value_maybe("thresh_pk") + if thresh_pk is not None: + thresh_pk = XonlyPk(thresh_pk) + msg = get_value_maybe("msg") + extra_in = get_value_maybe("extra_in") + expected = test_case["expected"] + + assert nonce_gen_internal( + rand_, secshare, pubshare, thresh_pk, msg, extra_in + ) == (bytes.fromhex(expected[0]), bytes.fromhex(expected[1])) + + +def test_nonce_agg_vectors(): + with open(os.path.join(sys.path[0], "vectors", "nonce_agg_vectors.json")) as f: + test_data = json.load(f) + + pubnonces_list = fromhex_all(test_data["pubnonces"]) + valid_test_cases = test_data["valid_tests"] + error_test_cases = test_data["error_tests"] + + for test_case in valid_test_cases: + pubnonces = [pubnonces_list[i] for i in test_case["pubnonce_indices"]] + expected_aggnonce = bytes.fromhex(test_case["expected"]) + assert nonce_agg(pubnonces) == expected_aggnonce + + for test_case in error_test_cases: + exception, except_fn = get_error_details(test_case) + pubnonces = [pubnonces_list[i] for i in test_case["pubnonce_indices"]] + assert_raises(exception, lambda: nonce_agg(pubnonces), except_fn) + + +def test_sign_verify_vectors(): + with open(os.path.join(sys.path[0], "vectors", "sign_verify_vectors.json")) as f: + test_data = json.load(f) + + for group in test_data["test_groups"]: + n = group["n"] + t = group["t"] + thresh_pk = bytes.fromhex(group["thresh_pk"]) + pubshares = fromhex_all(group["pubshares"]) + pubnonces = fromhex_all(group["pubnonces"]) + secshares = fromhex_all(group["secshares"]) + secnonces = fromhex_all(group["secnonces"]) + for i in range(n): + assert pubshares[i] == PlainPk(pubkey_gen_plain(secshares[i])) + k_1 = int_from_bytes(secnonces[0][0:32]) + k_2 = int_from_bytes(secnonces[0][32:64]) + assert not (k_1 * G).infinity and not (k_2 * G).infinity + assert ( + pubnonces[0] + == (k_1 * G).to_bytes_compressed() + (k_2 * G).to_bytes_compressed() + ) + + for tc in group["valid_tests"]: + ids_tmp = tc["ids"] + pubshares_tmp = [PlainPk(pubshares[i]) for i in tc["pubshare_indices"]] + pubnonces_tmp = [pubnonces[i] for i in tc["pubnonce_indices"]] + aggnonce_tmp = bytes.fromhex(tc["aggnonce"]) + # Make sure that pubnonces and aggnonce in the test vector are consistent + assert nonce_agg(pubnonces_tmp) == aggnonce_tmp + msg = bytes.fromhex(tc["msg"]) + my_id = tc["my_id"] + signer_index = ids_tmp.index(my_id) + secshare = secshares[tc["secshare_index"]] + expected = bytes.fromhex(tc["expected"]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext(aggnonce_tmp, signers_tmp, [], [], msg) + # WARNING: An actual implementation should _not_ copy the secnonce. + # Reusing the secnonce, as we do here for testing purposes, can leak the + # secret key. + secnonce_tmp = bytearray(secnonces[tc["secnonce_index"]]) + assert sign(secnonce_tmp, secshare, my_id, session_ctx) == expected + assert partial_sig_verify( + expected, pubnonces_tmp, signers_tmp, [], [], msg, signer_index + ) + + for tc in group["sign_error_tests"]: + exception, except_fn = get_error_details(tc) + ids_tmp = tc["ids"] + pubshares_tmp = [PlainPk(pubshares[i]) for i in tc["pubshare_indices"]] + aggnonce_tmp = bytes.fromhex(tc["aggnonce"]) + msg = bytes.fromhex(tc["msg"]) + my_id = tc["my_id"] + secnonce_tmp = bytearray(secnonces[tc["secnonce_index"]]) + secshare_tmp = secshares[tc["secshare_index"]] + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext(aggnonce_tmp, signers_tmp, [], [], msg) + assert_raises( + exception, + lambda: sign(secnonce_tmp, secshare_tmp, my_id, session_ctx), + except_fn, + ) + + for tc in group["verify_fail_tests"]: + psig = bytes.fromhex(tc["psig"]) + ids_tmp = tc["ids"] + pubshares_tmp = [PlainPk(pubshares[i]) for i in tc["pubshare_indices"]] + pubnonces_tmp = [pubnonces[i] for i in tc["pubnonce_indices"]] + msg = bytes.fromhex(tc["msg"]) + signer_index = tc["signer_index"] + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + assert not partial_sig_verify( + psig, + pubnonces_tmp, + signers_tmp, + [], + [], + msg, + signer_index, + ) + + for tc in group["verify_error_tests"]: + exception, except_fn = get_error_details(tc) + + psig = bytes.fromhex(tc["psig"]) + ids_tmp = tc["ids"] + pubshares_tmp = [PlainPk(pubshares[i]) for i in tc["pubshare_indices"]] + pubnonces_tmp = [pubnonces[i] for i in tc["pubnonce_indices"]] + msg = bytes.fromhex(tc["msg"]) + signer_index = tc["signer_index"] + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + assert_raises( + exception, + lambda: partial_sig_verify( + psig, pubnonces_tmp, signers_tmp, [], [], msg, signer_index + ), + except_fn, + ) + + +def test_tweak_vectors(): + with open(os.path.join(sys.path[0], "vectors", "tweak_vectors.json")) as f: + test_data = json.load(f) + + for group in test_data["test_groups"]: + n = group["n"] + t = group["t"] + thresh_pk = bytes.fromhex(group["thresh_pk"]) + pubshares = fromhex_all(group["pubshares"]) + pubnonces = fromhex_all(group["pubnonces"]) + secshares = fromhex_all(group["secshares"]) + secnonces = fromhex_all(group["secnonces"]) + tweaks = fromhex_all(group["tweaks"]) + for i in range(n): + assert pubshares[i] == PlainPk(pubkey_gen_plain(secshares[i])) + + for test_case in group["valid_tests"]: + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + pubnonces_tmp = [pubnonces[i] for i in test_case["pubnonce_indices"]] + aggnonce_tmp = bytes.fromhex(test_case["aggnonce"]) + # Make sure that pubnonces and aggnonce in the test vector are consistent + assert nonce_agg(pubnonces_tmp) == aggnonce_tmp + msg = bytes.fromhex(test_case["msg"]) + tweaks_tmp = [tweaks[i] for i in test_case["tweak_indices"]] + tweak_modes_tmp = test_case["is_xonly"] + my_id = test_case["my_id"] + signer_index = ids_tmp.index(my_id) + secshare = secshares[test_case["secshare_index"]] + # WARNING: An actual implementation should _not_ copy the secnonce. + # Reusing the secnonce, as we do here for testing purposes, can leak the + # secret key. + secnonce = bytearray(secnonces[test_case["secnonce_index"]]) + expected = bytes.fromhex(test_case["expected"]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext( + aggnonce_tmp, signers_tmp, tweaks_tmp, tweak_modes_tmp, msg + ) + assert sign(secnonce, secshare, my_id, session_ctx) == expected + assert partial_sig_verify( + expected, + pubnonces_tmp, + signers_tmp, + tweaks_tmp, + tweak_modes_tmp, + msg, + signer_index, + ) + + for test_case in group["error_tests"]: + exception, except_fn = get_error_details(test_case) + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + aggnonce_tmp = bytes.fromhex(test_case["aggnonce"]) + msg = bytes.fromhex(test_case["msg"]) + tweaks_tmp = [tweaks[i] for i in test_case["tweak_indices"]] + tweak_modes_tmp = test_case["is_xonly"] + my_id = test_case["my_id"] + secshare = secshares[test_case["secshare_index"]] + secnonce = bytearray(secnonces[test_case["secnonce_index"]]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext( + aggnonce_tmp, signers_tmp, tweaks_tmp, tweak_modes_tmp, msg + ) + assert_raises( + exception, + lambda: sign(secnonce, secshare, my_id, session_ctx), + except_fn, + ) + + +def test_det_sign_vectors(): + with open(os.path.join(sys.path[0], "vectors", "det_sign_vectors.json")) as f: + test_data = json.load(f) + + for group in test_data["test_groups"]: + n = group["n"] + t = group["t"] + thresh_pk = bytes.fromhex(group["thresh_pk"]) + pubshares = fromhex_all(group["pubshares"]) + secshares = fromhex_all(group["secshares"]) + for i in range(n): + assert pubshares[i] == PlainPk(pubkey_gen_plain(secshares[i])) + + for test_case in group["valid_tests"]: + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + secshare = secshares[test_case["secshare_index"]] + aggothernonce = ( + bytes.fromhex(test_case["aggothernonce"]) + if test_case["aggothernonce"] is not None + else None + ) + tweaks = fromhex_all(test_case["tweaks"]) + is_xonly = test_case["is_xonly"] + msg = bytes.fromhex(test_case["msg"]) + my_id = test_case["my_id"] + signer_index = ids_tmp.index(my_id) + rand = ( + bytes.fromhex(test_case["rand"]) + if test_case["rand"] is not None + else None + ) + expected = fromhex_all(test_case["expected"]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + pubnonce, psig = deterministic_sign( + secshare, + my_id, + aggothernonce, + signers_tmp, + tweaks, + is_xonly, + msg, + rand, + ) + assert pubnonce == expected[0] + assert psig == expected[1] + + # For a sole signer, aggothernonce is None and the aggnonce equals + # the signer's own pubnonce; skip the multi-party aggregation path. + if aggothernonce is not None: + aggnonce_tmp = nonce_agg([pubnonce, aggothernonce]) + else: + aggnonce_tmp = pubnonce + session_ctx = SessionContext( + aggnonce_tmp, signers_tmp, tweaks, is_xonly, msg + ) + assert partial_sig_verify_internal( + psig, my_id, pubnonce, pubshares_tmp[signer_index], session_ctx + ) + + for test_case in group["error_tests"]: + exception, except_fn = get_error_details(test_case) + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + secshare = secshares[test_case["secshare_index"]] + aggothernonce = ( + bytes.fromhex(test_case["aggothernonce"]) + if test_case["aggothernonce"] is not None + else None + ) + tweaks = fromhex_all(test_case["tweaks"]) + is_xonly = test_case["is_xonly"] + msg = bytes.fromhex(test_case["msg"]) + my_id = test_case["my_id"] + rand = ( + bytes.fromhex(test_case["rand"]) + if test_case["rand"] is not None + else None + ) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + assert_raises( + exception, + lambda: deterministic_sign( + secshare, + my_id, + aggothernonce, + signers_tmp, + tweaks, + is_xonly, + msg, + rand, + ), + except_fn, + ) + + +def test_sig_agg_vectors(): + with open(os.path.join(sys.path[0], "vectors", "sig_agg_vectors.json")) as f: + test_data = json.load(f) + + for group in test_data["test_groups"]: + n = group["n"] + t = group["t"] + thresh_pk = bytes.fromhex(group["thresh_pk"]) + pubshares = fromhex_all(group["pubshares"]) + tweaks = fromhex_all(group["tweaks"]) + + for test_case in group["valid_tests"]: + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + aggnonce_tmp = bytes.fromhex(test_case["aggnonce"]) + tweaks_tmp = [tweaks[i] for i in test_case["tweak_indices"]] + tweak_modes_tmp = test_case["is_xonly"] + psigs_tmp = fromhex_all(test_case["psigs"]) + msg = bytes.fromhex(test_case["msg"]) + expected = bytes.fromhex(test_case["expected"]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext( + aggnonce_tmp, signers_tmp, tweaks_tmp, tweak_modes_tmp, msg + ) + bip340sig = partial_sig_agg(psigs_tmp, session_ctx) + assert bip340sig == expected + tweaked_thresh_pk = get_xonly_pk( + thresh_pubkey_and_tweak(thresh_pk, tweaks_tmp, tweak_modes_tmp) + ) + assert schnorr_verify(msg, tweaked_thresh_pk, bip340sig) + + for test_case in group["error_tests"]: + exception, except_fn = get_error_details(test_case) + ids_tmp = test_case["ids"] + pubshares_tmp = [ + PlainPk(pubshares[i]) for i in test_case["pubshare_indices"] + ] + aggnonce_tmp = bytes.fromhex(test_case["aggnonce"]) + tweaks_tmp = [tweaks[i] for i in test_case["tweak_indices"]] + tweak_modes_tmp = test_case["is_xonly"] + psigs_tmp = fromhex_all(test_case["psigs"]) + msg = bytes.fromhex(test_case["msg"]) + + signers_tmp = SignersContext(n, t, ids_tmp, pubshares_tmp, thresh_pk) + session_ctx = SessionContext( + aggnonce_tmp, signers_tmp, tweaks_tmp, tweak_modes_tmp, msg + ) + assert_raises( + exception, + lambda: partial_sig_agg(psigs_tmp, session_ctx), + except_fn, + ) + + +def test_sign_and_verify_random(iterations: int) -> None: + for itr in range(iterations): + secure_rng = secrets.SystemRandom() + # randomly choose a number: 2 <= number <= 10 + n = secure_rng.randrange(2, 11) + # randomly choose a number: 2 <= number <= n + t = secure_rng.randrange(2, n + 1) + + thresh_pk, ids, secshares, pubshares = generate_frost_keys(n, t) + assert len(ids) == len(secshares) == len(pubshares) == n + + # randomly choose the signer set, with len: t <= len <= n + signer_count = secure_rng.randrange(t, n + 1) + signer_indices = secure_rng.sample(range(n), signer_count) + assert ( + len(set(signer_indices)) == signer_count + ) # signer set must not contain duplicate ids + + signer_ids = [ids[i] for i in signer_indices] + signer_pubshares = [pubshares[i] for i in signer_indices] + # NOTE: secret values MUST NEVER BE COPIED!!! + # we do it here to improve the code readability + signer_secshares = [secshares[i] for i in signer_indices] + + signers_ctx = SignersContext(n, t, signer_ids, signer_pubshares, thresh_pk) + + # In this example, the message and threshold pubkey are known + # before nonce generation, so they can be passed into the nonce + # generation function as a defense-in-depth measure to protect + # against nonce reuse. + # + # If these values are not known when nonce_gen is called, empty + # byte arrays can be passed in for the corresponding arguments + # instead. + msg = secrets.token_bytes(32) + v = secrets.randbelow(4) + tweaks = [secrets.token_bytes(32) for _ in range(v)] + tweak_modes = [secrets.choice([False, True]) for _ in range(v)] + tweaked_thresh_pk = get_xonly_pk( + thresh_pubkey_and_tweak(thresh_pk, tweaks, tweak_modes) + ) + + signer_secnonces = [] + signer_pubnonces = [] + for i in range(signer_count - 1): + # Use a clock for extra_in + timestamp = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + secnonce_i, pubnonce_i = nonce_gen( + signer_secshares[i], + signer_pubshares[i], + tweaked_thresh_pk, + msg, + timestamp.to_bytes(8, "big"), + ) + signer_secnonces.append(secnonce_i) + signer_pubnonces.append(pubnonce_i) + + # On even iterations use regular signing algorithm for the final signer, + # otherwise use deterministic signing algorithm + if itr % 2 == 0: + timestamp = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + secnonce_final, pubnonce_final = nonce_gen( + signer_secshares[-1], + signer_pubshares[-1], + tweaked_thresh_pk, + msg, + timestamp.to_bytes(8, "big"), + ) + signer_secnonces.append(secnonce_final) + else: + aggothernonce = nonce_agg(signer_pubnonces) + rand = secrets.token_bytes(32) + pubnonce_final, psig_final = deterministic_sign( + signer_secshares[-1], + signer_ids[-1], + aggothernonce, + signers_ctx, + tweaks, + tweak_modes, + msg, + rand, + ) + + signer_pubnonces.append(pubnonce_final) + aggnonce = nonce_agg(signer_pubnonces) + session_ctx = SessionContext(aggnonce, signers_ctx, tweaks, tweak_modes, msg) + + signer_psigs = [] + for i in range(signer_count): + if itr % 2 != 0 and i == signer_count - 1: + psig_i = psig_final # last signer would have already deterministically signed + else: + psig_i = sign( + signer_secnonces[i], signer_secshares[i], signer_ids[i], session_ctx + ) + assert partial_sig_verify( + psig_i, + signer_pubnonces, + signers_ctx, + tweaks, + tweak_modes, + msg, + i, + ) + signer_psigs.append(psig_i) + + # An exception is thrown if secnonce is accidentally reused + assert_raises( + ValueError, + lambda: sign( + signer_secnonces[0], signer_secshares[0], signer_ids[0], session_ctx + ), + lambda e: True, + ) + + # Wrong signer index + assert not partial_sig_verify( + signer_psigs[0], + signer_pubnonces, + signers_ctx, + tweaks, + tweak_modes, + msg, + 1, + ) + # Wrong message + assert not partial_sig_verify( + signer_psigs[0], + signer_pubnonces, + signers_ctx, + tweaks, + tweak_modes, + secrets.token_bytes(32), + 0, + ) + + bip340sig = partial_sig_agg(signer_psigs, session_ctx) + assert schnorr_verify(msg, tweaked_thresh_pk, bip340sig) + + +def run_test(test_name, test_func): + max_len = 30 + test_name = test_name.ljust(max_len, ".") + print(f"Running {test_name}...", end="", flush=True) + try: + test_func() + print("Passed!") + except Exception as e: + print(f"Failed :'(\nError: {e}") + + +if __name__ == "__main__": + run_test("test_nonce_gen_vectors", test_nonce_gen_vectors) + run_test("test_nonce_agg_vectors", test_nonce_agg_vectors) + run_test("test_sign_verify_vectors", test_sign_verify_vectors) + run_test("test_tweak_vectors", test_tweak_vectors) + run_test("test_det_sign_vectors", test_det_sign_vectors) + run_test("test_sig_agg_vectors", test_sig_agg_vectors) + run_test("test_sign_and_verify_random", lambda: test_sign_and_verify_random(6)) diff --git a/bip-0445/python/tests.sh b/bip-0445/python/tests.sh new file mode 100755 index 0000000000..db15516100 --- /dev/null +++ b/bip-0445/python/tests.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e + +check_availability() { + command -v "$1" > /dev/null 2>&1 || { + echo >&2 "$1 is required but it's not installed. Aborting."; + exit 1; + } +} + +check_availability mypy +check_availability ruff + +cd "$(dirname "$0")" + +# Keep going if a linter fails +ruff check --quiet || true +ruff format --diff --quiet || true +mypy --no-error-summary . || true +# Be more strict in the reference code +mypy --no-error-summary --strict --untyped-calls-exclude=secp256k1lab -p frost_ref --follow-imports=silent || true + +./gen_vectors.py +./tests.py diff --git a/bip-0445/python/trusted_dealer.py b/bip-0445/python/trusted_dealer.py new file mode 100644 index 0000000000..da26c56b15 --- /dev/null +++ b/bip-0445/python/trusted_dealer.py @@ -0,0 +1,170 @@ +# TODO: remove this file, and use trusted dealer BIP's reference code instead, after it gets published. + +# WARNING: Do not use this as an implementation reference. This trusted dealer is +# only used to generate the FROST keys needed for signing in the test vectors, and +# it is insecure (see secp256k1lab). Do not use it in production. + +# Implementation of the Trusted Dealer Key Generation approach for FROST mentioned +# in https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ (Appendix D). +# +# It's worth noting that this isn't the only compatible method (with BIP FROST Signing), +# there are alternative key generation methods available, such as BIP-FROST-DKG: +# https://github.com/BlockstreamResearch/bip-frost-dkg + +from typing import Tuple, List +import unittest +import secrets + +from secp256k1lab.secp256k1 import G, GE, Scalar +from secp256k1lab.util import tagged_hash +from frost_ref.signing import derive_interpolating_value +from frost_ref import PlainPk + + +COEFF_DERIVATION_TAG = "BIP0445/trusted/keygen" + + +# evaluates poly using Horner's method, assuming coeff[0] corresponds +# to the coefficient of highest degree term +def polynomial_evaluate(coeffs: List[Scalar], x: Scalar) -> Scalar: + res = Scalar(0) + for coeff in coeffs: + res = res * x + coeff + return res + + +def secret_share_combine(shares: List[Scalar], ids: List[int]) -> Scalar: + assert len(shares) == len(ids) + secret = Scalar(0) + for share, my_id in zip(shares, ids): + lam = derive_interpolating_value(ids, my_id) + secret += share * lam + return secret + + +def secret_share_shard(secret: Scalar, coeffs: List[Scalar], n: int) -> List[Scalar]: + coeffs = coeffs + [secret] + + secshares = [] + # ids are 0-indexed (0..n-1), but polynomial is evaluated at x = id + 1 + # because p(0) = secret + for i in range(n): + x_i = Scalar(i + 1) + y_i = polynomial_evaluate(coeffs, x_i) + assert y_i != 0 + secshares.append(y_i) + return secshares + + +def trusted_dealer_keygen( + thresh_sk_: bytes, n: int, t: int +) -> Tuple[PlainPk, List[bytes], List[PlainPk]]: + assert 1 <= t <= n + + thresh_sk = Scalar.from_bytes_nonzero_checked(thresh_sk_) + # Key generation protocols are allowed to generate plain public keys (i.e., non-xonly) + thresh_pk_ = thresh_sk * G + assert not thresh_pk_.infinity + thresh_pk = PlainPk(thresh_pk_.to_bytes_compressed()) + + # Derive coefficient i deterministically from the threshold secret and the + # index, so the same input always yields the same shares. + coeffs = [ + Scalar.from_bytes_nonzero_checked( + tagged_hash(COEFF_DERIVATION_TAG, thresh_sk_ + i.to_bytes(4, "big")) + ) + for i in range(1, t) + ] + + secshares_ = secret_share_shard(thresh_sk, coeffs, n) + secshares = [x.to_bytes() for x in secshares_] + + pubshares_ = [x * G for x in secshares_] + pubshares = [PlainPk(X.to_bytes_compressed()) for X in pubshares_] + + return (thresh_pk, secshares, pubshares) + + +# Test vector from RFC draft. +# section F.5 of https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ +class Tests(unittest.TestCase): + def setUp(self) -> None: + self.n = 3 + self.t = 2 + self.poly = [ + Scalar(0xFBF85EADAE3058EA14F19148BB72B45E4399C0B16028ACAF0395C9B03C823579), + Scalar(0x0D004150D27C3BF2A42F312683D35FAC7394B1E9E318249C1BFE7F0795A83114), + ] + # id[i] = i + 1, where i is the index in this list + self.secshares = [ + Scalar(0x08F89FFE80AC94DCB920C26F3F46140BFC7F95B493F8310F5FC1EA2B01F4254C), + Scalar(0x04F0FEAC2EDCEDC6CE1253B7FAB8C86B856A797F44D83D82A385554E6E401984), + Scalar(0x00E95D59DD0D46B0E303E500B62B7CCB0E555D49F5B849F5E748C071DA8C0DBC), + ] + self.secret = 0x0D004150D27C3BF2A42F312683D35FAC7394B1E9E318249C1BFE7F0795A83114 + + def test_polynomial_evaluate(self) -> None: + coeffs = self.poly.copy() + expected_secret = self.secret + + self.assertEqual(int(polynomial_evaluate(coeffs, Scalar(0))), expected_secret) + + def test_secret_share_combine(self) -> None: + secshares = self.secshares.copy() + expected_secret = self.secret + + # ids 0 and 1 + self.assertEqual( + secret_share_combine([secshares[0], secshares[1]], [0, 1]), expected_secret + ) + # ids 1 and 2 + self.assertEqual( + secret_share_combine([secshares[1], secshares[2]], [1, 2]), expected_secret + ) + # ids 0 and 2 + self.assertEqual( + secret_share_combine([secshares[0], secshares[2]], [0, 2]), expected_secret + ) + # all ids + self.assertEqual(secret_share_combine(secshares, [0, 1, 2]), expected_secret) + + def test_trusted_dealer_keygen(self) -> None: + thresh_sk_ = secrets.token_bytes(32) + n = 5 + t = 3 + thresh_pk_, secshares_, pubshares_ = trusted_dealer_keygen(thresh_sk_, n, t) + + thresh_sk = Scalar.from_bytes_nonzero_checked(thresh_sk_) + thresh_pk = GE.from_bytes_compressed(thresh_pk_) + secshares = [Scalar.from_bytes_nonzero_checked(s) for s in secshares_] + pubshares = [GE.from_bytes_compressed(p) for p in pubshares_] + + self.assertEqual(thresh_pk, thresh_sk * G) + + self.assertEqual(secret_share_combine(secshares, list(range(n))), thresh_sk) + self.assertEqual(len(secshares), n) + self.assertEqual(len(pubshares), n) + for i in range(len(pubshares)): + with self.subTest(i=i): + self.assertEqual(pubshares[i], secshares[i] * G) + + def test_trusted_dealer_keygen_deterministic(self) -> None: + thresh_sk_ = secrets.token_bytes(32) + first = trusted_dealer_keygen(thresh_sk_, 5, 3) + second = trusted_dealer_keygen(thresh_sk_, 5, 3) + self.assertEqual(first, second) + + def test_trusted_dealer_keygen_threshold_one(self) -> None: + thresh_sk_ = secrets.token_bytes(32) + thresh_pk_, secshares_, pubshares_ = trusted_dealer_keygen(thresh_sk_, 3, 1) + + # Degree-0 polynomial: every share equals the threshold secret, and every + # public share equals the threshold public key. + for secshare in secshares_: + self.assertEqual(secshare, thresh_sk_) + for pubshare in pubshares_: + self.assertEqual(pubshare, thresh_pk_) + + +if __name__ == "__main__": + unittest.main() diff --git a/bip-0445/python/vectors/det_sign_vectors.json b/bip-0445/python/vectors/det_sign_vectors.json new file mode 100644 index 0000000000..c08589cc8a --- /dev/null +++ b/bip-0445/python/vectors/det_sign_vectors.json @@ -0,0 +1,1505 @@ +{ + "test_groups": [ + { + "tg_id": "2of3", + "t": 2, + "n": 3, + "thresh_pk": "02D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "pubshares": [ + "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "0284DC4AB2CB78A621EB87FA1F14BCE2B725AFEAAC981ADCBAFF5CC2D417D2A63A", + "036441EC2D4C1266201CD89B69549A2F5B2188612A0D434153E625FB38173DD509", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secshares": [ + "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "5A7F9BD41F4B544664C54D777D43303CB5302434F9903B9B552C4E552BF02201", + "61BB07FE8123B9EC255CA3FC27AEF64D5D260A6FC73D7F15B63DBE12EC5DEA07", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 1, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "0230126A863D62A963148BAE7A4AA3736694336F12C868A8FE936D9102665F7C95020CC7C3170069AE05A8F5A4F036384C6549173F2B1782C35DB71EA2CC7193B70B", + "A76ED9607E048ED3FE182CAF092A853A7DFEC3B720BB0209251898D9FF97D7CE" + ] + }, + { + "tc_id": 2, + "comment": "Reordering the signer set leaves the deterministic output unchanged, because the identifiers are sorted before they are bound into the nonce derivation and the binding value", + "my_id": 0, + "ids": [1, 0], + "pubshare_indices": [1, 0], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "0230126A863D62A963148BAE7A4AA3736694336F12C868A8FE936D9102665F7C95020CC7C3170069AE05A8F5A4F036384C6549173F2B1782C35DB71EA2CC7193B70B", + "A76ED9607E048ED3FE182CAF092A853A7DFEC3B720BB0209251898D9FF97D7CE" + ] + }, + { + "tc_id": 3, + "comment": "A different threshold subset gives a different deterministic nonce, since the signer set is bound into the nonce derivation", + "my_id": 0, + "ids": [0, 2], + "pubshare_indices": [0, 2], + "secshare_index": 0, + "aggothernonce": "03A120B35316945A5EB22B309A9088EDEDFCF45C6CED2DDF153661CB7666031B86034A04D6C34C51F81BFD9ADAC20906DFCC7798210872020BF2AD0F52887D602623", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "0302BA55CB01DD643AEA065AAF002F1A29D38901E21FFDF0E8EA88F6427953449502DDAD2A68B7888B5D7385C31DC54C46FAA9CB1FBFD8268ABB59F9A4FEB70CB99E", + "2BAE4CD0CB79E2C797F9943854595E4514759B750BEEB1EABDC10486DFC72B00" + ] + }, + { + "tc_id": 4, + "comment": "Auxiliary randomness omitted (null), which is not equivalent to all-zeros randomness", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "02005BA71E0F89156746499B392A8F61969E2C51FBCB0231D104D42192AFDBCC740249789DED7317A415CD0E5CA7A51B292FE670F527E01DFBDDC7898DD1977224F3", + "rand": null, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03E5392567725332418CC6788C5BAE508B5F607FD7DCA05B95750FC2122DA20F9D0322D297FA579BFE64EE8CD94FA82AE8E62A5BBAFC815CA28AD06C6065EE570102", + "48C088B4B167BC8C784E4DCF7DD5601D92DC830647460029DDE82A9213AF137E" + ] + }, + { + "tc_id": 5, + "comment": "Auxiliary randomness is all ones, distinct from the all-zeros and omitted cases", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "02DB947F296BBE294CCE3117CD3DBBFB8DB2D3B3071F7598B42D12C04DF8D282F803BA1E9D78BE285B94459C182D3010E949C8FEDBB1209E3DA966D62CE504FF79CC", + "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "039FBBFD381C1EE8AC229245BB926419752471EFE3D8D030228103108AA478A8090265B931D2E56D12AA07AC7C931011E8F43E7E6BCE7FE32F3F850644495E3D6E9A", + "BCF6DF8D90C2645C76629DE151951E616A8CE77C504662B597CECCC47381D1F3" + ] + }, + { + "tc_id": 6, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "aggothernonce": "031A74BE0895A29513F40F10959B5D652547FDB7C3C0C4B7D27584346A87F25A9A03B8C237A09B794F0F3CDA7019E494C4C817F66D8AAE97D89F422E8EE9ADD50DE1", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02AC4B71D82097718EB6DDED855A831CB52B419067AFD0E829BD546A16D14F59ED028CEEBB228965F6E26BFF8AE9C906DC0B96AE85DA86FD559A0406A3BDD19EB7C0", + "DF3B8EC16712D7381C804E2CA0147C333DFE1A24116208E730B1272CECE97166" + ] + }, + { + "tc_id": 7, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "03596BB9814AB363F9938C47690ADA9489C0A0F9B97229D608B119A147B56073E80355CFC4E54291440B07C01E54273611662DC9E19324639D1C8878F5AA9C5D59E6", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "", + "tweaks": [], + "is_xonly": [], + "expected": [ + "035BF0FB165A252B776B84163CD36828970D8EB4F46241FE5B3E929304EF4FA46803076933C171BCEAD292B300F7579F1E5AEDA4942584A313694F3C397282A5B565", + "4FA3712E1EF9250A218100E2250E19F10958EA892732B8B3ADDD57258FCEFB8F" + ] + }, + { + "tc_id": 8, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "034A8CB75FAFF9B259ACEBCE97B91AA9DF3918F82DCF3AC7BB42737906B885CF7F02AA4F2AFADFD05439AA9B158342228CC6711C39E4050B1F50A00F94AA6E7CC820", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "tweaks": [], + "is_xonly": [], + "expected": [ + "034AF4733BC8B32CC4484387D636B27E7A0DDBE41114101038742BF66B17537E9003E70FA2884FCDA8122ECDC1780D776C49914AA071158F7003C0F061BD146FD8AB", + "B76A2533F13835471EB187A9DE47D335DB849E2006040A809B4F614548A242D5" + ] + }, + { + "tc_id": 9, + "comment": "Single x-only tweak applied", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" + ], + "is_xonly": [true], + "expected": [ + "038B82907722958F9384B8D5FA3C29B33959C30A772A5936F0F36FB0E522B74E0C0366BB9C3721C3DAB7A653C77188425E23F7EF29ADC128FC2355EFA6CC591E91CF", + "9AFDEB1ED0D0F64CC5B4D9DE93809E2259D006064D2C0DE1B2754D309FAEF828" + ] + } + ], + "error_tests": [ + { + "tc_id": 10, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 2, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "02DF47C4DC15F425FD450E420772F63C510548E012E3FE5EC420C7E312EB05C4FA03737C76985CBF3E8D551ED1957C6B2E717E4AABB8FF0AFF52A164B4560438A914", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 11, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "aggothernonce": "03CD1C89D40FD14192738E39F310C88D32D2A6250177E1E574DFEA2CCFF3876CF30268B2F82FB6189408F1789ADC08CE92744145152ECC54DDD9B8F94EF654D8C19A", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 12, + "comment": "Signer's public share is not in the public share list", + "my_id": 1, + "ids": [1, 2], + "pubshare_indices": [1, 2], + "secshare_index": 0, + "aggothernonce": "03A120B35316945A5EB22B309A9088EDEDFCF45C6CED2DDF153661CB7666031B86034A04D6C34C51F81BFD9ADAC20906DFCC7798210872020BF2AD0F52887D602623", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's pubshare must be included in the list of pubshares." + } + }, + { + "tc_id": 13, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 3], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 14, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [3, 1], + "pubshare_indices": [0, 1], + "secshare_index": 1, + "aggothernonce": "033F6252CADE1AB642BC9BF9D3CC15277A873D6CF56549F44D35B50FE15C00C24E02212B5210943685530220501404DF3BD891C264E4DEF990F33787EA6630BC2AFC", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 15, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [1, 0], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 16, + "comment": "Aggregate of the other signers' nonces is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 17, + "comment": "Aggregate of the other signers' nonces is invalid: first half is all zeros", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 18, + "comment": "Aggregate of the other signers' nonces is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC020000000000000000000000000000000000000000000000000000000000000009", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 19, + "comment": "Aggregate of the other signers' nonces is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 20, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 21, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 22, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 3, + "aggothernonce": "03B5623DAC86C61452568A3351C9BF29E4B9689338D2A96E0990306C9FFE6A640A034672C929A954E04F109C90EC415790D8463C8A43A84ABE39CC302E07299D95E0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ] + }, + { + "tg_id": "1of3", + "t": 1, + "n": 3, + "thresh_pk": "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "pubshares": [ + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secshares": [ + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 23, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "0295E62D67F98EE3F9349DDA8D39B6668BAF6F25E195135570FEC4E16725FE0DCD02667A763E416AF318B98A5D02ED80E89448049EFA66FAE73F6DAC30A998CF4399", + "5C07FF4B850F51D7377BD96AA8622AF2FB53524AFA4946549297FE6511D5A964" + ] + }, + { + "tc_id": 24, + "comment": "Auxiliary randomness omitted (null), which is not equivalent to all-zeros randomness", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": null, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02268A199CEA08BA3353C03D92A9D5889634BD2FC7614066B421939546D511C8BA03FC8189752FDB334AAC103CAC323A8110F900C160A9DE5809BCA290019496BB55", + "9AE1CCA2D19EFF42ECE9FA772DD0D862C03FAD9949FEC071AAA17FB747B224AA" + ] + }, + { + "tc_id": 25, + "comment": "Auxiliary randomness is all ones, distinct from the all-zeros and omitted cases", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "034C929B5FF1A32312344DBDB4CE731CB5F4BB5904BC6C4DAB06EE628161869F2903853322C0E04E7471C13BDD043AD379D0EF5868F6D15916AA9D93AC8411DEF138", + "CC0EEDC95C48F83DA37EC15634B46B3D3B8FD118B506A13C4A8F03D272E66B12" + ] + }, + { + "tc_id": 26, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "aggothernonce": "02BC6B50C4473EDA599339E76D9F1E4A970BB19B251EADE838C6ADF90347C7A14503294335962373C72A433F05B4921A74316DF9FE84999BEA55B28EF4CD83638FC6", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "036EAB069C1F2C264EF7F15BD142F822EC776E861088D64713007C0366ABB1A73903538966A30A0A1A1C0E6E719298A4C2D55F6E82E759ABC9DAFD6577706BFEA0F9", + "D8BE02C445BF9F327EE989A8859EBF411684D229DA455C954EE90F7EDDC4CBBA" + ] + }, + { + "tc_id": 27, + "comment": "Empty message", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "", + "tweaks": [], + "is_xonly": [], + "expected": [ + "038E64312D0DD47322D26C9B254E76FB496720D57C07493C5B4329B72A2E128A7B02E62E844D742B01F432B205545C441ED526F433F245F604BBC291A9B81F183F1E", + "F9736615BD6368C21F2FADAD40346D5B561E3DFBD9B08CB80587DA965AC605C6" + ] + }, + { + "tc_id": 28, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03CCE578EABEBA27ACF60873B6CF566F4561614336E73C80CB8313D47DAC01229F022F48D9E1A4F927849C3361BDD259475076D7F4D75829D914625E0515FEC4ADE9", + "848FF2549F3A2527F974C65915B6157DC175500B09FB0FD4F3A044D7E8B4175C" + ] + }, + { + "tc_id": 29, + "comment": "Single x-only tweak applied", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" + ], + "is_xonly": [true], + "expected": [ + "02678CE16AAC46533D375EA5D3E9A1220F7614074D341E2D6DA3A59695D87D85C90315E6229EDA7816AF02163957247D1CF9161E2A975EE04E76B594C6EA45F358B8", + "C5EB76498241CAECCF2E85041E3A43650EF7D0363D6916EECB0695D1DB76399E" + ] + } + ], + "error_tests": [ + { + "tc_id": 30, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 1, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": "03BF06149B3E614403EA899F37446B294117785D15D238E1E36989F294120200A20329620EA331F70450B94A7F6B175CF7C5A65CD967D367332AFD4C6FE514882467", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 31, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "aggothernonce": "02BC6B50C4473EDA599339E76D9F1E4A970BB19B251EADE838C6ADF90347C7A14503294335962373C72A433F05B4921A74316DF9FE84999BEA55B28EF4CD83638FC6", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 32, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 3], + "secshare_index": 0, + "aggothernonce": "03BF06149B3E614403EA899F37446B294117785D15D238E1E36989F294120200A20329620EA331F70450B94A7F6B175CF7C5A65CD967D367332AFD4C6FE514882467", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 33, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 0, + "ids": [3], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": "03B19C94C4B6537008FFF018971B311BF30B1BAB5F8983A5B262D0CD37CBD309200329A028ADEF6AB30E667123E82DFB1BB055F380D0CDD5CDBF4ADCB1067398F558", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 34, + "comment": "Aggregate of the other signers' nonces is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 35, + "comment": "Aggregate of the other signers' nonces is invalid: first half is all zeros", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 36, + "comment": "Aggregate of the other signers' nonces is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC020000000000000000000000000000000000000000000000000000000000000009", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 37, + "comment": "Aggregate of the other signers' nonces is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 38, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 39, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [], + "pubshare_indices": [], + "secshare_index": 0, + "aggothernonce": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 40, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 3, + "aggothernonce": null, + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ] + }, + { + "tg_id": "3of3", + "t": 3, + "n": 3, + "thresh_pk": "023E94D6A68620D3F60221A4186786274A711221A8BBB31A5388DFA6718E8EF7BB", + "pubshares": [ + "03A3E932BA9DD0063D721590B6EF21DD4566A97B203343FDE9DA5815DB9BE049CA", + "0201E5FEAC0DB5059C32B045F37799101A96262B3C88CE85D610451FC21A0457FF", + "03EF72D10D1DFE4E786619536A458507CAF105FCCDB8173DAF2E358054AF2FA4A2", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secshares": [ + "D4DE571C662C319E7C4F46420F5ACBEFD2D905C3B52771C553DE1F76197D940D", + "0EAAC017ABA2030272B7F8EAAD68E22BF07F08950D7B570B69B2CCEF278D52C1", + "1E4E4344B9B59410588CCF327B766E4D298882D904F0FBF288C86B73AC5962A3", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 41, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02B80EAFA062C4F9C47F361E815D569569F1380C870002139CDEE079080681EE6C03ED14FF4C014894D392529D35FACA5994FB7D147FB232F05D67184498053AF9AA", + "55C05DB762676CD9E26A1681863C54DC53EE8E86DD831D0013F1E2459B2DA4C3" + ] + }, + { + "tc_id": 42, + "comment": "Reordering the signer set leaves the deterministic output unchanged, because the identifiers are sorted before they are bound into the nonce derivation and the binding value", + "my_id": 0, + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02B80EAFA062C4F9C47F361E815D569569F1380C870002139CDEE079080681EE6C03ED14FF4C014894D392529D35FACA5994FB7D147FB232F05D67184498053AF9AA", + "55C05DB762676CD9E26A1681863C54DC53EE8E86DD831D0013F1E2459B2DA4C3" + ] + }, + { + "tc_id": 43, + "comment": "Auxiliary randomness omitted (null), which is not equivalent to all-zeros randomness", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "02198622491E3A0971DDD3E3942FF4CB87DD93C89F44E64F2F8EC67FEA87F7784103C76826C434E8FD7FC97035A02A48B69216E4C9B7B7D12E5F610932D684B97918", + "rand": null, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03CA9BF0AC6D0D53ED0462C460C0039AD2D9110E989936B0449FFD1D5CC9951B340277D02C4310270F0E8A88C1D4EC6ABF1BC3EC1115A318DECD42B9DB63383A8DC0", + "C434DDAEBDF29FE18CE077ADE6205F4BEC11F0E882C445FC700AE2B7A81DAF07" + ] + }, + { + "tc_id": 44, + "comment": "Auxiliary randomness is all ones, distinct from the all-zeros and omitted cases", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "030ED1C876E4F01533A8D4BAB57F066F1F524683B9780D410D95156AA2FF338C1703D1220C19343CD26DAFC53FA70AD7F2D2FB9D0916DE8CDAD71A6538E62E2DA73C", + "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03F2403F95C7EE842EFBF0F97F9C10475ACF040524BFD330A814698FB02DF9799E0334170AB7D14732FA2715D80F9837C065C94C7D01B3AF1D88ADBDF977B857C98C", + "5541CEB56A7553A0DA7D74B721B33A2AAC63C149A4FA9591FDE20156D40013B1" + ] + }, + { + "tc_id": 45, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "aggothernonce": "03D14459A3411BADB6C14B482BC173A8BBF644AC6274F7270419DC7C7967DD66BC0254E13581A0CCB65293AE69B3C070CC68AF13379805CBDE72809BA9A354683CAE", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02A1D0B9E85635BD87697EAF55DD319FACAE2E991C42336E6B6535FDC0932162B802A9B35155A8E77D0BCC8D5C47ECBCE37A6D0C7BB5CF0B2DD7B116512148CEC436", + "4D6720C084EE12026358BCA287B63602F9BA3871CFE65656C05D1ED53C558105" + ] + }, + { + "tc_id": 46, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "03DBEC1DF71805A741A07100BEE788344F006ECBFDF4D65E14CB18A73638D547F403F1379806F19A84BD49BE1FB02DB1EE7B149FE2362D990FADCEA3143AFB89B0B3", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03936EB45747FE9068DBC6D2D86FBB979D625D617F20519A6DDD1FE25591A491B303DCE086739A87DD04D97B7042DDDF217FFED3665C1D151735EA17B6A78068FD33", + "4D0F98ED886C019D6C4C47E5C30C5967D9D59EC57820EA9181270F944523A3EC" + ] + }, + { + "tc_id": 47, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "023E91D85064879ACA2EB0A1B1ACEEDAF5FF75B00BCE775552A012DD3D533A835E03A566DDF1C7C6C9059F6ED07E81C6F9E074DBC54E873E9EFE78DE1500CAF2D482", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "tweaks": [], + "is_xonly": [], + "expected": [ + "020BBE6991AF738E91119B5241B1BD00910CEC723A05C95F775B7F887D67D25FBD02F20C39B93C39D2CC6035C16D9D1FA38BCBC7B1B528355C6E4CF910944EBD09A6", + "8FC473EF9A9BE1F6599AF017B5575C983C912E15936E39CD708A651D80DDEA1F" + ] + }, + { + "tc_id": 48, + "comment": "Single x-only tweak applied", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" + ], + "is_xonly": [true], + "expected": [ + "03E2DC20E1BF882B1B2E6ADC6405714F92378151E663F019D8AEC40B9E5EF422CE0260D4CE00C2E0BC5AA5282FC974E8460E08154AD7E9316CED832981561F0267C7", + "E9DEC4131FCB84DAEAEDDDD0DFAA84E4D9E6C902AB63C7BB9BB5F1CB13697ACE" + ] + } + ], + "error_tests": [ + { + "tc_id": 49, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "aggothernonce": "037898008EFDB2DF4EC5D990A7461FBC51B9D35E6F9F06F2424F77F74D0DB6C49402A1958475AE7073DBE8C699A41BD543DD102671A7CDBC1241C4F613D13865A7E1", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 50, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 3, 2], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 51, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [3, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "aggothernonce": "037FF9B8D69E033F05484D8B6F3F1254528765850218BE9D6D438864D61D8794C6029DEB098F8FD67D8C006D686A642176B7E560CBD6F1356E454A5E93B2CE659609", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 52, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 2, 1], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 53, + "comment": "Aggregate of the other signers' nonces is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 54, + "comment": "Aggregate of the other signers' nonces is invalid: first half is all zeros", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 55, + "comment": "Aggregate of the other signers' nonces is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC020000000000000000000000000000000000000000000000000000000000000009", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 56, + "comment": "Aggregate of the other signers' nonces is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 57, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 58, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "02EF352923D0B4768732A380315C00960A71DA1FAB06D7CF3B440385084E3FAC2F02141328A420CA7916711CC3B40981B9342377FC75FB8FEF29D4D3BC71148A4082", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 59, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 3, + "aggothernonce": "0332B7FBB7F9A16AD4073FF482BF6F77A2943099FA5C2B691CA9002C39EC7249F3030FB4D9D5D74D2DEEDBA77ED1F16EF277769A12F57117EA0A0B999905FB0A042C", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ] + }, + { + "tg_id": "3of5", + "t": 3, + "n": 5, + "thresh_pk": "03E1AEA00A7B8D0E0393664FEBBD31569A02CAF223D9AD83E90331DC18B1987358", + "pubshares": [ + "02F333EDD2B6F532C2EF6C42E803B633A42289D0567A75C50EB1C8CF85EC480E8F", + "025322F63602ECCF146541B7C9EE4D1FCB84EC139471420472182AE968676A387F", + "0230639CEC0A83982E176D6905CE0620FDC57E151565A39D0E0821B8E753AE85B0", + "026E1A50E672FEDD3F3C378D6121E5702B050669DBC241D2C2299880152CD207BD", + "02DC06C4CA919DE70B4B8281901CCF9471AB91FBFBEE1A8C570D83911CAAF9E2FA", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secshares": [ + "77C9316A64770B17D500840E020A9478F793D37DB0E6A276CB4E6270187D6C90", + "74180420A784189B55F8C4667075E89C2064FF481B6ED2A23A7517AF71F6B7A8", + "BE6B9FB07586EBABD234EEE0A05E813DFFC14D73782085C8DFE77C1FADDA7172", + "56C40419CE7F844949B5037C91C45E5FDAF9E11917B31BAEFBD33133FBF258AD", + "3D21315CB26DE273BC79023A44A780006CBD971FA96F34904E0A95792C74AE9A", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 60, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03A10647D6B61EF5CAF27F4EC071B1E7ACF45EC2A55FE686F38B24FD7C36D5A2BF033AB814157913499001873909DA7843DDCEC15E64DFE9C3AC9080C3322183624A", + "68797978D8A3808FFFD609D37AEC851551B86169B645287200916F1033E617D0" + ] + }, + { + "tc_id": 61, + "comment": "Reordering the signer set leaves the deterministic output unchanged, because the identifiers are sorted before they are bound into the nonce derivation and the binding value", + "my_id": 0, + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03A10647D6B61EF5CAF27F4EC071B1E7ACF45EC2A55FE686F38B24FD7C36D5A2BF033AB814157913499001873909DA7843DDCEC15E64DFE9C3AC9080C3322183624A", + "68797978D8A3808FFFD609D37AEC851551B86169B645287200916F1033E617D0" + ] + }, + { + "tc_id": 62, + "comment": "A different threshold subset gives a different deterministic nonce, since the signer set is bound into the nonce derivation", + "my_id": 0, + "ids": [0, 3, 4], + "pubshare_indices": [0, 3, 4], + "secshare_index": 0, + "aggothernonce": "03BD60BBC5F251F988DA82505A0025C0B2AC9D90FE1708CDC6AF73A2D505120FFA032EA935A597AE9A6FFA87238B5E5B19D99B82DAFD2E63FBDF79405D8804B0BEA5", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03F3BF166F7258AF832B72C2B638C0712E62B0777B79A052C6B0B2E848475874B60327E180064432D0555554BD18601D0DAAEB74A49213D40493FBB752FB7C720C27", + "922462449E0A993EB778CCFA77D4EFCF8C44A6461845DF9A48508402EE4DE4D3" + ] + }, + { + "tc_id": 63, + "comment": "Auxiliary randomness omitted (null), which is not equivalent to all-zeros randomness", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0370EDEFE5A151F0300A2D5B0D4DEB852FCFA6FF5A51C49F2C165CDC89039BAEA3027EBD4CF587854DFF6E7E543A0F2946100EAFA06BA7725A5274FB3FFF51B44E12", + "rand": null, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02D1D64CCA49F6176E4004A613D77BF13CCC269ACA131C30731D6B57ECD16CDC350259C6BB03EE92FC0AC2966F7C0710A9F080963F5C2D9D5656796D14255E33BBA4", + "04BE5D6944441D05B1BA9633A184C9DEACA8820C56F87E641EC4028466DF674B" + ] + }, + { + "tc_id": 64, + "comment": "Auxiliary randomness is all ones, distinct from the all-zeros and omitted cases", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "02C578DB2BCA4288BAE8C820EFCAB44F1F71007060AE30AB53626B851E93EC4D3A037C5FB4E1AE08FCC48CF4FE5A6A07D9BAFA450A832427C49E4AC07FBAFE0391CF", + "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "03290D3ABEE38251286025A8481C0E1075B2FA0B0CA1DFFB5CA7BF29B6E497241C020A86452ADC52036AD06D2E0CFB8704998C65AA7AE130EF79C9D2805C566BC5FA", + "D79FF828EBEF3ED9E83F612B1B3C4BB30D5FA96E79B500353A8ADA4146D8E7D8" + ] + }, + { + "tc_id": 65, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2, 3, 4], + "pubshare_indices": [0, 1, 2, 3, 4], + "secshare_index": 1, + "aggothernonce": "031B2116862A490740D7082F477A81EC2410A68C39BFBD38B50978C80E4A2BFD2802A769A1EF6EC7295F19040BF0193BD21692977215C9B9332D18AD815205EE483D", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02351BDA5BEEC231E09483237F67D7EABC006E3C386B6BF8303951EA14045E2ADF030E49F36C12A2467EE777BF76D3EEFBE53FAB399F0F5EB08B9B89BD20B3586248", + "34B68A4510C7488AC9DD421B926E3F4C5C83A40446A3244E6E542ED40480D7DD" + ] + }, + { + "tc_id": 66, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "024C00C326A19D4FD350BDA2215C6106EA7F6EDBED39FACA843D63BA0818C29BC102F60801C2F53CAA17A3178827BFE54227910548C5763F4128ACE98B35F5DB920E", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "", + "tweaks": [], + "is_xonly": [], + "expected": [ + "02C53EF94A81C740959954BA92112638EA9F2FAA82A52141246275EF91EC6C6BFB0207B251CEB4045FBBBF6D64A85A06FDB9DE2B79F9599E4E1CA9617379713A5EBA", + "FBC611CE731AF8DD0864F697398696A1BF7A5F43860014CB2EA4B48DAA26F24C" + ] + }, + { + "tc_id": 67, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "039509DFFF34C4536958557301E50BA8F46EEA1CC9C3AF0543F748F2287666F0F4038659DC107436BD5CACC6019DE0DDB6FAA1762D9AC01A0A8CE792EDB5A17C4CE0", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "tweaks": [], + "is_xonly": [], + "expected": [ + "0298A8254793996B39880389EAC21A24E698BD5F723E83857EB8926DFFF22B10A9024B32BC14A48B2E7B2CDEBA207C77E34EC8EB394C34D1F9640813596FB5F9961A", + "11440936EFF97E326102806F539D929EB49078D2C8EB66902DB1D824863FF6DB" + ] + }, + { + "tc_id": 68, + "comment": "Single x-only tweak applied", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" + ], + "is_xonly": [true], + "expected": [ + "0310B14CBDD8E9319442C239353AC24AD698C60288879ADAD357BF4ACD4A7C8DB70290D0199D6C7BA8A3D72462D9AAB70150BE92E54FD031EDE2116725944C1C07B7", + "89112CF4A4D87E1BDF3D33BD6847DDD09874D7EC1BA237F9BD64097FB5179F74" + ] + } + ], + "error_tests": [ + { + "tc_id": 69, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 3, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "026FA8A1E96458EF85245011687718880BD474A2433618437ED67ABAF2D12C5A6A03183F1777ED7434F02421F8391597736124D3EC38B21C98F1FF5F01A5B4BE196F", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 70, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "aggothernonce": "02DB1AFDB69CAB66CDBE9DD45F0487E51E72238F1E2FA6F1C468E19153C538115B026BA7F81DFD36E6787962FCA76C754B54B02BB3BF314313D2189214FAEFF7E724", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 71, + "comment": "Signer's public share is not in the public share list", + "my_id": 1, + "ids": [1, 2, 3], + "pubshare_indices": [1, 2, 3], + "secshare_index": 0, + "aggothernonce": "03CC8D9E200DF6C735AD179221FF4425B00744556C77BA0A89E6DD710E1103686002F8DB2417FAA2FF7696322094471DEF9352A8DE601B3EA267A59089B49A4C294A", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's pubshare must be included in the list of pubshares." + } + }, + { + "tc_id": 72, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 5, 2], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 73, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [5, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "aggothernonce": "035CDD6D2363EA4AE8D2EB92A78B3B9E741DEA0654DD220706B1A05664E6B4F7FE02C9F9FA0982818E3FE755F78D740A8A07FE1FB5A0CF253389A75533ED6A036486", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 74, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 2, 1], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 75, + "comment": "Aggregate of the other signers' nonces is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 76, + "comment": "Aggregate of the other signers' nonces is invalid: first half is all zeros", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0000000000000000000000000000000000000000000000000000000000000000000287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 77, + "comment": "Aggregate of the other signers' nonces is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC020000000000000000000000000000000000000000000000000000000000000009", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 78, + "comment": "Aggregate of the other signers' nonces is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0353BC2314D46C813AF81317AF1BDF99816B6444E416BB8D3DC04ACB2F5388D1AC02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggothernonce" + } + }, + { + "tc_id": 79, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 80, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "aggothernonce": "02DC0CD5CAC6A1EDFF9AFE93C22F1A86189BD5493DD8735A0981C00FFB267D00750300CA727FCD503E732E37567B12A27DFBE5440B8240B03848FFF55F14168AE397", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 81, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 5, + "aggothernonce": "0219F17947F46DF47FF6498266840A3EB4EB3C000842A77AFF629133255674C92E0344A483E63F20FFD037A3BCA53ADF552549DF79D64F3DCF89596B7037B856C1E4", + "rand": "0000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweaks": [], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ] + } + ] +} \ No newline at end of file diff --git a/bip-0445/python/vectors/nonce_agg_vectors.json b/bip-0445/python/vectors/nonce_agg_vectors.json new file mode 100644 index 0000000000..92a2239273 --- /dev/null +++ b/bip-0445/python/vectors/nonce_agg_vectors.json @@ -0,0 +1,57 @@ +{ + "pubnonces": [ + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" + ], + "valid_tests": [ + { + "tc_id": 1, + "comment": "Two well-formed public nonces", + "pubnonce_indices": [0, 1], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8" + }, + { + "tc_id": 2, + "comment": "Second halves sum to the point at infinity, which is serialized as the all-zero encoding", + "pubnonce_indices": [2, 3], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B000000000000000000000000000000000000000000000000000000000000000000" + } + ], + "error_tests": [ + { + "tc_id": 3, + "comment": "Public nonce is invalid: first half has an unknown tag 0x04", + "pubnonce_indices": [0, 4], + "error": { + "type": "InvalidContributionError", + "signer_index": 1, + "contrib": "pubnonce" + } + }, + { + "tc_id": 4, + "comment": "Public nonce is invalid: second half is not a point on the curve", + "pubnonce_indices": [5, 1], + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + }, + { + "tc_id": 5, + "comment": "Public nonce is invalid: second half's x-coordinate exceeds the field size", + "pubnonce_indices": [6, 1], + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + } + ] +} \ No newline at end of file diff --git a/bip-0445/python/vectors/nonce_gen_vectors.json b/bip-0445/python/vectors/nonce_gen_vectors.json new file mode 100644 index 0000000000..1ec149ffae --- /dev/null +++ b/bip-0445/python/vectors/nonce_gen_vectors.json @@ -0,0 +1,74 @@ +{ + "valid_tests": [ + { + "tc_id": 1, + "comment": "All optional defense-in-depth arguments present", + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "secshare": "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "pubshare": "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "thresh_pk": "D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "msg": "0101010101010101010101010101010101010101010101010101010101010101", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected": [ + "60F73DF0C66B5BCADF68B4CB3487ECF3F3065CC9D9C1DCF556453A42E21ABF5C9CA37BF6CFE0E90C16D83EFB572095D82863F299CA86BE3C53F167FFD1F3606A", + "038EBC31EF5C88C3E4470BDA8765A8377697E1CB20A1D07D6B09C0146814EC5BDF0386D61A1CCFBEC0A8AEF01FFD83A6F9E0E90FDC20D093861A27AC4A10C931B0D3" + ] + }, + { + "tc_id": 2, + "comment": "Empty message", + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "secshare": "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "pubshare": "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "thresh_pk": "D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "msg": "", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected": [ + "233536903340C2F51047933410ADD75CC62A9099AFAC1FAD4109CA234EC94099A30106B187A6572B2EE1570DE05832B1683DF272769DD0D34E460CB278086173", + "034CCFCF7B15D5E5A0BAED01F744B84D3A405C1800B6E17BF1F2BCE8817D4A965903C110E64C61F7527C0D2CF8E99F9A62B8A4340F08303E272845351C9A607FDF8F" + ] + }, + { + "tc_id": 3, + "comment": "Non-standard message length (38 bytes)", + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "secshare": "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "pubshare": "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "thresh_pk": "D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected": [ + "E37A192E7BB911947C758F8A8B81965CBE0FB339A7FD3682CC649E290C8111A2626895940357A4BF92DF2502BE5566BBE664D8CAC0A7AF39E57AD5F515F7DF0D", + "02DCF5FA504E62A3C8BD7349C02B13A6BB9ED5A34F755D2533CC014DD16C5DAE1E026537B347683C1795AB20C34EF64C7C086C87D26B149FF4B2448CD58F217AA026" + ] + }, + { + "tc_id": 4, + "comment": "All optional defense-in-depth arguments omitted", + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "secshare": null, + "pubshare": null, + "thresh_pk": null, + "msg": null, + "extra_in": null, + "expected": [ + "42B39CA56390449A85BCEC2EF9B102CE945E693570A0077E98F8E77476AC3063EC9CFC5CE249D19C7BE5C310A425FB5E1E9B3B4577F628D9854A4A3C6C64C653", + "0375B28F1525614D3A57AE7B5D9109C24BDCB823065C4AD85D01A53EDCEBD22FEA03EBAC798BC7D8F6580F21792938CD7A8B4462BBBDB5AB171370F17951E55CE990" + ] + }, + { + "tc_id": 5, + "comment": "Message omitted, other optional arguments present", + "rand_": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "secshare": "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "pubshare": "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "thresh_pk": "D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "msg": null, + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected": [ + "00C1423289D2FA1F44CC1538CD7568F8F944A68122BC3DF385C157ED5F8191098A53AFAF92EE42F6705F0DE2E01D2621A1FA762D80F450E087B521AA7CCA1667", + "02B51AE074E15B3C34EE1E5328BA91FB9BA88A973B676D770063B54306F873E0D702A89DAEBF9F9E393E9F5E398803D6CA544C26F9A8DD8FCC814432F9C1CAC1F6C3" + ] + } + ] +} \ No newline at end of file diff --git a/bip-0445/python/vectors/sig_agg_vectors.json b/bip-0445/python/vectors/sig_agg_vectors.json new file mode 100644 index 0000000000..cbd876b60d --- /dev/null +++ b/bip-0445/python/vectors/sig_agg_vectors.json @@ -0,0 +1,454 @@ +{ + "test_groups": [ + { + "tg_id": "2of3", + "t": 2, + "n": 3, + "thresh_pk": "02D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "pubshares": [ + "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "0284DC4AB2CB78A621EB87FA1F14BCE2B725AFEAAC981ADCBAFF5CC2D417D2A63A", + "036441EC2D4C1266201CD89B69549A2F5B2188612A0D434153E625FB38173DD509" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" + ], + "valid_tests": [ + { + "tc_id": 1, + "comment": "Minimum threshold subset of signers, no tweaks", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862", + "6A9DCEB9F706A4B87BB2794EEA0DDF7C005E51A88CE315464F13E75E404265A5" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "A50BE88D44E5F5D4DBBA6C509FB421E70B170C4F5F14CFC6E5D79E718486AF7E960712E992D4C673EDDAAAF0FF1D9922DFCB543160814395A59BD0887C8CDE07" + }, + { + "tc_id": 2, + "comment": "Reordering the signer set leaves the aggregate signature unchanged, because the partial signatures are summed and the identifiers are sorted before they are bound into the binding value", + "ids": [1, 0], + "pubshare_indices": [1, 0], + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "6A9DCEB9F706A4B87BB2794EEA0DDF7C005E51A88CE315464F13E75E404265A5", + "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "A50BE88D44E5F5D4DBBA6C509FB421E70B170C4F5F14CFC6E5D79E718486AF7E960712E992D4C673EDDAAAF0FF1D9922DFCB543160814395A59BD0887C8CDE07" + }, + { + "tc_id": 3, + "comment": "Aggregation with three tweaks applied (one x-only, two plain)", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "tweak_indices": [0, 1, 2], + "is_xonly": [true, false, false], + "psigs": [ + "7A0A93E3F388315DDF8E85181FC78EDB3663F0461FB7F3E01808288EF038BBDB", + "10F1DF28F04DBBEB9A35844B4C8014FCE0B3C89219649E2B96409FA387087803" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "39C164B140C19AD2A04FD6A0A872DC97D6A1344D1E41A33F094575AC7BEB496D934292F486A9910ACC0D383DF01A6AC1CA975BDB476FFD0E4384EB827A76B8E6" + }, + { + "tc_id": 4, + "comment": "All signers participate, no tweaks", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "02CBC2E41AF87290A1401D49927FC30D7F40A404C4DF3E5FC734014188DBC44E1503C15312EDC691EB213F3B8AAC195D251A390563CD5E313D738A89DAEB4DA80746", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "E5FC2CD881A3B6383FF574DB62922F51CE52D23488C35B8D339D9A596290520E", + "CCEA8A2B761343247785EA971C7ADF022E0890E8764A1C4AC8F75116B65FAA98", + "18A3CA164447848E810C4FE661CDA2184D7693A6BF33D09DB1CD6FF4E1705DCA" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "2A395C32B669A5704D289BEC8DF67ACC33AE943778811BE9530EEB140E98A57BCB8A811A3BFE7DEB3887AF58E0DAB06D8F2319DD0EF8A839EE8FFCD82A2A192F" + } + ], + "error_tests": [ + { + "tc_id": 5, + "comment": "Partial signature equals the group order, which is out of range", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 1, + "contrib": "psig" + } + }, + { + "tc_id": 6, + "comment": "Number of partial signatures does not match the number of signers", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The psigs and ids arrays must have the same length." + } + } + ] + }, + { + "tg_id": "1of3", + "t": 1, + "n": 3, + "thresh_pk": "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "pubshares": [ + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" + ], + "valid_tests": [ + { + "tc_id": 7, + "comment": "Minimum threshold subset of signers, no tweaks", + "ids": [0], + "pubshare_indices": [0], + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "468943B2324187A2B2F2082F562C9E5AEA55C5748DFB382EED749840BB9932FA0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831" + }, + { + "tc_id": 8, + "comment": "Aggregation with three tweaks applied (one x-only, two plain)", + "ids": [0], + "pubshare_indices": [0], + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "tweak_indices": [0, 1, 2], + "is_xonly": [true, false, false], + "psigs": [ + "D4491BC20826CA2931DABD4AC57D6DA8278C0CCBE419F2EB60B25E21FB855005" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "985E99395C9FD967DD5DF5E6D61A7B60861A1DD5ED2A6AF8BC39AE79E008E6350C23C09A38908FFB710037CB937D723F15128CB7A9CBAF6A54B57754F21EADD1" + }, + { + "tc_id": 9, + "comment": "All signers participate, no tweaks", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "0277E716756BAEFD922DCD339D083FEB1C0263B06999328492331FB876A225EEA3033910C4FC151C7AA914B4D2AC97B3F09C56A36A5F95F14043D90210CC1FCD4245", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "98880D6A53460E57168FB517431D65569AE9AEE791796959E21E4EDF4BE8F8C8", + "010D7844DBD7DCDE861BA2DEAEBF1EDB620446760857FC88A18C3735496F1306", + "6609DBB32B76A8843BBE59AF11A8A32D87F28C170E6E451421ED9C514B15AC32" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "96EB76FDD08FB48B41A6674F5008427F6E3EC1D105D634411BF83A9405DBA393FF9F61625A9493B9D869B1A50385275F84E08174A83FAAF6A5982265E06DB800" + } + ], + "error_tests": [ + { + "tc_id": 10, + "comment": "Partial signature equals the group order, which is out of range", + "ids": [0], + "pubshare_indices": [0], + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "psig" + } + }, + { + "tc_id": 11, + "comment": "Number of partial signatures does not match the number of signers", + "ids": [0], + "pubshare_indices": [0], + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "tweak_indices": [], + "is_xonly": [], + "psigs": [], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The psigs and ids arrays must have the same length." + } + } + ] + }, + { + "tg_id": "3of3", + "t": 3, + "n": 3, + "thresh_pk": "023E94D6A68620D3F60221A4186786274A711221A8BBB31A5388DFA6718E8EF7BB", + "pubshares": [ + "03A3E932BA9DD0063D721590B6EF21DD4566A97B203343FDE9DA5815DB9BE049CA", + "0201E5FEAC0DB5059C32B045F37799101A96262B3C88CE85D610451FC21A0457FF", + "03EF72D10D1DFE4E786619536A458507CAF105FCCDB8173DAF2E358054AF2FA4A2" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" + ], + "valid_tests": [ + { + "tc_id": 12, + "comment": "Minimum threshold subset of signers, no tweaks", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "6D180D3D1776FC298E1F00F806D8444EA597749245ED88E05B0FF08C98AA6316", + "E13827F0D8317FB4E178BFA453147B35C51F797E1FE0A23AAF701341E264E977" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "A644B6E81BFF3C477C33B99F71839845267BA37A933F178E78059D44D6A8151656F89BD9DFF272E9ED1BE71D47A44BA224351491BF912A36E76C42B1C4CE45D9" + }, + { + "tc_id": 13, + "comment": "Reordering the signer set leaves the aggregate signature unchanged, because the partial signatures are summed and the identifiers are sorted before they are bound into the binding value", + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "E13827F0D8317FB4E178BFA453147B35C51F797E1FE0A23AAF701341E264E977", + "6D180D3D1776FC298E1F00F806D8444EA597749245ED88E05B0FF08C98AA6316", + "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "A644B6E81BFF3C477C33B99F71839845267BA37A933F178E78059D44D6A8151656F89BD9DFF272E9ED1BE71D47A44BA224351491BF912A36E76C42B1C4CE45D9" + }, + { + "tc_id": 14, + "comment": "Aggregation with three tweaks applied (one x-only, two plain)", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "tweak_indices": [0, 1, 2], + "is_xonly": [true, false, false], + "psigs": [ + "D34E4B9813BB91338AE7AAF3E18AB30C72576AB8F384AF7815FBCBF9C1896198", + "6F4E87BCE8A96D88751BD9197218B9F1F19078413D887932762719085CC4C89B", + "3BC22B9A3CB0905FC1CD4DA9C1D455687A7EE92BB0445B1DBF4EF61E03D3F712" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "BECBF4B59D2E628FC146D74BDFF7FA0F17723366AE0768E72428AC8747F50C7763003C708F483162BBD57EC6528C15E23CD1C6CE9A6569C2D21702E5F9AEE10A" + } + ], + "error_tests": [ + { + "tc_id": 15, + "comment": "Partial signature equals the group order, which is out of range", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "6D180D3D1776FC298E1F00F806D8444EA597749245ED88E05B0FF08C98AA6316", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 2, + "contrib": "psig" + } + }, + { + "tc_id": 16, + "comment": "Number of partial signatures does not match the number of signers", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "6D180D3D1776FC298E1F00F806D8444EA597749245ED88E05B0FF08C98AA6316" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The psigs and ids arrays must have the same length." + } + } + ] + }, + { + "tg_id": "3of5", + "t": 3, + "n": 5, + "thresh_pk": "03E1AEA00A7B8D0E0393664FEBBD31569A02CAF223D9AD83E90331DC18B1987358", + "pubshares": [ + "02F333EDD2B6F532C2EF6C42E803B633A42289D0567A75C50EB1C8CF85EC480E8F", + "025322F63602ECCF146541B7C9EE4D1FCB84EC139471420472182AE968676A387F", + "0230639CEC0A83982E176D6905CE0620FDC57E151565A39D0E0821B8E753AE85B0", + "026E1A50E672FEDD3F3C378D6121E5702B050669DBC241D2C2299880152CD207BD", + "02DC06C4CA919DE70B4B8281901CCF9471AB91FBFBEE1A8C570D83911CAAF9E2FA" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" + ], + "valid_tests": [ + { + "tc_id": 17, + "comment": "Minimum threshold subset of signers, no tweaks", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "2EA9B38288385D6AB64FAEDC74ADF407BA7FF74926E85A10A3B3A6B45C98BD6D", + "AC075489568998C47504EC8E4FC7AB5B243AF7739111DC6ECFF63276F924AE65" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "BC26C0D0B29D2A416C44D04BCFA9D38C835E4D2D18C2E9BF0AC04635044CBF46711413B902B559256D2D53F84E65A616DF4AD90F16320A7D4BC4773B1C286564" + }, + { + "tc_id": 18, + "comment": "Reordering the signer set leaves the aggregate signature unchanged, because the partial signatures are summed and the identifiers are sorted before they are bound into the binding value", + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "AC075489568998C47504EC8E4FC7AB5B243AF7739111DC6ECFF63276F924AE65", + "2EA9B38288385D6AB64FAEDC74ADF407BA7FF74926E85A10A3B3A6B45C98BD6D", + "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "BC26C0D0B29D2A416C44D04BCFA9D38C835E4D2D18C2E9BF0AC04635044CBF46711413B902B559256D2D53F84E65A616DF4AD90F16320A7D4BC4773B1C286564" + }, + { + "tc_id": 19, + "comment": "Aggregation with three tweaks applied (one x-only, two plain)", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "tweak_indices": [0, 1, 2], + "is_xonly": [true, false, false], + "psigs": [ + "770F3D44887BDBBDF9677EA60A15EB76D7B8F902380F37F18410E20D0098D9C0", + "716296B6D612CA76BBD15CAD9E7C4D4B4DF21A45EDD56A29F4DE6C43E412784F", + "C119785E757BE23896CD7EC52F8255AD7A884E459DD4E201C379E4BA636E5CD3" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "77C86962FF9E39A6372B526890AF9E74D2A3AFAADFE98B1027448792426B2ECFFA1886EA901A10640C4ACAE261E133110D005F13AED5F84BFD52FA8910AD8EA1" + }, + { + "tc_id": 20, + "comment": "All signers participate, no tweaks", + "ids": [0, 1, 2, 3, 4], + "pubshare_indices": [0, 1, 2, 3, 4], + "aggnonce": "03536024F21D3C18E0F094D15E9DDA07740754905570A1B68CAFAC743A6C213745037C9EA6730CD21904C131FF4A6C62480A7BABAA75F0A4F63F1AFD8DEE9FDD699D", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "6336EBA6F79E63DE2C31882D8CFC00B6E307D4C3F169346EE70DF0F82A74E9CC", + "EE624D5540B13082E7F4385188485CB757978F2A84A3E72B7D09DE1760558AAC", + "3F69A9D808A052C2151C68FDC7DB78C7581E6774B6FBC7C344DCB2A44C74E650", + "F4E0DDFF8DDE7745DD787B966F2B5F31A047ADB702E9EA8D2260D8E5BA2B3901", + "7B9D33EF9A490DDFC3628A7EFA6AD78CE9989AAD57C7271DFDA34CB9CF86F7EE" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "0CC7BC744D06AE2B185A7E0799E963F092582CB412955456A1D0B85A064A3AFA0180F4C369176C48CA1D2F9246B60CF7EC917D1379E0145589818BACF04EC7F4" + } + ], + "error_tests": [ + { + "tc_id": 21, + "comment": "Partial signature equals the group order, which is out of range", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "2EA9B38288385D6AB64FAEDC74ADF407BA7FF74926E85A10A3B3A6B45C98BD6D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 2, + "contrib": "psig" + } + }, + { + "tc_id": 22, + "comment": "Number of partial signatures does not match the number of signers", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "tweak_indices": [], + "is_xonly": [], + "psigs": [ + "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "2EA9B38288385D6AB64FAEDC74ADF407BA7FF74926E85A10A3B3A6B45C98BD6D" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The psigs and ids arrays must have the same length." + } + } + ] + } + ] +} \ No newline at end of file diff --git a/bip-0445/python/vectors/sign_verify_vectors.json b/bip-0445/python/vectors/sign_verify_vectors.json new file mode 100644 index 0000000000..e06ffb598d --- /dev/null +++ b/bip-0445/python/vectors/sign_verify_vectors.json @@ -0,0 +1,1465 @@ +{ + "test_groups": [ + { + "tg_id": "2of3", + "t": 2, + "n": 3, + "thresh_pk": "02D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "pubshares": [ + "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "0284DC4AB2CB78A621EB87FA1F14BCE2B725AFEAAC981ADCBAFF5CC2D417D2A63A", + "036441EC2D4C1266201CD89B69549A2F5B2188612A0D434153E625FB38173DD509", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "pubnonces": [ + "03FC0D3E212D25027356A2C9BB7EDC3B900DEA5E32AB10C03868E01C8490F9D36202C61389B6469B4345E00640CDC7ECA301547B97D30FC11AF410E23AD7B4BF69EE", + "03706F00353EA69C0836BF63C2884D2DE5AFE549A33EC87427CA8B5CF4666B28AE021B6300D0545E0B225C2DFC67AE33A9B0259B62809740363717E05CDA4CC9EBFC", + "02B7F6EF566DD2DDAA6AF38371A41F80F93A90B9F407B5BDB8ED763F00103DB0FC039C677604E7B75FF6E47C42B0249A3BAA9CC6B17E68D6D78E21ABA9D04BF40745", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "020F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE202BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261" + ], + "secshares": [ + "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "5A7F9BD41F4B544664C54D777D43303CB5302434F9903B9B552C4E552BF02201", + "61BB07FE8123B9EC255CA3FC27AEF64D5D260A6FC73D7F15B63DBE12EC5DEA07", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "secnonces": [ + "67AFBE2B50F521569B9C1D943F8FE20620B8C25FA81E9788718AF6EE87D5275C9A485A634FE41558E4E108FBB52C5CF616C0CDF02BC0C10A99359FF6C1BD705C", + "6E5AABD814F30045744DC470103D44BCD846284D7A763F9BA3F5C73DCE20BF459FCEDE0DFFF09B9E0150CE5A2C05D3199B4B0987A18C49E9273DEADBBA40B4D6", + "762F4601AC206E1F1497EB541202B727CC19FA0450289C778B5D5F4F1A4B53F4D63D312CC2C9196D08FD62F3958FE8BBC66B8517D10DE534F6A641D0E6E5EBCB", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "67AFBE2B50F521569B9C1D943F8FE20620B8C25FA81E9788718AF6EE87D5275C0000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 1, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862" + }, + { + "tc_id": 2, + "comment": "Reordering the signer set leaves the partial signature unchanged, because the identifiers are sorted before they are bound into the binding value", + "my_id": 0, + "ids": [1, 0], + "pubshare_indices": [1, 0], + "pubnonce_indices": [1, 0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862" + }, + { + "tc_id": 3, + "comment": "A different threshold subset gives a different partial signature, since the Lagrange coefficients depend on the signer set", + "my_id": 0, + "ids": [0, 2], + "pubshare_indices": [0, 2], + "pubnonce_indices": [0, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "03E0D6D449A7CDEC17785A2CF14A4FC52368FC6C8EB00F02F3B218A73FF687388B032947BCD7430962690DBA0594715D16F5C2136F7516AB75A10A0EDB3D5BBAD46E", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "F5CF3F1DA966360FB4588120725BECAE496BE4C4DF18826FEB11371B34EB85C0" + }, + { + "tc_id": 4, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "02CBC2E41AF87290A1401D49927FC30D7F40A404C4DF3E5FC734014188DBC44E1503C15312EDC691EB213F3B8AAC195D251A390563CD5E313D738A89DAEB4DA80746", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "CCEA8A2B761343247785EA971C7ADF022E0890E8764A1C4AC8F75116B65FAA98" + }, + { + "tc_id": 5, + "comment": "Aggregate nonce is the point at infinity, so the final nonce point falls back to the generator G", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 4], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "8A34A98D1548C1F52DC47789EEC9B7837FE11AEBD38510C66A41762D67903C0D" + }, + { + "tc_id": 6, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "", + "expected": "38CF067E1D866E1406AFDBBD6247B9C078A9A487DBE9604D816013A267C7C293" + }, + { + "tc_id": 7, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "expected": "8C89EC85CC9086B18EFD22CDFC5D09CE5AF6AFF6B63E4147E1A1134AD033F5B9" + } + ], + "sign_error_tests": [ + { + "tc_id": 8, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 2, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 9, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 10, + "comment": "Signer's public share is not in the public share list", + "my_id": 1, + "ids": [1, 2], + "pubshare_indices": [1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "033334D94EBA8ED8D3CD7CDD4430F0EE85A207917A12DF12DFE08E1861B004A6D4033D71B2938B48516AB2EE546C5EC36D0EFFBF0B3E290E9084CE5606394395EE5A", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's pubshare must be included in the list of pubshares." + } + }, + { + "tc_id": 11, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 3], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 12, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [3, 1], + "pubshare_indices": [0, 1], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 13, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [1, 0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 14, + "comment": "Aggregate nonce is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 15, + "comment": "Aggregate nonce is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 16, + "comment": "Aggregate nonce is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 17, + "comment": "Secret nonce's first half is out of range (all-zero nonce, which may indicate nonce reuse)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 3, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "first secnonce value is out of range." + } + }, + { + "tc_id": 18, + "comment": "Secret nonce's second half is out of range (zero)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 4, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "second secnonce value is out of range." + } + }, + { + "tc_id": 19, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 20, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 3, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ], + "verify_fail_tests": [ + { + "tc_id": 21, + "comment": "Negated partial signature fails the verification equation", + "psig": "D496BBD06431DE448DD7CE5DEAF04657DB41DA5DDBAA71EC694A756293EBC8DF", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 22, + "comment": "A valid partial signature checked against the wrong signer fails the verification equation", + "psig": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 1, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 23, + "comment": "Partial signature equals the group order, which is out of range", + "psig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + } + ], + "verify_error_tests": [ + { + "tc_id": 24, + "comment": "Verification rejects an invalid public nonce, blaming the malicious signer", + "psig": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [3, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + }, + { + "tc_id": 25, + "comment": "A public share is not a valid point", + "psig": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862", + "ids": [0, 1], + "pubshare_indices": [3, 1], + "pubnonce_indices": [0, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 0." + } + } + ] + }, + { + "tg_id": "1of3", + "t": 1, + "n": 3, + "thresh_pk": "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "pubshares": [ + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "pubnonces": [ + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "02FBEA8F3C4CCA7DFD6BD61F7DF7B49BE720D0DE014144457C1EEB0882B40CA76B03289A43F164E3403CDA987383887BB807EB8CF3329949DC80C3DD5D757EF4B841" + ], + "secshares": [ + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "secnonces": [ + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190", + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190", + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA820000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 26, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831" + }, + { + "tc_id": 27, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "0277E716756BAEFD922DCD339D083FEB1C0263B06999328492331FB876A225EEA3033910C4FC151C7AA914B4D2AC97B3F09C56A36A5F95F14043D90210CC1FCD4245", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "010D7844DBD7DCDE861BA2DEAEBF1EDB620446760857FC88A18C3735496F1306" + }, + { + "tc_id": 28, + "comment": "Aggregate nonce is the point at infinity, so the final nonce point falls back to the generator G", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 4], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "E128865B1A8286740EA76D8D2188FEB3322D34F36A8E1F1593B655F50EF392FF" + }, + { + "tc_id": 29, + "comment": "Empty message", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "", + "expected": "996482A3C99DA47A225CBE6A85CC64D391ACE8C232E5DEED0000F0D7B248CBE4" + }, + { + "tc_id": 30, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "expected": "FEC0E03F4E2BBB84BD1D9B5D3B6F72915D282AF995679ECDCF5F00D74CFED683" + } + ], + "sign_error_tests": [ + { + "tc_id": 31, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 1, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 32, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 33, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 3], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "03FBEA8F3C4CCA7DFD6BD61F7DF7B49BE720D0DE014144457C1EEB0882B40CA76B02289A43F164E3403CDA987383887BB807EB8CF3329949DC80C3DD5D757EF4B841", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 34, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 0, + "ids": [3], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 35, + "comment": "Aggregate nonce is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 36, + "comment": "Aggregate nonce is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 37, + "comment": "Aggregate nonce is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 38, + "comment": "Secret nonce's first half is out of range (all-zero nonce, which may indicate nonce reuse)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 3, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "first secnonce value is out of range." + } + }, + { + "tc_id": 39, + "comment": "Secret nonce's second half is out of range (zero)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 4, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "second secnonce value is out of range." + } + }, + { + "tc_id": 40, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [], + "pubshare_indices": [], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 41, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 3, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ], + "verify_fail_tests": [ + { + "tc_id": 42, + "comment": "Negated partial signature fails the verification equation", + "psig": "B6BA032DAEC58A6B9E92062AD48513B3BD44E727895919A847D9D043009CBE52", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 43, + "comment": "A valid partial signature checked against the wrong signer fails the verification equation", + "psig": "4945FCD2513A7594616DF9D52B7AEC4AFD69F5BF25EF869377F88E49CF9982EF", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 1, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 44, + "comment": "Partial signature equals the group order, which is out of range", + "psig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + } + ], + "verify_error_tests": [ + { + "tc_id": 45, + "comment": "Verification rejects an invalid public nonce, blaming the malicious signer", + "psig": "0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831", + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [3], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + }, + { + "tc_id": 46, + "comment": "A public share is not a valid point", + "psig": "0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831", + "ids": [0], + "pubshare_indices": [3], + "pubnonce_indices": [0], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 0." + } + } + ] + }, + { + "tg_id": "3of3", + "t": 3, + "n": 3, + "thresh_pk": "023E94D6A68620D3F60221A4186786274A711221A8BBB31A5388DFA6718E8EF7BB", + "pubshares": [ + "03A3E932BA9DD0063D721590B6EF21DD4566A97B203343FDE9DA5815DB9BE049CA", + "0201E5FEAC0DB5059C32B045F37799101A96262B3C88CE85D610451FC21A0457FF", + "03EF72D10D1DFE4E786619536A458507CAF105FCCDB8173DAF2E358054AF2FA4A2", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "pubnonces": [ + "02B6418EDDC3E2D48B064BE46F71CEDE3A34594DEAC6E7F6707C8A71CB523EA58602C0B402216E9952E649B8CCFEDBAC6AA10BAEEFF4C02EEAA1148E87C7B21BDFB7", + "02B63A2B319D64A7BFE6634404CB9D80AAA57BAFD8AA01FC52BF77232DC0EE4B600358E8B8F6FB17FB5D92FAFAC1093773645FE32330D6E2774B4BBBCEF7DDFA5102", + "0364F60E66B7BC35A332B4F9688BD5CA2EF508E25C691D6A1A194097D170BBCCE602494AB47255521F0134EC7C41079A8955DA7220172A60371DC32C696AE5D61410", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0335CCFFD0F3A9110FF368B72360D76A9940910BD2DB6C2CCC26F2C4DA984A1E9B03502D16AC8AE6322A0AE9A8CBE887158DE8236A6F1C510B2D5D06F00A851B48D0" + ], + "secshares": [ + "D4DE571C662C319E7C4F46420F5ACBEFD2D905C3B52771C553DE1F76197D940D", + "0EAAC017ABA2030272B7F8EAAD68E22BF07F08950D7B570B69B2CCEF278D52C1", + "1E4E4344B9B59410588CCF327B766E4D298882D904F0FBF288C86B73AC5962A3", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "secnonces": [ + "7B2247A3980C88714A0821F230F689F436FBF80B0B6DED33540B92CB65BA6ED149A8ADAF97EF6DBAA6015CD5BA84E4CAADB7A453508DD17FD8EE3618E119C0F9", + "B1456FA8F924860A665AFC2AE2746E6E2C99EBBE4BC1D0F5BD00BC52CB7DFD79A823D92911393852D4ECCC59387DB88C526FD1AD7574A3336893B618750DBDDD", + "C309D4BC0457A9D6031CDAC33BAFA47BAB2334445BF22D09E1936F919756D275BC09410F5D39E5D701C81BBA99E4E95F9E07FFC32C322F25604582CB242DD46A", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "7B2247A3980C88714A0821F230F689F436FBF80B0B6DED33540B92CB65BA6ED10000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 47, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D" + }, + { + "tc_id": 48, + "comment": "Reordering the signer set leaves the partial signature unchanged, because the identifiers are sorted before they are bound into the binding value", + "my_id": 0, + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "pubnonce_indices": [2, 1, 0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D" + }, + { + "tc_id": 49, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "6D180D3D1776FC298E1F00F806D8444EA597749245ED88E05B0FF08C98AA6316" + }, + { + "tc_id": 50, + "comment": "Aggregate nonce is the point at infinity, so the final nonce point falls back to the generator G", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 4], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "2EFF6BA537599ACDD23E18BF07D639C524F6FE2C13E20C5D1ABC6A06A49E8272" + }, + { + "tc_id": 51, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "", + "expected": "FC83936BE88FFE128B3FD58D93A57D58C2705D55926D6FF3ACDA0B112BC790B2" + }, + { + "tc_id": 52, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "expected": "0D59B34C9B1BBE46767899798CA3DED2C6531D9FE35ADDCED398B960E9B3B927" + } + ], + "sign_error_tests": [ + { + "tc_id": 53, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 54, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 3, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 55, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [3, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 56, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 2, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 57, + "comment": "Aggregate nonce is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 58, + "comment": "Aggregate nonce is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 59, + "comment": "Aggregate nonce is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 60, + "comment": "Secret nonce's first half is out of range (all-zero nonce, which may indicate nonce reuse)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 3, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "first secnonce value is out of range." + } + }, + { + "tc_id": 61, + "comment": "Secret nonce's second half is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 4, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "second secnonce value is out of range." + } + }, + { + "tc_id": 62, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 63, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 3, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ], + "verify_fail_tests": [ + { + "tc_id": 64, + "comment": "Negated partial signature fails the verification equation", + "psig": "F75799540FB608F4827BD97F124873E24681D97EA63D00E42313C11CB64106B4", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 65, + "comment": "A valid partial signature checked against the wrong signer fails the verification equation", + "psig": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 1, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 66, + "comment": "Partial signature equals the group order, which is out of range", + "psig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + } + ], + "verify_error_tests": [ + { + "tc_id": 67, + "comment": "Verification rejects an invalid public nonce, blaming the malicious signer", + "psig": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [3, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + }, + { + "tc_id": 68, + "comment": "A public share is not a valid point", + "psig": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D", + "ids": [0, 1, 2], + "pubshare_indices": [3, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 0." + } + } + ] + }, + { + "tg_id": "3of5", + "t": 3, + "n": 5, + "thresh_pk": "03E1AEA00A7B8D0E0393664FEBBD31569A02CAF223D9AD83E90331DC18B1987358", + "pubshares": [ + "02F333EDD2B6F532C2EF6C42E803B633A42289D0567A75C50EB1C8CF85EC480E8F", + "025322F63602ECCF146541B7C9EE4D1FCB84EC139471420472182AE968676A387F", + "0230639CEC0A83982E176D6905CE0620FDC57E151565A39D0E0821B8E753AE85B0", + "026E1A50E672FEDD3F3C378D6121E5702B050669DBC241D2C2299880152CD207BD", + "02DC06C4CA919DE70B4B8281901CCF9471AB91FBFBEE1A8C570D83911CAAF9E2FA", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "pubnonces": [ + "02EBD21E4F4EA2772BD4E427D98C9DC268894D18EC45E14063785B892D42921FF402FB73028B710ACBE49FC2D995757E077760CA7C1B2452791A3BDA086B91A1CEF3", + "02BCBEF1171F78857016B205C0C9DE61750CC21448C167AAFAFD9190962AEAFF79032798E533A65F94D1DFF901A2CB4F107D49FE6B0527E95D7AE24E4DBBB681F552", + "02A455D731ECDCAA59228CE82CA1B5CF45974F6E6D21EB57501BE22310F089BF69037E2647E73C022A913C87D8813D48E76D72313DF4697644C23DDE820DBEF76D4A", + "026730E278BDB65A8E771864103890A1F10BA5AB9B429E0843CCB45FD2D9052C6A03525D89B7C9615F846D36223D777C5D6BF134FD1981FCB2F6B94F7FA70CE668AB", + "034D048ED3554BE5D0F03308936060FAAA8A0889FD135E8176DC0843C4FB68A89903497442C3AC05C98A8FDDB40646A6203A6EA53E20846F8D0A863447011BE8EA71", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0342B812EE2B499BAC6AAE1CC4F3B2E0739D0C3DCB558513DC737AAA4B7F5BF7DE023AFC98C8EBD47B2277E650056543F287E4585BC8419F472D6A3FC3E5D2B42D4A" + ], + "secshares": [ + "77C9316A64770B17D500840E020A9478F793D37DB0E6A276CB4E6270187D6C90", + "74180420A784189B55F8C4667075E89C2064FF481B6ED2A23A7517AF71F6B7A8", + "BE6B9FB07586EBABD234EEE0A05E813DFFC14D73782085C8DFE77C1FADDA7172", + "56C40419CE7F844949B5037C91C45E5FDAF9E11917B31BAEFBD33133FBF258AD", + "3D21315CB26DE273BC79023A44A780006CBD971FA96F34904E0A95792C74AE9A", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "secnonces": [ + "8A1160479C4188DA237C53639EDEFC60C24E87F91AC8972D966000680A7D07328A5318B81726CB9B16B9321264611C7DF0C737BB64941F79D403D2D503FAB3AB", + "38214C927297FD9F61722C93A6568A7CFE37D8BFA7C73822337FEA77AD97A325CB33A2D2CB486A6F2EE597D7ED9D5552DD096452E3AC9DFA9787019BAB7652EB", + "E3490427C987333662C649E5801683B8BFCB7E5F0725908DBC968AA86AF1FA2C10E4690766A547CA58136EF3DC1045A8C3ECEA532A057F1A1AF1E42761E0ECD7", + "EE855A53463521DECAFA1132D121644CF0BBA4A860456F26540261023D9A49910E40FA146B6B9D4B8914D57D0B14A03BF5C753067CC144C69C14D924F037EF9E", + "CE5F5A722536E56561C5C1258A8E6A38480A7D00F79645460CCA3DE7F10E480308EF46C644D01F0FCB05950F384C655198101BDD83C604A8484FCCB49914CC39", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "8A1160479C4188DA237C53639EDEFC60C24E87F91AC8972D966000680A7D07320000000000000000000000000000000000000000000000000000000000000000" + ], + "valid_tests": [ + { + "tc_id": 69, + "comment": "Minimum threshold subset of signers", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3" + }, + { + "tc_id": 70, + "comment": "Reordering the signer set leaves the partial signature unchanged, because the identifiers are sorted before they are bound into the binding value", + "my_id": 0, + "ids": [2, 1, 0], + "pubshare_indices": [2, 1, 0], + "pubnonce_indices": [2, 1, 0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3" + }, + { + "tc_id": 71, + "comment": "A different threshold subset gives a different partial signature, since the Lagrange coefficients depend on the signer set", + "my_id": 0, + "ids": [0, 3, 4], + "pubshare_indices": [0, 3, 4], + "pubnonce_indices": [0, 3, 4], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "03CE3C1C182829F31DE2E6FD057A06CFA52376FEE42C3FBD487C19CFF3B5C70DC5020D9DD41C4A2B9B43DB7C4E93B7C504946446636F25C5D893E90C626286D90765", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "BB2E12DF70774F9F5583A620F0F73217FA9C21E27A4C065D3A9A69D90195ADBF" + }, + { + "tc_id": 72, + "comment": "All signers participate, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2, 3, 4], + "pubshare_indices": [0, 1, 2, 3, 4], + "pubnonce_indices": [0, 1, 2, 3, 4], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "03536024F21D3C18E0F094D15E9DDA07740754905570A1B68CAFAC743A6C213745037C9EA6730CD21904C131FF4A6C62480A7BABAA75F0A4F63F1AFD8DEE9FDD699D", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "EE624D5540B13082E7F4385188485CB757978F2A84A3E72B7D09DE1760558AAC" + }, + { + "tc_id": 73, + "comment": "Aggregate nonce is the point at infinity, so the final nonce point falls back to the generator G", + "my_id": 0, + "ids": [0, 1, 2, 3, 4], + "pubshare_indices": [0, 1, 2, 3, 4], + "pubnonce_indices": [0, 1, 2, 3, 6], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "expected": "559961C1086B45BA3E083E32EF837A2636ADA8BDE5FF0F1823ADDE9A414A5AB1" + }, + { + "tc_id": 74, + "comment": "Empty message", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "", + "expected": "8943BF49C69B9243584C218666141D0C4D13ED1FE42A183340619E33FE514844" + }, + { + "tc_id": 75, + "comment": "Non-standard message length (38 bytes)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "expected": "6A950ECF293909B8AD4C59E09399626C6577664FE43B1D34F7423ABE41F1DF9A" + } + ], + "sign_error_tests": [ + { + "tc_id": 76, + "comment": "Signer's identifier is absent from the signer set", + "my_id": 3, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's id must be present in the participant identifier list." + } + }, + { + "tc_id": 77, + "comment": "Signer set contains a duplicate id", + "my_id": 0, + "ids": [0, 1, 1], + "pubshare_indices": [0, 1, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier list contains duplicate elements." + } + }, + { + "tc_id": 78, + "comment": "Signer's public share is not in the public share list", + "my_id": 1, + "ids": [1, 2, 3], + "pubshare_indices": [1, 2, 3], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "027FEAEF20E0DF0EC419CF5D4FCE8CB18D14B051A1A3A0374E9AB42F2E637BCDC802F76F1B7F5C275C6278AA61138560262606C84E66478C0E20B3980D9B029F5F3C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's pubshare must be included in the list of pubshares." + } + }, + { + "tc_id": 79, + "comment": "A public share is not a valid point", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 5, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 1." + } + }, + { + "tc_id": 80, + "comment": "A signer id is outside the valid range [0, n-1]", + "my_id": 1, + "ids": [5, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The participant identifier at index 0 is out of range." + } + }, + { + "tc_id": 81, + "comment": "Signer set's public shares do not match the threshold public key", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 2, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The provided key material is incorrect." + } + }, + { + "tc_id": 82, + "comment": "Aggregate nonce is invalid: first half has an unknown tag 0x04", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 83, + "comment": "Aggregate nonce is invalid: second half is not a point on the curve", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 84, + "comment": "Aggregate nonce is invalid: second half's x-coordinate exceeds the field size", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": null, + "contrib": "aggnonce" + } + }, + { + "tc_id": 85, + "comment": "Secret nonce's first half is out of range (all-zero nonce, which may indicate nonce reuse)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 5, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "first secnonce value is out of range." + } + }, + { + "tc_id": 86, + "comment": "Secret nonce's second half is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 6, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "second secnonce value is out of range." + } + }, + { + "tc_id": 87, + "comment": "Fewer signers than the threshold t", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The number of signers must be between t and n." + } + }, + { + "tc_id": 88, + "comment": "Secret share is out of range (zero)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 5, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "The signer's secret share value is out of range." + } + } + ], + "verify_fail_tests": [ + { + "tc_id": 89, + "comment": "Negated partial signature fails the verification equation", + "psig": "699CF452DC0C9D09BE274772760FF94BFF7015ADA1C82C0227E561F03995066E", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 90, + "comment": "A valid partial signature checked against the wrong signer fails the verification equation", + "psig": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 1, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + }, + { + "tc_id": 91, + "comment": "Partial signature equals the group order, which is out of range", + "psig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF" + } + ], + "verify_error_tests": [ + { + "tc_id": 92, + "comment": "Verification rejects an invalid public nonce, blaming the malicious signer", + "psig": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [5, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "InvalidContributionError", + "signer_index": 0, + "contrib": "pubnonce" + } + }, + { + "tc_id": 93, + "comment": "A public share is not a valid point", + "psig": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3", + "ids": [0, 1, 2], + "pubshare_indices": [5, 1, 2], + "pubnonce_indices": [0, 1, 2], + "signer_index": 0, + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "error": { + "type": "ValueError", + "message": "Invalid pubshare at index 0." + } + } + ] + } + ] +} \ No newline at end of file diff --git a/bip-0445/python/vectors/test_vectors_summary.md b/bip-0445/python/vectors/test_vectors_summary.md new file mode 100644 index 0000000000..af8f3871a9 --- /dev/null +++ b/bip-0445/python/vectors/test_vectors_summary.md @@ -0,0 +1,126 @@ +# FROST Signing Test Vectors + +This folder holds the JSON test vectors for BIP-FROST signing. They're +generated by `python/gen_vectors.py` and checked by `python/tests.py`, and +they're meant for anyone porting the protocol to another language. Treat the +JSON as build output: edit `gen_vectors.py` and regenerate, don't hand-edit +the files. + +| File | API Tested | Test Case arrays | +|---|---|---| +| `nonce_gen_vectors.json` | `nonce_gen_internal` | `valid_tests` | +| `nonce_agg_vectors.json` | `nonce_agg` | `valid_tests`, `error_tests` | +| `sign_verify_vectors.json` | `sign`, `partial_sig_verify` | `valid_tests`, `sign_error_tests`, `verify_fail_tests`, `verify_error_tests` | +| `tweak_vectors.json` | `sign` with tweaks | `valid_tests`, `error_tests` | +| `det_sign_vectors.json` | `deterministic_sign` | `valid_tests`, `error_tests` | +| `sig_agg_vectors.json` | `partial_sig_agg` | `valid_tests`, `error_tests` | + +## How test cases are laid out + +Most files don't repeat shared inputs inside every test case. Instead each file +groups its cases under a `test_groups` array, one group per `(t, n)` +configuration, and every group carries the key setup and the inputs it shares +(`pubshares`, `pubnonces`, `secshares`, `secnonces`, `tweaks`). A case points into those shared inputs by index: a +field named `_index` picks one entry, and `_indices` picks an +ordered set. So to run a case, read its group's shared inputs, look up the +case's indices in them, and call the function with the values you get back. + +Within each shared input, entry `i` holds the value for the signer whose id is +`i`: `pubshares[i]`, `secshares[i]`, `pubnonces[i]`, and `secnonces[i]` all +belong to signer `i`, for the `n` real signers in a `test_group`. A few +deliberately bad or out-of-range entries sit after those. + +A valid case points each index straight at its matching signer, so its +`pubshare_indices` and `pubnonce_indices` just track its `ids` (signer `1` +reads index `1`, and so on). An error case breaks that pattern on purpose, +either pointing an index at one of the appended bad entries or mixing the +indices up, to trigger the failure it's testing. + +A few things stay out of that scheme: + +- `tg_id`, `n`, `t`, and `thresh_pk` are group-level scalars you read directly + (the `(t, n)` label and the shared key setup), not part of the index scheme. +- Per-session values stay inline in the case, with no shared input: the + message (`msg`), the aggregate nonce (`aggnonce`, or `aggothernonce` in + `det_sign`), and the signer's own id (`my_id`). +- `tweaks` and `is_xonly` are parallel lists: position `k` in `is_xonly` says + whether tweak `k` is x-only. They mirror the API, which takes the two as + separate arguments. `tweak_vectors` keeps its tweak values in a shared input + and selects them with `tweak_indices`, while `det_sign` inlines its `tweaks` + list directly in the case. +- A case's expected output stays inline, in its `expected` field. + +`nonce_gen` and `nonce_agg` skip the `test_groups` wrapper. `nonce_gen` is +single-signer and uses no shared key, so every case inlines its own inputs. +`nonce_agg` has no key material at all, so it keeps just a top-level +`pubnonces` shared input that its cases index into. + +## The key setups + +Every file except `nonce_gen` and `nonce_agg` carries four test groups, one +per `(t, n)` configuration: `2-of-3`, `1-of-3`, `3-of-3`, and `3-of-5`. A +group's `tg_id` names its configuration (for example `"3of5"`), and each group +is generated independently, so it has its own `t`, `n`, `thresh_pk`, and `n` +real signers with identifiers `0 .. n - 1`. The examples elsewhere in this +document use the `2-of-3` group. + +In every group, one bogus public share and an out-of-range identifier (the +value `n`) are appended after the real signers, but only error cases use them +to trigger "invalid public share" and out-of-range-id failures, so don't treat +them as valid signers. + +Valid cases exercise different threshold-sized subsets of the group's `n` +signers, so the `ids` list varies from case to case: a single signer in the +`1-of-3` group, three of five in `3-of-5`, and subsets like `[0, 1]`, `[1, 0]`, +`[0, 2]`, or the full `[0, 1, 2]` in the `n = 3` groups. + +The `secshares` and `secnonces` shared inputs carry the real secret material +for the group's `n` signers at indices `0 .. n - 1`, signer-aligned like the +public ones. After those, a few deliberately bad entries are appended that +only the error cases select: a zero secret share, an all-zero secret nonce, +and a secret nonce whose second half is zero. Each case's `secshare_index` and +`secnonce_index` pick out the secret material for the signer it signs as. + +## Case kinds + +Each case array has its own contract: + +- `valid_tests` must succeed and produce exactly the bytes in `expected`. + (`nonce_gen`'s `expected` is the pair `[secnonce, pubnonce]`, `nonce_agg`'s + is the aggregate nonce, and `det_sign`'s is the pair `[pubnonce, psig]`.) +- `error_tests` and `sign_error_tests` must raise the exception described in + the case's `error` object. +- `verify_fail_tests` must make `partial_sig_verify` return `False` without + raising. +- `verify_error_tests` must make `partial_sig_verify` raise. + +The `error` object comes in two shapes: + +```json +{ "type": "ValueError", "message": "" } +``` + +```json +{ "type": "InvalidContributionError", + "signer_index": , + "contrib": "pubnonce" | "aggnonce" | "aggothernonce" | "psig" } +``` + +`signer_index` is `null` when the bad contribution is aggregator-level (an +`aggnonce` or `aggothernonce`) rather than from a specific signer. + +## Signer identifiers + +Three nearby fields are easy to mix up. The distinction that matters: `my_id` +is an id value, while both `signer_index` fields are positions in a list. + +| Field | Where | What it is | +|---|---|---| +| `my_id` | sign-side cases (`sign`, `deterministic_sign`, tweak) | The signer's id value. Pass it straight to the function. | +| `signer_index` | verify-side cases (`verify_fail`, `verify_error`) | The signer's position in the case's `ids` list, passed as the index argument to `partial_sig_verify`. | +| `error.signer_index` | inside an `error` object | The position of the blamed contribution in the input list, or `null` for an aggregator-level fault. | + +## Comments + +Every case has a one-line `comment` describing what it's for. The JSON +fields are what's actually tested. diff --git a/bip-0445/python/vectors/tweak_vectors.json b/bip-0445/python/vectors/tweak_vectors.json new file mode 100644 index 0000000000..80603a5875 --- /dev/null +++ b/bip-0445/python/vectors/tweak_vectors.json @@ -0,0 +1,860 @@ +{ + "test_groups": [ + { + "tg_id": "2of3", + "t": 2, + "n": 3, + "thresh_pk": "02D772A09F5F675783D275ED9F6AAEDB2ECCBC74171B37AC23AE3BBD9D7AE2CDAA", + "pubshares": [ + "039EE3335AF48DFE23702AB353F4AF20D401F67A130DF783CC8457323A860A2FB4", + "0284DC4AB2CB78A621EB87FA1F14BCE2B725AFEAAC981ADCBAFF5CC2D417D2A63A", + "036441EC2D4C1266201CD89B69549A2F5B2188612A0D434153E625FB38173DD509" + ], + "pubnonces": [ + "03FC0D3E212D25027356A2C9BB7EDC3B900DEA5E32AB10C03868E01C8490F9D36202C61389B6469B4345E00640CDC7ECA301547B97D30FC11AF410E23AD7B4BF69EE", + "03706F00353EA69C0836BF63C2884D2DE5AFE549A33EC87427CA8B5CF4666B28AE021B6300D0545E0B225C2DFC67AE33A9B0259B62809740363717E05CDA4CC9EBFC", + "02B7F6EF566DD2DDAA6AF38371A41F80F93A90B9F407B5BDB8ED763F00103DB0FC039C677604E7B75FF6E47C42B0249A3BAA9CC6B17E68D6D78E21ABA9D04BF40745" + ], + "secshares": [ + "53442FA9BD72EEA0A42DF6F2D2D76A2C0D3A3DFA2BE2F820F41ADE976B8259FB", + "5A7F9BD41F4B544664C54D777D43303CB5302434F9903B9B552C4E552BF02201", + "61BB07FE8123B9EC255CA3FC27AEF64D5D260A6FC73D7F15B63DBE12EC5DEA07" + ], + "secnonces": [ + "67AFBE2B50F521569B9C1D943F8FE20620B8C25FA81E9788718AF6EE87D5275C9A485A634FE41558E4E108FBB52C5CF616C0CDF02BC0C10A99359FF6C1BD705C", + "6E5AABD814F30045744DC470103D44BCD846284D7A763F9BA3F5C73DCE20BF459FCEDE0DFFF09B9E0150CE5A2C05D3199B4B0987A18C49E9273DEADBBA40B4D6", + "762F4601AC206E1F1497EB541202B727CC19FA0450289C778B5D5F4F1A4B53F4D63D312CC2C9196D08FD62F3958FE8BBC66B8517D10DE534F6A641D0E6E5EBCB" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "B3F73C80A46577051C695F91D7945BE3556A85275112EB952CC8EFB32521AF4C", + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BBFF" + ], + "valid_tests": [ + { + "tc_id": 1, + "comment": "No tweaks applied", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [], + "is_xonly": [], + "expected": "2B69442F9BCE21BB722831A2150FB9A6DF6D0288D39E2E4F5687E92A3C4A7862" + }, + { + "tc_id": 2, + "comment": "Single x-only tweak (used for BIP341 Taproot)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [true], + "expected": "F714F362C7F861693F64DD33FFE89BB8D167E3D84B5EF9C9CBA02E5C108C87CB" + }, + { + "tc_id": 3, + "comment": "Single plain tweak (used for BIP32 derivation)", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [false], + "expected": "F714F362C7F861693F64DD33FFE89BB8D167E3D84B5EF9C9CBA02E5C108C87CB" + }, + { + "tc_id": 4, + "comment": "A plain tweak followed by an x-only tweak", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "expected": "E4A306FA4FCDC6DD5E8EA8978AF549D7A8F74C39EE9FA55B2C7853DDBEA59482" + }, + { + "tc_id": 5, + "comment": "Four tweaks alternating x-only and plain", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "expected": "67DE6D20964B5D58AAABCEA30959709662DD9246246D8C5C7224DF91440B0DB2" + }, + { + "tc_id": 6, + "comment": "Four tweaks: two plain followed by two x-only", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "pubnonce_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "1D7087F917EA80B6D01A1D6395338DFB5534C735CB202C9A0F6CC4EC32636EAD" + }, + { + "tc_id": 7, + "comment": "Same tweaks as the previous case but with all signers participating, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "02CBC2E41AF87290A1401D49927FC30D7F40A404C4DF3E5FC734014188DBC44E1503C15312EDC691EB213F3B8AAC195D251A390563CD5E313D738A89DAEB4DA80746", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "657710181BDF649430193A12D837C22C27E1B48B6974C17E5C7EAE90619BFF84" + } + ], + "error_tests": [ + { + "tc_id": 8, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [4], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 9, + "comment": "Plain tweak drives the tweaked threshold public key to the point at infinity", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [5], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The result of tweaking cannot be infinity." + } + }, + { + "tc_id": 10, + "comment": "Number of tweaks does not match the number of tweak modes", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The tweaks and is_xonly arrays must have the same length." + } + }, + { + "tc_id": 11, + "comment": "Tweak is not a 32-byte array", + "my_id": 0, + "ids": [0, 1], + "pubshare_indices": [0, 1], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030F261BFE5DF0AE3EC06BB7A1D6394A3AF206968281F03B70D25FB84D49036CE203BE48277630579C72160430C819AABB1DFA20FF70749666080BB886B728476261", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [6], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak must be a 32-byte array." + } + } + ] + }, + { + "tg_id": "1of3", + "t": 1, + "n": 3, + "thresh_pk": "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "pubshares": [ + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F", + "03D1939BE87D18EA5AB302E27077D16B2C2FD93516F91514BA049933D348DA441F" + ], + "pubnonces": [ + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57" + ], + "secshares": [ + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32", + "06D47E05E97481428654563E5AE69C20C49642773B7334220E63110259A30C32" + ], + "secnonces": [ + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190", + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190", + "85988D37E7F2EE21FC47295E90D0FD85027CE0E71DA37B36CFB55BD471D3BA82ACE9F73C19DCD387AEF30F9E894564A47F8BE85FAAAE710DD0F442DF2A380190" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "F92B81FA168B7EBD79ABA9C1A51963DDF6189A6F73D56C19B16F4D8A7693350F", + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BBFF" + ], + "valid_tests": [ + { + "tc_id": 12, + "comment": "No tweaks applied", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [], + "is_xonly": [], + "expected": "0662F783275AEB48C647E93D941AD63AF0A53F05C31C19153D203D68DAFF2831" + }, + { + "tc_id": 13, + "comment": "Single x-only tweak (used for BIP341 Taproot)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [true], + "expected": "6A0F1F619F01E41CD05911AAF4C53D7F650454D238D44F9E2F7D3FEA2C65A112" + }, + { + "tc_id": 14, + "comment": "Single plain tweak (used for BIP32 derivation)", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [false], + "expected": "BCCADF64B166EFC477A89F7DC805CA4429FC5509F882CA073384772F2CEA84D4" + }, + { + "tc_id": 15, + "comment": "A plain tweak followed by an x-only tweak", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "expected": "3139EF83EF488AC03D078077EFDC42DAD9B93799C877FF527B6EFD87072D99E2" + }, + { + "tc_id": 16, + "comment": "Four tweaks alternating x-only and plain", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "expected": "C45C3034CBB4B0F37B880E20CF54263618187333885D92EC5C53732BFB368D07" + }, + { + "tc_id": 17, + "comment": "Four tweaks: two plain followed by two x-only", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "pubnonce_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "8D736A8EC51940189121B993ECC27FC6D411761A4C6E935FB37EDA26FFB70969" + }, + { + "tc_id": 18, + "comment": "Same tweaks as the previous case but with all signers participating, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "0277E716756BAEFD922DCD339D083FEB1C0263B06999328492331FB876A225EEA3033910C4FC151C7AA914B4D2AC97B3F09C56A36A5F95F14043D90210CC1FCD4245", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "7C23569D6ABC6B7DD9D09AA98CAF85FE58D1C0AB9F2B7F0DADB64457868B7E54" + } + ], + "error_tests": [ + { + "tc_id": 19, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [4], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 20, + "comment": "Plain tweak drives the tweaked threshold public key to the point at infinity", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [5], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The result of tweaking cannot be infinity." + } + }, + { + "tc_id": 21, + "comment": "Number of tweaks does not match the number of tweak modes", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The tweaks and is_xonly arrays must have the same length." + } + }, + { + "tc_id": 22, + "comment": "Tweak is not a 32-byte array", + "my_id": 0, + "ids": [0], + "pubshare_indices": [0], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "026805DC7B318F796C360BB82F85FA0C106AB8775FECF892A0A7F85FB66720CBAB039509CF07C0A787AB8628C84E1F6F5E9CA424496D6ED4AC36722E5602E0C32C57", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [6], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak must be a 32-byte array." + } + } + ] + }, + { + "tg_id": "3of3", + "t": 3, + "n": 3, + "thresh_pk": "023E94D6A68620D3F60221A4186786274A711221A8BBB31A5388DFA6718E8EF7BB", + "pubshares": [ + "03A3E932BA9DD0063D721590B6EF21DD4566A97B203343FDE9DA5815DB9BE049CA", + "0201E5FEAC0DB5059C32B045F37799101A96262B3C88CE85D610451FC21A0457FF", + "03EF72D10D1DFE4E786619536A458507CAF105FCCDB8173DAF2E358054AF2FA4A2" + ], + "pubnonces": [ + "02B6418EDDC3E2D48B064BE46F71CEDE3A34594DEAC6E7F6707C8A71CB523EA58602C0B402216E9952E649B8CCFEDBAC6AA10BAEEFF4C02EEAA1148E87C7B21BDFB7", + "02B63A2B319D64A7BFE6634404CB9D80AAA57BAFD8AA01FC52BF77232DC0EE4B600358E8B8F6FB17FB5D92FAFAC1093773645FE32330D6E2774B4BBBCEF7DDFA5102", + "0364F60E66B7BC35A332B4F9688BD5CA2EF508E25C691D6A1A194097D170BBCCE602494AB47255521F0134EC7C41079A8955DA7220172A60371DC32C696AE5D61410" + ], + "secshares": [ + "D4DE571C662C319E7C4F46420F5ACBEFD2D905C3B52771C553DE1F76197D940D", + "0EAAC017ABA2030272B7F8EAAD68E22BF07F08950D7B570B69B2CCEF278D52C1", + "1E4E4344B9B59410588CCF327B766E4D298882D904F0FBF288C86B73AC5962A3" + ], + "secnonces": [ + "7B2247A3980C88714A0821F230F689F436FBF80B0B6DED33540B92CB65BA6ED149A8ADAF97EF6DBAA6015CD5BA84E4CAADB7A453508DD17FD8EE3618E119C0F9", + "B1456FA8F924860A665AFC2AE2746E6E2C99EBBE4BC1D0F5BD00BC52CB7DFD79A823D92911393852D4ECCC59387DB88C526FD1AD7574A3336893B618750DBDDD", + "C309D4BC0457A9D6031CDAC33BAFA47BAB2334445BF22D09E1936F919756D275BC09410F5D39E5D701C81BBA99E4E95F9E07FFC32C322F25604582CB242DD46A" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "8F16F7AD16ABE01B8AAD48C75EB3D4635F761C4F11E49492F82CB89DEE789D3C", + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BBFF" + ], + "valid_tests": [ + { + "tc_id": 23, + "comment": "No tweaks applied", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [], + "is_xonly": [], + "expected": "08A866ABF049F70B7D842680EDB78C1C742D0368090B9F579CBE9D7019F53A8D" + }, + { + "tc_id": 24, + "comment": "Single x-only tweak (used for BIP341 Taproot)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [true], + "expected": "C8EFFDEE3AFD0E8CA02BFA16900E5AA2C00F3D64881A1983D77C6B5A64C0BC3C" + }, + { + "tc_id": 25, + "comment": "Single plain tweak (used for BIP32 derivation)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [false], + "expected": "C8EFFDEE3AFD0E8CA02BFA16900E5AA2C00F3D64881A1983D77C6B5A64C0BC3C" + }, + { + "tc_id": 26, + "comment": "A plain tweak followed by an x-only tweak", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "expected": "15F4431495CDBB8930535A4EF31DA2C66D1DC3CDA13C182655A6961CD280F9F7" + }, + { + "tc_id": 27, + "comment": "Four tweaks alternating x-only and plain", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "expected": "C6EF3457001788495D014A758DC8D540E2D8A6E478D7E2A3204D18B6D97BB632" + }, + { + "tc_id": 28, + "comment": "Four tweaks: two plain followed by two x-only", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "468D28680CA66CBDBD6F5B0500D055E778806CC7CA72A2A5D08B8E8E1F5B35D1" + }, + { + "tc_id": 29, + "comment": "Same tweaks as the previous case but with all signers participating, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "0A2650D22510951BD711B57E0F4D3345B7000F40302043D4819EEE9D89CE431A" + } + ], + "error_tests": [ + { + "tc_id": 30, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [4], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 31, + "comment": "Plain tweak drives the tweaked threshold public key to the point at infinity", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [5], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The result of tweaking cannot be infinity." + } + }, + { + "tc_id": 32, + "comment": "Number of tweaks does not match the number of tweak modes", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The tweaks and is_xonly arrays must have the same length." + } + }, + { + "tc_id": 33, + "comment": "Tweak is not a 32-byte array", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "030CC8DD7811033474FAD57D2DD03801584ED6A68CD1A73B3301C3DD1F685BBF7802DE9250BDBEF74C5EC1854A171EC8F5BD06CBDDCFB78384435AC62E5B359ACF2C", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [6], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak must be a 32-byte array." + } + } + ] + }, + { + "tg_id": "3of5", + "t": 3, + "n": 5, + "thresh_pk": "03E1AEA00A7B8D0E0393664FEBBD31569A02CAF223D9AD83E90331DC18B1987358", + "pubshares": [ + "02F333EDD2B6F532C2EF6C42E803B633A42289D0567A75C50EB1C8CF85EC480E8F", + "025322F63602ECCF146541B7C9EE4D1FCB84EC139471420472182AE968676A387F", + "0230639CEC0A83982E176D6905CE0620FDC57E151565A39D0E0821B8E753AE85B0", + "026E1A50E672FEDD3F3C378D6121E5702B050669DBC241D2C2299880152CD207BD", + "02DC06C4CA919DE70B4B8281901CCF9471AB91FBFBEE1A8C570D83911CAAF9E2FA" + ], + "pubnonces": [ + "02EBD21E4F4EA2772BD4E427D98C9DC268894D18EC45E14063785B892D42921FF402FB73028B710ACBE49FC2D995757E077760CA7C1B2452791A3BDA086B91A1CEF3", + "02BCBEF1171F78857016B205C0C9DE61750CC21448C167AAFAFD9190962AEAFF79032798E533A65F94D1DFF901A2CB4F107D49FE6B0527E95D7AE24E4DBBB681F552", + "02A455D731ECDCAA59228CE82CA1B5CF45974F6E6D21EB57501BE22310F089BF69037E2647E73C022A913C87D8813D48E76D72313DF4697644C23DDE820DBEF76D4A", + "026730E278BDB65A8E771864103890A1F10BA5AB9B429E0843CCB45FD2D9052C6A03525D89B7C9615F846D36223D777C5D6BF134FD1981FCB2F6B94F7FA70CE668AB", + "034D048ED3554BE5D0F03308936060FAAA8A0889FD135E8176DC0843C4FB68A89903497442C3AC05C98A8FDDB40646A6203A6EA53E20846F8D0A863447011BE8EA71" + ], + "secshares": [ + "77C9316A64770B17D500840E020A9478F793D37DB0E6A276CB4E6270187D6C90", + "74180420A784189B55F8C4667075E89C2064FF481B6ED2A23A7517AF71F6B7A8", + "BE6B9FB07586EBABD234EEE0A05E813DFFC14D73782085C8DFE77C1FADDA7172", + "56C40419CE7F844949B5037C91C45E5FDAF9E11917B31BAEFBD33133FBF258AD", + "3D21315CB26DE273BC79023A44A780006CBD971FA96F34904E0A95792C74AE9A" + ], + "secnonces": [ + "8A1160479C4188DA237C53639EDEFC60C24E87F91AC8972D966000680A7D07328A5318B81726CB9B16B9321264611C7DF0C737BB64941F79D403D2D503FAB3AB", + "38214C927297FD9F61722C93A6568A7CFE37D8BFA7C73822337FEA77AD97A325CB33A2D2CB486A6F2EE597D7ED9D5552DD096452E3AC9DFA9787019BAB7652EB", + "E3490427C987333662C649E5801683B8BFCB7E5F0725908DBC968AA86AF1FA2C10E4690766A547CA58136EF3DC1045A8C3ECEA532A057F1A1AF1E42761E0ECD7", + "EE855A53463521DECAFA1132D121644CF0BBA4A860456F26540261023D9A49910E40FA146B6B9D4B8914D57D0B14A03BF5C753067CC144C69C14D924F037EF9E", + "CE5F5A722536E56561C5C1258A8E6A38480A7D00F79645460CCA3DE7F10E480308EF46C644D01F0FCB05950F384C655198101BDD83C604A8484FCCB49914CC39" + ], + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "3680D87253A03CDEB0B3D228AAE37B2A356112D276C0AAF52D5F022B2EC7B117", + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BBFF" + ], + "valid_tests": [ + { + "tc_id": 34, + "comment": "No tweaks applied", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [], + "is_xonly": [], + "expected": "96630BAD23F362F641D8B88D89F006B2BB3EC7390D80743997ECFC9C96A13AD3" + }, + { + "tc_id": 35, + "comment": "Single x-only tweak (used for BIP341 Taproot)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [true], + "expected": "37A44A2791830833444BC5E6393B1F507A836B421E9D8269E2282A74B52D2F3E" + }, + { + "tc_id": 36, + "comment": "Single plain tweak (used for BIP32 derivation)", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [false], + "expected": "71BEE91B3D4A2DF955FC25E336F5BFA32607232580CF3E0E080749B30E5BBF4D" + }, + { + "tc_id": 37, + "comment": "A plain tweak followed by an x-only tweak", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "expected": "D8D9171548E72DB21F403FA51902DB6FB73C64FB2E4512F8C84F4228EAEF9A7E" + }, + { + "tc_id": 38, + "comment": "Four tweaks alternating x-only and plain", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "expected": "4E5B190157C2BDA2C0DA2630CC7D47F39F36D50BDE05B7FBCEE9B1317E721912" + }, + { + "tc_id": 39, + "comment": "Four tweaks: two plain followed by two x-only", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "pubnonce_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "25113209B8EEB8E296DC6327AEF2DBC8F2F8562E41070472FEF2483345C34CB6" + }, + { + "tc_id": 40, + "comment": "Same tweaks as the previous case but with all signers participating, signed by a non-first member of the signer set", + "my_id": 1, + "ids": [0, 1, 2, 3, 4], + "pubshare_indices": [0, 1, 2, 3, 4], + "pubnonce_indices": [0, 1, 2, 3, 4], + "secshare_index": 1, + "secnonce_index": 1, + "aggnonce": "03536024F21D3C18E0F094D15E9DDA07740754905570A1B68CAFAC743A6C213745037C9EA6730CD21904C131FF4A6C62480A7BABAA75F0A4F63F1AFD8DEE9FDD699D", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "expected": "B87A1F629AA546EA9FB95FBC3B8E234AB93FEE695CFCB76C130693CFF8D45051" + } + ], + "error_tests": [ + { + "tc_id": 41, + "comment": "Tweak exceeds the group order", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [4], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak value is out of range." + } + }, + { + "tc_id": 42, + "comment": "Plain tweak drives the tweaked threshold public key to the point at infinity", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [5], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The result of tweaking cannot be infinity." + } + }, + { + "tc_id": 43, + "comment": "Number of tweaks does not match the number of tweak modes", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [0], + "is_xonly": [], + "error": { + "type": "ValueError", + "message": "The tweaks and is_xonly arrays must have the same length." + } + }, + { + "tc_id": 44, + "comment": "Tweak is not a 32-byte array", + "my_id": 0, + "ids": [0, 1, 2], + "pubshare_indices": [0, 1, 2], + "secshare_index": 0, + "secnonce_index": 0, + "aggnonce": "0391D52F656B3CA3E66AC5DAEF5B0D347F7E85B7DD7A9EBA10E9827AE23FB76E93031610E184E540470139F7626253F27E8871D6354941CC9FF94A07D83F61BB4CD5", + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "tweak_indices": [6], + "is_xonly": [false], + "error": { + "type": "ValueError", + "message": "The tweak must be a 32-byte array." + } + } + ] + } + ] +} \ No newline at end of file