From 2bcec131592e47f5588b36d6115c4c12a3b6a8e0 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Mon, 17 Apr 2023 14:25:25 +0100 Subject: [PATCH 01/22] RFC 10: ROAST Schnorr signatures --- docs/rfc/rfc-10.adoc | 613 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 docs/rfc/rfc-10.adoc diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc new file mode 100644 index 000000000..74d4687fe --- /dev/null +++ b/docs/rfc/rfc-10.adoc @@ -0,0 +1,613 @@ +:toc: macro + += RFC 10: ROAST + +:icons: font +:numbered: +toc::[] + +== Background + +tBTC requires a threshold signature scheme recognised by BTC. +Until 2021 ECDSA was the only available option, +but with the introduction of Taproot we now have the option of using Schnorr signatures instead. +Schnorr signatures enable significantly easier attributability of misbehaviour during signing. + +=== Current functionality + +The protocol currently used in tBTC (GG18) does not provide attributability +when signing fails due to one or more members misbehaving. +Because of this, the only solution is to retry the signing +until a set of signers with no misbehaving participants is stumbled upon. +This means that even a small group of misbehaving participants +can cause a dramatic slowdown in signing, +rendering the system vulnerable to severe DoS attacks. + +Previously, the goal was to replace GG18 with CGGMP21, +which has attributable misbehaviour at the cost of significant implementation complexity. + +== Proposal + +Replace ECDSA with Schnorr signatures, +enabling the use of ROAST with FROST threshold signatures instead of CGGMP21. +Use GJKR for distributed key generation of signature shares for FROST. + +This reduces the complexity of key generation, +and makes signature production significantly simpler and faster +while providing the attributability missing from the current tECDSA implementation. + +=== Requirements + +The Taproot upgrade for Bitcoin enabled Schnorr signature support in 2021. +BIP-340 defines these Schnorr signatures, +and our implementation must conform to it. + +Schnorr signatures are incompatible with existing Bitcoin scripts, +requiring the use of taproot scripts instead. + +=== BIP-340 + +link:https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki[BIP-340] defines Schnorr signatures for the curve secp256k1, +already widely used in Bitcoin. +A signature for message m and public key point P +is defined as the (point, scalar) pair (R, s) +where s * G = R + hash(R || P || m) * P. + +The points P and R are encoded as the X-coordinate, +and the Y-coordinate is defined to be even. +This gives a 32-byte encoding +that corresponds to the standard 33-byte compressed format with the prefix 0x02. + +The hash function for BIP340 signatures hash(tag, x) is defined as +SHA256(SHA256(tag) || SHA256(tag) || x) +where tag is a context-dependent domain separator. + +For our purposes, we will consider the verification specification: + +Input: + +* The public key pk: a 32-byte array +* The message m: a 32-byte array +* A signature sig: a 64-byte array + +The algorithm Verify(pk, m, sig) is defined as: + +* Let P = lift_x(int(pk)); fail if that fails. +* Let r = int(sig[0:32]); fail if r ≥ p. +* Let s = int(sig[32:64]); fail if s ≥ n. +* Let e = int(hash(BIP0340/challenge, bytes(r) || bytes(P) || m)) mod n. +* Let R = s⋅G - e⋅P. +* Fail if is_infinite(R). +* Fail if not has_even_y(R). +* Fail if x(R) ≠ r. +* Return success iff no failure occurred before reaching this point. + +For every valid secret key sk and message m, Verify(PubKey(sk),m,Sign(sk,m)) will succeed. + +We see that the signature is a concatenation of +a 32-byte x-coordinate or a curve point R, +and a 32-byte scalar s. +The hash function uses the tag BIP0340/challenge. + +=== FROST + +link:https://www.ietf.org/id/draft-irtf-cfrg-frost-12.html[FROST (Flexible Round-Optimized Schnorr Threshold)] signing protocol +is a protocol for generating valid Schnorr signatures for a public key PK +which corresponds to a secret key sk shared among n participants using Shamir's method. + +==== Compatibility with BIP-340 + +These signatures are of the form (R, z) where R is a curve point and z a scalar, +and are verified against a public key PK and message msg as follows: + + prime_order_verify(msg, sig, PK): + + Inputs: + - msg, signed message, a byte string. + - sig, a tuple (R, z) output from signature generation. + - PK, public key, an Element. + + Outputs: + - True if signature is valid, and False otherwise. + + def prime_order_verify(msg, sig = (R, z), PK): + comm_enc = G.SerializeElement(R) + pk_enc = G.SerializeElement(PK) + challenge_input = comm_enc || pk_enc || msg + c = H2(challenge_input) + + l = G.ScalarBaseMult(z) + r = R + G.ScalarMult(PK, c) + return l == r + +Compared to BIP340, +we see that the point R matches in both, +and the scalar z of FROST corresponds to the scalar s of BIP340. + +After accounting for encoding differences, +we see that the hash function H2 producing the challenge c +must match the hash of BIP340 used to compute the scalar e. +This is the only required deviation from FROST as specified. +Otherwise an examination of the protocols +will show that a FROST-generated signature would pass BIP340 verification. + +==== Protocol + +FROST is a two-round protocol for generating t-of-n threshold Schnorr signatures +with the help of a semi-trusted coordinator (Alice). + +In the first round, each participant (Bobs) produces a pair of commitments +and sends them to the coordinator Alice. + +After the Alice has acquired a sufficient number of commintments, +she assembles a set of commitments from exactly t Bobs, +and sends it to those same Bobs, along with the message to sign. + +In the second round, each Bob calculates his signature share +using his secret key share, and the message and commitment list sent by Alice. +The Bobs then send their signature shares to Alice. + +Once Alice has received signature shares from all t Bobs, +she can aggregate them into a signature candidate. +If the signature candidate is not valid, +Alice can verify each signature share sent by the Bobs +and identify at least one misbehaving Bob +who sent an invalid share. +If Alice is misbehaving, +she can prevent the signature candidate from being created, +but can learn no secret information. + +A more detailed specification of the FROST protocol +is found in the draft RFC https://www.ietf.org/id/draft-irtf-cfrg-frost-12.html + +=== ROAST + +link:https://eprint.iacr.org/2022/550.pdf[ROAST (Robust Asynchronous Schnorr Threshold Signatures)] +is a wrapper for FROST specifying how to deal with misbehaving participants. + +In ROAST, Alice begins by requesting commitments from all Bobs. +As she receives valid messages from Bobs, +she adds those Bobs to the list of responsive signers R. + +Whenever there are t Bobs in R, +Alice assembles their commitments, asks them to produce a signature share, +and removes them from R. + +When a Bob produces a signature share, +he also produces new commitments and sends them to Alice alongside the share. + +When Alice receives a valid signature share and commitment from a Bob, +she adds that Bob back to R. +When Alice receives an invalid signature share from a Bob, +she does not add that bob back to R. +As a result, misbehaving or unresponsive Bobs +are eventually excluded from the executions of the FROST protocol, +and a valid signature will inevitably be produced by some set of Bobs +assuming at least t Bobs are honest, Alice is honest, +and all messages between Alice and Bobs are eventually delivered. + +To avoid the dependency on Alice's honesty, +the signers can choose (n - t + 1) Alices from among themselves, +ensuring that if at least t signers are honest +at least one Alice must also be honest and the protocol must succeed. + +== tBTC-ROAST + +The proposed adaptation of ROAST to produce BIP-340 compliant signatures +for the purpose of tBTC will be called tBTC-ROAST in this RFC. + +tBTC-ROAST has n = 100 participants in a signing group, +of whom t = 51 are required to cooperate to produce a signature. +The indices i of the members are in the range [1, 100] + +=== DKG + +The (51, 100) secret key for a tBTC-ROAST signing group (aka wallet) +is produced using GJKR. + +In the execution of the GJKR DKG protocol, +inactive and misbehaving operators are identified and removed from the wallet. + +=== Signing + +==== Coordinator selection + +When a wallet is required to sign message msg in ethereum block B, +one coordinator P_c is selected +by taking the block number modulo the group size, +and choosing the member whose index matches this number; +c = B % n = B % 100. + +P_c then executes the signing protocol as the coordinator. +If a valid signature is not produced by the time block B+1 is mined, +another coordinator P_c+1 is selected. +This continues until a valid signature is produced, +or until block B+99 when all members have become coordinators. + +==== Execution + +1. P_c sends everyone in the group a coordinator(P_c, msg) message. + +2. When a member P_i receives a coordinator(P_c, m) message, +they check if they have privately listed P_c as being unreliable/malicious, +whether m is a valid message to sign, +and whether P_c has been selected as a coordinator yet. +If both checks are good, P_i sends P_c a message commit(P_i, cc_i, m) +where cc_i is commit data for the Frost protocol. +P_i does this to all coordinators P_c passing these checks. + +3. When coordinator P_c receives a commit(P_j, cc_j, m) message, +they check if they consider P_j reliable +and whether m is the correct message to sign m == msg. +If these checks pass, +the commit message is added to the list of commitments cs. + +4. Once there are at least 51 commitments in cs, +P_c chooses 51 members Pks = [P_k1, ..., P_k51] +based on which commitments were received first. +P_c then assembles the list ks consisting of the pairs +[(P_k1, cc_k1), ..., (P_k51, cc_k51)] from those members +and sends them to all in Pks as message signRequest(P_c, ks, msg). +P_c also records the sent request as `requests[hash(ks)] = Pks`. + +5. When a member P_i who has previously sent a commit message to P_c +receives a message signRequest(P_c, ks, m) from P_c +and m matches the message P_i intended to sign, +and cc_i in ks matches the commitment P_i made earlier, +P_i calculates the signature share s_i and new commit data cc'_i, +and sends P_c the messages sign(P_i, s_i, m, hash(ks)) and commit(P_i, cc_i', m). +The member P_i then goes on standby, +waiting to execute phase 5 again if another signing request comes from the same coordinator, +or phase 2 with a new coordinator. + +6. When member P_c receives sign(P_j, s_j, m, h) from P_j, +they check if P_j was in list Pks = requests[h]. +If yes, they validate s_j and add it to the list of signature shares sks = shares[h]. +The coordinator simultaneously executes step 3 again with the commit(P_j, cc_j', m) message, +possibly following up with step 4 as well. +If s_j fails the validation, +P_c adds P_j to its list of bad participants, ignores P_j's commit message, +and returns to step 3. + +7. Once some sks has all 51 valid shares in it, P_c tries to assemble a signature s. +If the signature is successful, they send a success(s, m) message to all other members +so they know to abort the signing for m. +P_c can now clear all data used in the execution of the protocol. + +8. When member P_i receives a message success(s, m) from any other member P_j, +they check if s is a valid signature for m, +and that m is the correct message to sign. +If s and m are valid, they stop executing whatever step they were in +and clear all data for the execution. +If s is invalid, P_i adds P_j to the list of bad participants. +If m is invalid but s is valid, +P_i should probably raise an alarm over the fraudulent signature +but in practice this shouldn't happen and is out of scope for this RFC. + +==== Details + +The below details have been replicated from the FROST paper for convenience: + +===== Nonce generation + + nonce_generate(secret): + + Inputs: + - secret, a Scalar. + + Outputs: + - nonce, a Scalar. + + def nonce_generate(secret): + random_bytes = random_bytes(32) + secret_enc = G.SerializeScalar(secret) + return H3(random_bytes || secret_enc) + +===== Polynomial interpolation + + derive_interpolating_value(x_i, L): + + Inputs: + - x_i, an x-coordinate contained in L, a NonZeroScalar. + - L, the set of x-coordinates, each a NonZeroScalar. + + Outputs: + - value, a Scalar. + + Errors: + - "invalid parameters", if 1) x_i is not in L, or if 2) any + x-coordinate is represented more than once in L. + + def derive_interpolating_value(x_i, L): + if x_i not in L: + raise "invalid parameters" + for x_j in L: + if count(x_j, L) > 1: + raise "invalid parameters" + + numerator = Scalar(1) + denominator = Scalar(1) + for x_j in L: + if x_j == x_i: continue + numerator *= x_j + denominator *= x_j - x_i + + value = numerator / denominator + return value + +===== Encode commitments to a byte string + + Inputs: + - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], + a list of commitments issued by each participant, where each element in the list + indicates a NonZeroScalar identifier i and two commitment Element values + (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list MUST be sorted + in ascending order by identifier. + + Outputs: + - encoded_group_commitment, the serialized representation of commitment_list, a byte string. + + def encode_group_commitment_list(commitment_list): + encoded_group_commitment = nil + for (identifier, hiding_nonce_commitment, binding_nonce_commitment) in commitment_list: + encoded_commitment = G.SerializeScalar(identifier) || + G.SerializeElement(hiding_nonce_commitment) || + G.SerializeElement(binding_nonce_commitment) + encoded_group_commitment = encoded_group_commitment || encoded_commitment + return encoded_group_commitment + +===== Extract identifiers from a commitment list + + Inputs: + - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], + a list of commitments issued by each participant, where each element in the list + indicates a NonZeroScalar identifier i and two commitment Element values + (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list MUST be sorted + in ascending order by identifier. + + Outputs: + - identifiers, a list of NonZeroScalar values. + + def participants_from_commitment_list(commitment_list): + identifiers = [] + for (identifier, _, _) in commitment_list: + identifiers.append(identifier) + return identifiers + +===== Extract a blinding factor from a list of blinding factors + + Inputs: + - binding_factor_list = [(i, binding_factor), ...], + a list of binding factors for each participant, where each element in the list + indicates a NonZeroScalar identifier i and Scalar binding factor. + - identifier, participant identifier, a NonZeroScalar. + + Outputs: + - binding_factor, a Scalar. + + Errors: + - "invalid participant", when the designated participant is not known. + + def binding_factor_for_participant(binding_factor_list, identifier): + for (i, binding_factor) in binding_factor_list: + if identifier == i: + return binding_factor + raise "invalid participant" + +===== Blinding factors computation + + Inputs: + - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], + a list of commitments issued by each participant, where each element in the list + indicates a NonZeroScalar identifier i and two commitment Element values + (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list MUST be sorted + in ascending order by identifier. + - msg, the message to be signed. + + Outputs: + - binding_factor_list, a list of (NonZeroScalar, Scalar) tuples representing the binding factors. + + def compute_binding_factors(commitment_list, msg): + msg_hash = H4(msg) + encoded_commitment_hash = H5(encode_group_commitment_list(commitment_list)) + rho_input_prefix = msg_hash || encoded_commitment_hash + + binding_factor_list = [] + for (identifier, hiding_nonce_commitment, binding_nonce_commitment) in commitment_list: + rho_input = rho_input_prefix || G.SerializeScalar(identifier) + binding_factor = H1(rho_input) + binding_factor_list.append((identifier, binding_factor)) + return binding_factor_list + +===== Group commitment computation + + Inputs: + - commitment_list = + [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list + of commitments issued by each participant, where each element in the list + indicates a NonZeroScalar identifier i and two commitment Element values + (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list MUST be + sorted in ascending order by identifier. + - binding_factor_list = [(i, binding_factor), ...], + a list of (NonZeroScalar, Scalar) tuples representing the binding factor Scalar + for the given identifier. + + Outputs: + - group_commitment, an Element. + + def compute_group_commitment(commitment_list, binding_factor_list): + group_commitment = G.Identity() + for (identifier, hiding_nonce_commitment, binding_nonce_commitment) in commitment_list: + binding_factor = binding_factor_for_participant(binding_factor_list, identifier) + group_commitment = group_commitment + + hiding_nonce_commitment + G.ScalarMult(binding_nonce_commitment, binding_factor) + return group_commitment + +===== Signature challenge computation + + Inputs: + - group_commitment, the group commitment, an Element. + - group_public_key, the public key corresponding to the group signing key, an + Element. + - msg, the message to be signed, a byte string. + + Outputs: + - challenge, a Scalar. + + def compute_challenge(group_commitment, group_public_key, msg): + group_comm_enc = G.SerializeElement(group_commitment) + group_public_key_enc = G.SerializeElement(group_public_key) + challenge_input = group_comm_enc || group_public_key_enc || msg + challenge = H2(challenge_input) + return challenge + +===== Round one: commitment + + Inputs: + - sk_i, the secret key share, a Scalar. + + Outputs: + - (nonce, comm), a tuple of nonce and nonce commitment pairs, + where each value in the nonce pair is a Scalar and each value in + the nonce commitment pair is an Element. + + def commit(sk_i): + hiding_nonce = nonce_generate(sk_i) + binding_nonce = nonce_generate(sk_i) + hiding_nonce_commitment = G.ScalarBaseMult(hiding_nonce) + binding_nonce_commitment = G.ScalarBaseMult(binding_nonce) + nonce = (hiding_nonce, binding_nonce) + comm = (hiding_nonce_commitment, binding_nonce_commitment) + return (nonce, comm) + +===== Round two: signature share generation + + Inputs: + - identifier, identifier i of the participant, a NonZeroScalar. + - sk_i, Signer secret key share, a Scalar. + - group_public_key, public key corresponding to the group signing key, + an Element. + - nonce_i, pair of Scalar values (hiding_nonce, binding_nonce) generated in + round one. + - msg, the message to be signed, a byte string. + - commitment_list = + [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a + list of commitments issued in Round 1 by each participant and sent by the Coordinator. + Each element in the list indicates a NonZeroScalar identifier j and two commitment + Element values (hiding_nonce_commitment_j, binding_nonce_commitment_j). + This list MUST be sorted in ascending order by identifier. + + Outputs: + - sig_share, a signature share, a Scalar. + + def sign(identifier, sk_i, group_public_key, nonce_i, msg, commitment_list): + # Compute the binding factor(s) + binding_factor_list = compute_binding_factors(commitment_list, msg) + binding_factor = binding_factor_for_participant(binding_factor_list, identifier) + + # Compute the group commitment + group_commitment = compute_group_commitment(commitment_list, binding_factor_list) + + # Compute the interpolating value + participant_list = participants_from_commitment_list(commitment_list) + lambda_i = derive_interpolating_value(identifier, participant_list) + + # Compute the per-message challenge + challenge = compute_challenge(group_commitment, group_public_key, msg) + + # Compute the signature share + (hiding_nonce, binding_nonce) = nonce_i + sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * sk_i * challenge) + + return sig_share + +===== Signature share aggregation + + Inputs: + - commitment_list = + [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a + list of commitments issued in Round 1 by each participant, where each element + in the list indicates a NonZeroScalar identifier j and two commitment + Element values (hiding_nonce_commitment_j, binding_nonce_commitment_j). + This list MUST be sorted in ascending order by identifier. + - msg, the message to be signed, a byte string. + - sig_shares, a set of signature shares z_i, Scalar values, for each participant, + of length NUM_PARTICIPANTS, where MIN_PARTICIPANTS <= NUM_PARTICIPANTS <= MAX_PARTICIPANTS. + + Outputs: + - (R, z), a Schnorr signature consisting of an Element R and Scalar z. + + def aggregate(commitment_list, msg, sig_shares): + # Compute the binding factors + binding_factor_list = compute_binding_factors(commitment_list, msg) + + # Compute the group commitment + group_commitment = compute_group_commitment(commitment_list, binding_factor_list) + + # Compute aggregated signature + z = Scalar(0) + for z_i in sig_shares: + z = z + z_i + return (group_commitment, z) + +===== Signature share verification + + Inputs: + - identifier, identifier i of the participant, a NonZeroScalar. + - PK_i, the public key for the i-th participant, where PK_i = G.ScalarBaseMult(sk_i), + an Element. + - comm_i, pair of Element values in G (hiding_nonce_commitment, binding_nonce_commitment) + generated in round one from the i-th participant. + - sig_share_i, a Scalar value indicating the signature share as produced in + round two from the i-th participant. + - commitment_list = + [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a + list of commitments issued in Round 1 by each participant, where each element + in the list indicates a NonZeroScalar identifier j and two commitment + Element values (hiding_nonce_commitment_j, binding_nonce_commitment_j). + This list MUST be sorted in ascending order by identifier. + - group_public_key, public key corresponding to the group signing key, + an Element. + - msg, the message to be signed, a byte string. + + Outputs: + - True if the signature share is valid, and False otherwise. + + def verify_signature_share(identifier, PK_i, comm_i, sig_share_i, commitment_list, + group_public_key, msg): + # Compute the binding factors + binding_factor_list = compute_binding_factors(commitment_list, msg) + binding_factor = binding_factor_for_participant(binding_factor_list, identifier) + + # Compute the group commitment + group_commitment = compute_group_commitment(commitment_list, binding_factor_list) + + # Compute the commitment share + (hiding_nonce_commitment, binding_nonce_commitment) = comm_i + comm_share = hiding_nonce_commitment + G.ScalarMult(binding_nonce_commitment, binding_factor) + + # Compute the challenge + challenge = compute_challenge(group_commitment, group_public_key, msg) + + # Compute the interpolating value + participant_list = participants_from_commitment_list(commitment_list) + lambda_i = derive_interpolating_value(identifier, participant_list) + + # Compute relation values + l = G.ScalarBaseMult(sig_share_i) + r = comm_share + G.ScalarMult(PK_i, challenge * lambda_i) + + return l == r + + + + + + + + + + + + + From 34586750629729b4fc501f8f8ef3c74a25f57c28 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Mon, 17 Apr 2023 14:34:46 +0100 Subject: [PATCH 02/22] clean up formatting --- docs/rfc/rfc-10.adoc | 98 ++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index 74d4687fe..eee2aceaa 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -212,76 +212,76 @@ inactive and misbehaving operators are identified and removed from the wallet. ==== Coordinator selection -When a wallet is required to sign message msg in ethereum block B, -one coordinator P_c is selected +When a wallet is required to sign message msg in ethereum block `B`, +one coordinator `P_c` is selected by taking the block number modulo the group size, and choosing the member whose index matches this number; -c = B % n = B % 100. +`c = B % n = B % 100`. -P_c then executes the signing protocol as the coordinator. -If a valid signature is not produced by the time block B+1 is mined, -another coordinator P_c+1 is selected. +`P_c` then executes the signing protocol as the coordinator. +If a valid signature is not produced by the time block `B+1` is mined, +another coordinator `P_c+1` is selected. This continues until a valid signature is produced, or until block B+99 when all members have become coordinators. ==== Execution -1. P_c sends everyone in the group a coordinator(P_c, msg) message. +1. `P_c` sends everyone in the group a `coordinator(P_c, msg)` message. -2. When a member P_i receives a coordinator(P_c, m) message, -they check if they have privately listed P_c as being unreliable/malicious, -whether m is a valid message to sign, -and whether P_c has been selected as a coordinator yet. -If both checks are good, P_i sends P_c a message commit(P_i, cc_i, m) -where cc_i is commit data for the Frost protocol. -P_i does this to all coordinators P_c passing these checks. +2. When a member `P_i` receives a `coordinator(P_c, m)` message, +they check if they have privately listed `P_c` as being unreliable/malicious, +whether `m` is a valid message to sign, +and whether `P_c` has been selected as a coordinator yet. +If both checks are good, `P_i` sends `P_c` a message `commit(P_i, cc_i, m)` +where `cc_i` is commit data for the Frost protocol. +`P_i` does this to all coordinators `P_c` passing these checks. -3. When coordinator P_c receives a commit(P_j, cc_j, m) message, -they check if they consider P_j reliable -and whether m is the correct message to sign m == msg. +3. When coordinator `P_c` receives a `commit(P_j, cc_j, m)` message, +they check if they consider `P_j` reliable +and whether `m` is the correct message to sign `m == msg`. If these checks pass, -the commit message is added to the list of commitments cs. +the commit message is added to the list of commitments `cs`. -4. Once there are at least 51 commitments in cs, -P_c chooses 51 members Pks = [P_k1, ..., P_k51] +4. Once there are at least 51 commitments in `cs`, +`P_c` chooses 51 members `Pks = [P_k1, ..., P_k51]` based on which commitments were received first. -P_c then assembles the list ks consisting of the pairs -[(P_k1, cc_k1), ..., (P_k51, cc_k51)] from those members -and sends them to all in Pks as message signRequest(P_c, ks, msg). -P_c also records the sent request as `requests[hash(ks)] = Pks`. - -5. When a member P_i who has previously sent a commit message to P_c -receives a message signRequest(P_c, ks, m) from P_c -and m matches the message P_i intended to sign, -and cc_i in ks matches the commitment P_i made earlier, -P_i calculates the signature share s_i and new commit data cc'_i, -and sends P_c the messages sign(P_i, s_i, m, hash(ks)) and commit(P_i, cc_i', m). -The member P_i then goes on standby, +`P_c` then assembles the list `ks` consisting of the pairs +`[(P_k1, cc_k1), ..., (P_k51, cc_k51)]` from those members +and sends them to all in Pks as message `signRequest(P_c, ks, msg)`. +`P_c` also records the sent request as `requests[hash(ks)] = Pks`. + +5. When a member `P_i` who has previously sent a commit message to `P_c` +receives a message `signRequest(P_c, ks, m)` from `P_c` +and `m` matches the message `P_i` intended to sign, +and `cc_i` in `ks` matches the commitment `P_i` made earlier, +`P_i` calculates the signature share `s_i` and new commit data `cc'_i`, +and sends `P_c` the messages `sign(P_i, s_i, m, hash(ks))` and `commit(P_i, cc_i', m)`. +The member `P_i` then goes on standby, waiting to execute phase 5 again if another signing request comes from the same coordinator, or phase 2 with a new coordinator. -6. When member P_c receives sign(P_j, s_j, m, h) from P_j, -they check if P_j was in list Pks = requests[h]. -If yes, they validate s_j and add it to the list of signature shares sks = shares[h]. -The coordinator simultaneously executes step 3 again with the commit(P_j, cc_j', m) message, +6. When member `P_c` receives `sign(P_j, s_j, m, h)` from `P_j`, +they check if `P_j` was in list `Pks = requests[h]`. +If yes, they validate `s_j` and add it to the list of signature shares `sks = shares[h]`. +The coordinator simultaneously executes step 3 again with the `commit(P_j, cc_j', m)` message, possibly following up with step 4 as well. -If s_j fails the validation, -P_c adds P_j to its list of bad participants, ignores P_j's commit message, +If `s_j` fails the validation, +`P_c` adds `P_j` to its list of bad participants, ignores `P_j`'s commit message, and returns to step 3. -7. Once some sks has all 51 valid shares in it, P_c tries to assemble a signature s. -If the signature is successful, they send a success(s, m) message to all other members -so they know to abort the signing for m. -P_c can now clear all data used in the execution of the protocol. +7. Once some sks has all 51 valid shares in it, `P_c` tries to assemble a signature `s`. +If the signature is successful, they send a `success(s, m)` message to all other members +so they know to abort the signing for `m`. +`P_c` can now clear all data used in the execution of the protocol. -8. When member P_i receives a message success(s, m) from any other member P_j, -they check if s is a valid signature for m, -and that m is the correct message to sign. -If s and m are valid, they stop executing whatever step they were in +8. When member `P_i` receives a message `success(s, m)` from any other member `P_j`, +they check if `s` is a valid signature for `m`, +and that `m` is the correct message to sign. +If `s` and `m` are valid, they stop executing whatever step they were in and clear all data for the execution. -If s is invalid, P_i adds P_j to the list of bad participants. -If m is invalid but s is valid, -P_i should probably raise an alarm over the fraudulent signature +If `s` is invalid, `P_i` adds `P_j` to the list of bad participants. +If `m` is invalid but `s` is valid, +`P_i` should probably raise an alarm over the fraudulent signature but in practice this shouldn't happen and is out of scope for this RFC. ==== Details From 2f1d4a7e3f01b3efa767a789e630a83130581ef9 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Mon, 17 Apr 2023 14:38:21 +0100 Subject: [PATCH 03/22] further cleanup --- docs/rfc/rfc-10.adoc | 57 +++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index eee2aceaa..270a335ac 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -50,16 +50,16 @@ requiring the use of taproot scripts instead. link:https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki[BIP-340] defines Schnorr signatures for the curve secp256k1, already widely used in Bitcoin. A signature for message m and public key point P -is defined as the (point, scalar) pair (R, s) -where s * G = R + hash(R || P || m) * P. +is defined as the (point, scalar) pair `(R, s)` +where `s * G = R + hash(R || P || m) * P`. The points P and R are encoded as the X-coordinate, and the Y-coordinate is defined to be even. This gives a 32-byte encoding -that corresponds to the standard 33-byte compressed format with the prefix 0x02. +that corresponds to the standard 33-byte compressed format with the prefix `0x02`. -The hash function for BIP340 signatures hash(tag, x) is defined as -SHA256(SHA256(tag) || SHA256(tag) || x) +The hash function for BIP340 signatures `hash(tag, x)` is defined as +`SHA256(SHA256(tag) || SHA256(tag) || x)` where tag is a context-dependent domain separator. For our purposes, we will consider the verification specification: @@ -70,7 +70,7 @@ Input: * The message m: a 32-byte array * A signature sig: a 64-byte array -The algorithm Verify(pk, m, sig) is defined as: +The algorithm `Verify(pk, m, sig)` is defined as: * Let P = lift_x(int(pk)); fail if that fails. * Let r = int(sig[0:32]); fail if r ≥ p. @@ -82,12 +82,12 @@ The algorithm Verify(pk, m, sig) is defined as: * Fail if x(R) ≠ r. * Return success iff no failure occurred before reaching this point. -For every valid secret key sk and message m, Verify(PubKey(sk),m,Sign(sk,m)) will succeed. +For every valid secret key `sk` and message `m`, `Verify(PubKey(sk),m,Sign(sk,m))` will succeed. We see that the signature is a concatenation of a 32-byte x-coordinate or a curve point R, and a 32-byte scalar s. -The hash function uses the tag BIP0340/challenge. +The hash function uses the tag `BIP0340/challenge`. === FROST @@ -100,6 +100,7 @@ which corresponds to a secret key sk shared among n participants using Shamir's These signatures are of the form (R, z) where R is a curve point and z a scalar, and are verified against a public key PK and message msg as follows: +---- prime_order_verify(msg, sig, PK): Inputs: @@ -119,13 +120,14 @@ and are verified against a public key PK and message msg as follows: l = G.ScalarBaseMult(z) r = R + G.ScalarMult(PK, c) return l == r +---- Compared to BIP340, we see that the point R matches in both, and the scalar z of FROST corresponds to the scalar s of BIP340. After accounting for encoding differences, -we see that the hash function H2 producing the challenge c +we see that the hash function `H2` producing the challenge c must match the hash of BIP340 used to compute the scalar e. This is the only required deviation from FROST as specified. Otherwise an examination of the protocols @@ -290,6 +292,7 @@ The below details have been replicated from the FROST paper for convenience: ===== Nonce generation +---- nonce_generate(secret): Inputs: @@ -302,9 +305,11 @@ The below details have been replicated from the FROST paper for convenience: random_bytes = random_bytes(32) secret_enc = G.SerializeScalar(secret) return H3(random_bytes || secret_enc) +---- ===== Polynomial interpolation +---- derive_interpolating_value(x_i, L): Inputs: @@ -334,9 +339,11 @@ The below details have been replicated from the FROST paper for convenience: value = numerator / denominator return value +---- ===== Encode commitments to a byte string +---- Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -355,9 +362,11 @@ The below details have been replicated from the FROST paper for convenience: G.SerializeElement(binding_nonce_commitment) encoded_group_commitment = encoded_group_commitment || encoded_commitment return encoded_group_commitment +---- ===== Extract identifiers from a commitment list +---- Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -373,9 +382,11 @@ The below details have been replicated from the FROST paper for convenience: for (identifier, _, _) in commitment_list: identifiers.append(identifier) return identifiers +---- ===== Extract a blinding factor from a list of blinding factors +---- Inputs: - binding_factor_list = [(i, binding_factor), ...], a list of binding factors for each participant, where each element in the list @@ -393,9 +404,11 @@ The below details have been replicated from the FROST paper for convenience: if identifier == i: return binding_factor raise "invalid participant" +---- ===== Blinding factors computation +---- Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -418,9 +431,11 @@ The below details have been replicated from the FROST paper for convenience: binding_factor = H1(rho_input) binding_factor_list.append((identifier, binding_factor)) return binding_factor_list +---- ===== Group commitment computation +---- Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list @@ -442,9 +457,11 @@ The below details have been replicated from the FROST paper for convenience: group_commitment = group_commitment + hiding_nonce_commitment + G.ScalarMult(binding_nonce_commitment, binding_factor) return group_commitment +---- ===== Signature challenge computation +---- Inputs: - group_commitment, the group commitment, an Element. - group_public_key, the public key corresponding to the group signing key, an @@ -460,9 +477,11 @@ The below details have been replicated from the FROST paper for convenience: challenge_input = group_comm_enc || group_public_key_enc || msg challenge = H2(challenge_input) return challenge +---- ===== Round one: commitment +---- Inputs: - sk_i, the secret key share, a Scalar. @@ -479,9 +498,11 @@ The below details have been replicated from the FROST paper for convenience: nonce = (hiding_nonce, binding_nonce) comm = (hiding_nonce_commitment, binding_nonce_commitment) return (nonce, comm) +---- ===== Round two: signature share generation +---- Inputs: - identifier, identifier i of the participant, a NonZeroScalar. - sk_i, Signer secret key share, a Scalar. @@ -520,9 +541,11 @@ The below details have been replicated from the FROST paper for convenience: sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * sk_i * challenge) return sig_share +---- ===== Signature share aggregation +---- Inputs: - commitment_list = [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a @@ -549,9 +572,11 @@ The below details have been replicated from the FROST paper for convenience: for z_i in sig_shares: z = z + z_i return (group_commitment, z) +---- ===== Signature share verification +---- Inputs: - identifier, identifier i of the participant, a NonZeroScalar. - PK_i, the public key for the i-th participant, where PK_i = G.ScalarBaseMult(sk_i), @@ -598,16 +623,4 @@ The below details have been replicated from the FROST paper for convenience: r = comm_share + G.ScalarMult(PK_i, challenge * lambda_i) return l == r - - - - - - - - - - - - - +---- From 8c5c99c0c83be033ff09f28598d929d21ea8f9b7 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Mon, 17 Apr 2023 14:57:58 +0100 Subject: [PATCH 04/22] Add compatibility considerations --- docs/rfc/rfc-10.adoc | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index 270a335ac..f5da3f25e 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -193,6 +193,48 @@ the signers can choose (n - t + 1) Alices from among themselves, ensuring that if at least t signers are honest at least one Alice must also be honest and the protocol must succeed. +=== Compatibility with tBTC + +==== Script + +BIP-340 Schnorr signatures are not compatible with existing P2(W)SH deposits, +and require the use of Tapscript as specified in +link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki[BIP-341] and +link:https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki[BIP-342]. + +==== Fraud proofs + +A fraudulent BIP-340 transaction would use a Schnorr signature. +An on-chain implementation of BIP-340 signature validation is needed. +Schnorr transactions are not compatible with other signature protocols, +so proving fraud by the existence of a Schnorr signature +for a wallet's public key and an unauthorised message +would be sufficient to detect (attempted) theft of funds by the wallet. + +==== Misbehaviour + +ROAST and FROST are extremely robust against misbehaviour by participants. +Invalid messages can be simply ignored and their sender blocklisted by the recipient. +Validating a received message takes negligible time, +and if misbehaviour during protocol execution could be proved on-chain, +malicious participants could simply stop responding instead and appear inactive. +Thus there is very little motive to put in effort +to develop a way to speficically punish misbehaviour. + +Because ROAST involves multiple concurrent runs of FROST, +participants must track data for individual runs +and match messages accordingly. + +A coordinator may have multiple runs of FROST, +where they communicate with the threshold number of participants. +Thus they must track the state of every run they have started, +and associate incoming messages with the correct run. + +A member has at most one run with each coordinator, +and will only be receiving messages from that run's coordinator. +The member must not reuse nonces between different calls to produce a signature share, +and must erase used nonces immediately. + == tBTC-ROAST The proposed adaptation of ROAST to produce BIP-340 compliant signatures @@ -258,7 +300,7 @@ and `m` matches the message `P_i` intended to sign, and `cc_i` in `ks` matches the commitment `P_i` made earlier, `P_i` calculates the signature share `s_i` and new commit data `cc'_i`, and sends `P_c` the messages `sign(P_i, s_i, m, hash(ks))` and `commit(P_i, cc_i', m)`. -The member `P_i` then goes on standby, +The member `P_i` then erases `cc_i` and goes on standby, waiting to execute phase 5 again if another signing request comes from the same coordinator, or phase 2 with a new coordinator. From d1fcfaa1c3512f3907f28ec82439d41678999e8b Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Fri, 28 Apr 2023 13:28:57 +0100 Subject: [PATCH 05/22] Add full consideration of compatibility and script changes --- docs/rfc/rfc-10.adoc | 594 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 544 insertions(+), 50 deletions(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index f5da3f25e..c5bb4435c 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -30,7 +30,8 @@ which has attributable misbehaviour at the cost of significant implementation co Replace ECDSA with Schnorr signatures, enabling the use of ROAST with FROST threshold signatures instead of CGGMP21. -Use GJKR for distributed key generation of signature shares for FROST. +Use link:https://link.springer.com/article/10.1007/s00145-006-0347-3[GJKR] +for distributed key generation of signature shares for FROST. This reduces the complexity of key generation, and makes signature production significantly simpler and faster @@ -45,6 +46,10 @@ and our implementation must conform to it. Schnorr signatures are incompatible with existing Bitcoin scripts, requiring the use of taproot scripts instead. +This would introduce one user-facing change: +refund addresses would have to be bech32m P2TR addresses +rather than legacy address types. + === BIP-340 link:https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki[BIP-340] defines Schnorr signatures for the curve secp256k1, @@ -72,14 +77,14 @@ Input: The algorithm `Verify(pk, m, sig)` is defined as: -* Let P = lift_x(int(pk)); fail if that fails. -* Let r = int(sig[0:32]); fail if r ≥ p. -* Let s = int(sig[32:64]); fail if s ≥ n. -* Let e = int(hash(BIP0340/challenge, bytes(r) || bytes(P) || m)) mod n. -* Let R = s⋅G - e⋅P. -* Fail if is_infinite(R). -* Fail if not has_even_y(R). -* Fail if x(R) ≠ r. +* Let `P = lift_x(int(pk))`; fail if that fails. +* Let `r = int(sig[0:32])`; fail if `r ≥ p`. +* Let `s = int(sig[32:64])`; fail if `s ≥ n`. +* Let `e = int(hash(BIP0340/challenge, bytes(r) || bytes(P) || m)) mod n`. +* Let `R = s⋅G - e⋅P`. +* Fail if `is_infinite(R)`. +* Fail if not `has_even_y(R)`. +* Fail if `x(R) ≠ r`. * Return success iff no failure occurred before reaching this point. For every valid secret key `sk` and message `m`, `Verify(PubKey(sk),m,Sign(sk,m))` will succeed. @@ -89,18 +94,105 @@ a 32-byte x-coordinate or a curve point R, and a 32-byte scalar s. The hash function uses the tag `BIP0340/challenge`. +=== BIP-341 & BIP-342 + +link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki[BIP-341] +and link:https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki[BIP-342] +define the Taproot outputs and scripting, respectively. + +---- +A Taproot output is a native SegWit output with version number 1, +and a 32-byte witness program. +---- + +The witness program `q` represents a BIP-340 public key, +and the output is constructed like a link:https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh[P2WSH] output +except the version byte is `0x01` instead of `0x00`. + +Taproot outputs can be spent with either a signature for the key `q`, +or a script `s` and an associated control block `c` +matching link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules[the following rules]: + +---- +* Let `q` be the 32-byte array containing the witness program +(the second push in the scriptPubKey) +which represents a public key according to BIP340. + +* Fail if the witness stack has 0 elements. + +* If there are at least two witness elements, +and the first byte of the last element is `0x50`, +this last element is called annex `a` and is removed from the witness stack. +The annex (or the lack of thereof) is always covered by the signature +and contributes to transaction weight, +but is otherwise ignored during taproot validation. + +* If there is exactly one element left in the witness stack, key path spending is used: + +** The single witness stack element is interpreted as the signature +and must be valid (see the next section) for the public key `q` (see the next subsection). + +* If there are at least two witness elements left, script path spending is used: + +** Call the second-to-last stack element `s`, the script. + +** The last stack element is called the control block `c`, and must have length `33 + 32m`, +for a value of `m` that is an integer between 0 and 128, inclusive. +Fail if it does not have such a length. + +** Let `p = c[1:33]` and let `P = lift_x(int(p))` where `lift_x` and `[:]` are defined as in BIP340. +Fail if this point is not on the curve. + +** Let `v = c[0] & 0xfe` and call it the leaf version[7]. + +** Let `k0 = hashTapLeaf(v || compact_size(size of s) || s)`; also call it the tapleaf hash. + +** For `j in [0,1,...,m-1]`: + +*** Let `ej = c[33+32j:65+32j]`. + +*** Let `kj+1` depend on whether `kj < ej` (lexicographically): + +**** If `kj < ej`: `kj+1 = hashTapBranch(kj || ej)`. + +**** If `kj ≥ ej`: `kj+1 = hashTapBranch(ej || kj)`. + +** Let `t = hashTapTweak(p || km)`. + +** If `t ≥ 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141` (order of secp256k1), fail. + +** Let `Q = P + int(t)G`. + +** If `q ≠ x(Q)` or `c[0] & 1 ≠ y(Q) mod 2`, fail. + +** Execute the script, according to the applicable script rules, +using the witness stack elements excluding the script s, the control block c, +and the annex a if present, as initial stack. +This implies that for the future leaf versions (non-0xC0) the execution must succeed. + +q is referred to as taproot output key and p as taproot internal key. +---- + +The way key `q` is constructed +means that every Taproot spend has essentially a "backdoor" +for the holder of the secret key corresponding to `p`. +This allows the simplification of scripts +where one of the spending conditions is a simple signature. + === FROST link:https://www.ietf.org/id/draft-irtf-cfrg-frost-12.html[FROST (Flexible Round-Optimized Schnorr Threshold)] signing protocol is a protocol for generating valid Schnorr signatures for a public key PK which corresponds to a secret key sk shared among n participants using Shamir's method. -==== Compatibility with BIP-340 +==== Compatibility with BIP-340, 341 & 342 + +===== BIP-340 These signatures are of the form (R, z) where R is a curve point and z a scalar, and are verified against a public key PK and message msg as follows: ----- +.... prime_order_verify(msg, sig, PK): Inputs: @@ -120,7 +212,7 @@ and are verified against a public key PK and message msg as follows: l = G.ScalarBaseMult(z) r = R + G.ScalarMult(PK, c) return l == r ----- +.... Compared to BIP340, we see that the point R matches in both, @@ -133,6 +225,38 @@ This is the only required deviation from FROST as specified. Otherwise an examination of the protocols will show that a FROST-generated signature would pass BIP340 verification. +===== BIP-341 + +The Taproot output key `q` is produced by `q = Q.x; Q = P + tG` +for a constant `t` determined by `P` and the script used. + +For a public key `P = skG`, the secret key `sk'` matching `Q = sk'G` +is calculated by `sk' = sk + t`. + +Because FROST uses Shamir's polynomial secret sharing, +individual participants' secret key shares are points on a polynomial +whose constant term equals `sk`. +Thus adding `t` to each secret key share yields a new polynomial +whose constant term equals `sk'`. +Similarly, each secret key share's corresponding public key share +can simply have `tG` added to it for the purpose of verification. + +This makes adaptation of FROST to produce signatures for Taproot output keys simple. + +BIP-341 link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message[also defines] +how the message to be signed is computed. +This determines the `msg` but has no direct bearing on FROST. + +===== BIP-342 + +link:https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#signature-validation[BIP-342] +defines signature verification rules +that only apply when using a script path spend. + +These rules affect the refund path of tBTC deposits +but aren't directly relevant for FROST +which simply produces signatures for the BIP-341 Taproot output key. + ==== Protocol FROST is a two-round protocol for generating t-of-n threshold Schnorr signatures @@ -195,14 +319,369 @@ at least one Alice must also be honest and the protocol must succeed. === Compatibility with tBTC -==== Script - -BIP-340 Schnorr signatures are not compatible with existing P2(W)SH deposits, -and require the use of Tapscript as specified in -link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki[BIP-341] and -link:https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki[BIP-342]. - -==== Fraud proofs +The only way to use BIP-340 Schnorr signatures is with Taproot, +which breaks compatibility with existing addresses and scripts. +Only bech32m P2TR addresses using Schnorr signatures are accepted in Taproot outputs, +but Taproot outputs can be spent into legacy addresses. + +Most importantly, this means that the refund address provided when depositing BTC +must be a bech32m P2TR address, as it would be used from a Taproot script. + +Redemptions can still be paid to legacy addresses; +only the change output remaining in the wallet has to be Taproot. + +==== Script changes + +===== Deposit + +Tapscript allows the definition of separate script paths, +but most importantly we can use the wallet's public key +as the script's internal public key, +simplifying deposit scripts to only cover the refund path. + +Current deposit script: +.... + DROP + DROP +DUP HASH160 EQUAL +IF + CHECKSIG +ELSE + DUP HASH160 EQUALVERIFY + CHECKLOCKTIMEVERIFY DROP + CHECKSIG +ENDIF +.... + +Taproot refund path: +.... + DROP + DROP + CHECKLOCKTIMEVERIFY DROP + CHECKSIG +.... + +Corresponding opcodes (73 bytes total): +.... +- 0x14: Byte length of depositor ethereum address +- +- 0x75: OP_DROP +- 0x08: Byte length of blinding factor +- +- 0x75: OP_DROP +- 0x04: Byte length of refund locktime value +- +- 0xb1: OP_CHECKLOCKTIMEVERIFY +- 0x75: OP_DROP +- 0x20: Byte length of refund public key +- +- 0xac: OP_CHECKSIG +.... + +link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs[When spending a Taproot output with the key path], +wallet's public key is tweaked with a hash of the script. +This creates an effective commitment to the matching script +while saving on transaction fees due to the smaller witness of a key path spend. + +===== Otherwise + +Taproot P2TR scripts always consist of a push of the version byte `0x01`, +and the 32-byte plaintext public key `q`. + +These can be constructed as follows: +.... +bytes4 constant P2TR_PREFIX = bytes4(0x23010120) // script length 35, push 1 byte (version), 0x01, push 32 bytes (public key) + +function makeP2TRScript(bytes32 q) + internal + pure + returns (bytes memory) +{ + return (abi.encodePacked(P2TR_PREFIX, q); // prefix followed by public key q +} +.... + +==== Solidity changes + +===== Wallet pubkeys + +Wallets need to be identified by their 32-byte plaintext BIP-340 pubkeys, +rather than 20-byte pubkey hashes as they currently are. + +===== BTCUtils + +`BTCUtils.extractHash()` is incompatible with P2TR. +Due to the simplification of only needing to consider the P2TR case, +we can implement a unified `extractP2TRKey()` function instead: + +.... +function extractP2TRKey(bytes memory output) internal pure returns (bytes32 q) { + uint256 scriptLen = output.length - 8; + require(scriptLen == 36, "Invalid length for P2TR output"); + bytes4 prefix = output.slice4(8); + require(prefix == P2TR_PREFIX, "Invalid prefix for P2TR output"); // "0x23010120" + q = output.slice32(12); + return q; +} +.... + +This function would take the role of `BitcoinTx.extractPubKeyHash()` +in P2TR contexts. + +===== Deposit + +When a user reveals a deposit, +the expected script needs to conform to the Taproot script, +and the refund pubkey must be an unhashed Schnorr pubkey. + +The funding output's script is produced by taking the wallet's public key `p` +and applying a tweak corresponding to the correct refund script `s`. +The tweak is defined in link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules[BIP-341], +and the presence of only one script path allows it to be slightly simplified: + +.... +P = lift_x(p) +k0 = hash("TapLeaf", v || compact_size(size of s) || s) +t = hash("TapTweak", p || k0) +(require t < order of secp256k1) +Q = P + int(t)G +q = Q.x +.... + +The size of `s` is always `73 = 0x49`, and the leaf version `v` must be `0xc0`. + +To verify that the `q` of the output is correct for the claimed deposit, +we need to check that `P + tG == Q` which would be prohibitively expensive +in a naive implementation using on-chain scalar multiplication. + +However, there exists a method to +link:https://ethresear.ch/t/you-can-kinda-abuse-ecrecover-to-do-ecmul-in-secp256k1-today/2384[use ECRECOVER to verify the result of a scalar multiplication]. +Thus we can require that the depositor also provides the values +`P` and `Q` in the reveal transaction. +By calculating `t = hash("TapTweak", p || hash("TapLeaf", 0xc049 || s))` +we can simply verify the correctness of the scalar multiplication +and of the provided curve points, +at a dramatically lower gas cost: + +.... +require(Q.x == q && P.x == p) // check provided points match x-coordinates +require(P.y % 2 == 0) // check P evenness +require(isOnCurve(P) && isOnCurve(Q)) +require(verifyEcBaseMul(ecSub(Q, P), t)) // verify that tG == Q - P +.... + +All in all, we get this: +.... +struct DepositRevealInfo { + // Index of the funding output belonging to the funding transaction. + uint32 fundingOutputIndex; + // The blinding factor as 8 bytes. Byte endianness doesn't matter + // as this factor is not interpreted as uint. The blinding factor allows + // to distinguish deposits from the same depositor. + bytes8 blindingFactor; + // The BIP-340 public key p of the deposit's wallet. + bytes32 walletPubKey; + // The wallet's public key in point form. + Point P; + // The deposit output key q in point form. + Point Q; + // The BIP-340 public key that can be used to make the deposit refund + // after the refund locktime passes. + bytes20 refundPubKey; + // The refund locktime (4-byte LE). Interpreted according to locktime + // parsing rules described in: + // https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number + // and used with OP_CHECKLOCKTIMEVERIFY opcode as described in: + // https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki + bytes4 refundLocktime; + // Address of the Bank vault to which the deposit is routed to. + // Optional, can be 0x0. The vault must be trusted by the Bridge. + address vault; + // This struct doesn't contain `__gap` property as the structure is not + // stored, it is used as a function's calldata argument. +} + +(...) + +function revealDeposit( + BridgeState.Storage storage self, + BitcoinTx.Info calldata fundingTx, + DepositRevealInfo calldata reveal +) external { + bytes32 p = reveal.walletPubKey; + + require( + self.registeredWallets[p].state == + Wallets.WalletState.Live, + "Wallet must be in Live state" + ); + + require( + reveal.vault == address(0) || self.isVaultTrusted[reveal.vault], + "Vault is not trusted" + ); + + if (self.depositRevealAheadPeriod > 0) { + validateDepositRefundLocktime(self, reveal.refundLocktime); + } + + bytes memory expectedLeaf = abi.encodePacked( + hex"c0", // Leaf version c0 + hex"49", // Byte length of the script + hex"14", // Byte length of depositor Ethereum address. + msg.sender, + hex"75", // OP_DROP + hex"08", // Byte length of blinding factor value. + reveal.blindingFactor, + hex"75", // OP_DROP + hex"04", // Byte length of refund locktime value. + reveal.refundLocktime, + hex"b1", // OP_CHECKLOCKTIMEVERIFY + hex"75", // OP_DROP + hex"20", // Byte length of BIP-340 public key + reveal.refundPubKey, + hex"ac", // OP_CHECKSIG + ); + + bytes memory fundingOutput = fundingTx + .outputVector + .extractOutputAtIndex(reveal.fundingOutputIndex); + bytes32 q = fundingOutput.extractP2TRKey(); + + require( + reveal.Q.x == uint256(q) && reveal.P.x == uint256(p), + "Mismatched reveal points" + ); + require( + isOnCurve(reveal.P) && isOnCurve(reveal.Q), + "Reveal points not on curve" + ); + require(reveal.P.y % 2 == 0, "Revealed P has odd y"); + + bytes32 t = tagHash( + "TapTweak", + abi.encodePacked(p, tagHash("TapLeaf", expectedLeaf)) + ); + + require( + verifyEcBaseMul(ecSub(Q, P), t), + "Output does not match expected script" + ); + + // Resulting TX hash is in native Bitcoin little-endian format. + bytes32 fundingTxHash = abi + .encodePacked( + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime + ) + .hash256View(); + + DepositRequest storage deposit = self.deposits[ + uint256( + keccak256( + abi.encodePacked(fundingTxHash, reveal.fundingOutputIndex) + ) + ) + ]; + require(deposit.revealedAt == 0, "Deposit already revealed"); + + uint64 fundingOutputAmount = fundingOutput.extractValue(); + + require( + fundingOutputAmount >= self.depositDustThreshold, + "Deposit amount too small" + ); + + deposit.amount = fundingOutputAmount; + deposit.depositor = msg.sender; + /* solhint-disable-next-line not-rely-on-time */ + deposit.revealedAt = uint32(block.timestamp); + deposit.vault = reveal.vault; + deposit.treasuryFee = self.depositTreasuryFeeDivisor > 0 + ? fundingOutputAmount / self.depositTreasuryFeeDivisor + : 0; + // slither-disable-next-line reentrancy-events + emit DepositRevealed( + fundingTxHash, + reveal.fundingOutputIndex, + msg.sender, + fundingOutputAmount, + reveal.blindingFactor, + reveal.walletPubKey, + reveal.refundPubKeyHash, + reveal.refundLocktime, + reveal.vault + ); +} +.... + +===== Sweeping + +Taproot uses plaintext Schnorr pubkeys, +not pubkey hashes. + +In link:https://github.com/keep-network/tbtc-v2/blob/main/solidity/contracts/bridge/DepositSweep.sol#L349[DepositSweep.processDepositSweepTxOutput()] +tweak as follows: + +.... +function processDepositSweepTxOutput( + BridgeState.Storage storage self, + bytes memory sweepTxOutputVector +) internal view returns (bytes32 walletPubKey, uint64 value) { + // ... + // ... + (, uint256 outputsCount) = sweepTxOutputVector.parseVarInt(); + require( + outputsCount == 1, + "Sweep transaction must have a single output" + ); + + bytes memory output = sweepTxOutputVector.extractOutputAtIndex(0); + walletPubKey = output.extractP2TRKey(); + value = output.extractValue(); + + return (walletPubKey, value); +} +.... + +Propagate changes as required. + +===== Redemption + +We can use the output of `makeP2TRScript()` directly on the wallet's public key. +link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23[BIP-341 recommends a taproot commitment] +even if there is no script path, +but the public key being used here is not an aggregate of keys, +but rather a threshold protocol public key, +so the consideration of attacks against key aggregation don't apply. + +Tweak link:https://github.com/keep-network/tbtc-v2/blob/main/solidity/contracts/bridge/Redemption.sol#L824[Redemption.processRedemptionTxOutputs()]: + +.... +if ( + resultInfo.changeValue == 0 && + (outputScriptHash == processInfo.walletP2TRScriptKeccak) && + outputValue > 0 +) { + // If we entered here, that means the change output with a + // proper non-zero value was found. + resultInfo.changeIndex = uint32(i); + resultInfo.changeValue = outputValue; +} else { +.... + +The wallet can mix Taproot and legacy outputs, +so we shouldn't have issues with compatibility. + +===== Moving funds + +All outputs of moving funds need to use Taproot scripts +matching the destination wallets' 32-byte public keys, +rather than legacy scripts referring to 20-byte hashes. + +===== Fraud proofs A fraudulent BIP-340 transaction would use a Schnorr signature. An on-chain implementation of BIP-340 signature validation is needed. @@ -211,7 +690,16 @@ so proving fraud by the existence of a Schnorr signature for a wallet's public key and an unauthorised message would be sufficient to detect (attempted) theft of funds by the wallet. -==== Misbehaviour +==== DApp changes + +The DApp needs to produce P2TR deposits, +and require a bech32m refund address. +This is also a user-facing change. +The `q` of the output is calculated the same way as in Solidity. + +For redemptions, legacy outputs can be specified. + +==== Misbehaviour handling ROAST and FROST are extremely robust against misbehaviour by participants. Invalid messages can be simply ignored and their sender blocklisted by the recipient. @@ -270,11 +758,12 @@ or until block B+99 when all members have become coordinators. ==== Execution -1. `P_c` sends everyone in the group a `coordinator(P_c, msg)` message. +1. `P_c` sends everyone in the group a `coordinator(P_c, msg, t)` message. -2. When a member `P_i` receives a `coordinator(P_c, m)` message, +2. When a member `P_i` receives a `coordinator(P_c, m, t)` message, they check if they have privately listed `P_c` as being unreliable/malicious, whether `m` is a valid message to sign, +whether `t` is the correct TapTweak hash for it, and whether `P_c` has been selected as a coordinator yet. If both checks are good, `P_i` sends `P_c` a message `commit(P_i, cc_i, m)` where `cc_i` is commit data for the Frost protocol. @@ -291,15 +780,19 @@ the commit message is added to the list of commitments `cs`. based on which commitments were received first. `P_c` then assembles the list `ks` consisting of the pairs `[(P_k1, cc_k1), ..., (P_k51, cc_k51)]` from those members -and sends them to all in Pks as message `signRequest(P_c, ks, msg)`. +and sends them to all in Pks as message `signRequest(P_c, ks, msg, t)`. `P_c` also records the sent request as `requests[hash(ks)] = Pks`. 5. When a member `P_i` who has previously sent a commit message to `P_c` -receives a message `signRequest(P_c, ks, m)` from `P_c` -and `m` matches the message `P_i` intended to sign, +receives a message `signRequest(P_c, ks, m, t)` from `P_c` +and `m` and `t` match the message `P_i` intended to sign and its TapTweak, and `cc_i` in `ks` matches the commitment `P_i` made earlier, `P_i` calculates the signature share `s_i` and new commit data `cc'_i`, and sends `P_c` the messages `sign(P_i, s_i, m, hash(ks))` and `commit(P_i, cc_i', m)`. +For this purpose, `P_i` adds `t` to their secret key share `sk_i' == sk_i + t`. +If every member performs this correctly, +the effect is the same as if `t` was added to the wallet's secret key +as adding a constant to a polynomial adds that same constant to all of its points. The member `P_i` then erases `cc_i` and goes on standby, waiting to execute phase 5 again if another signing request comes from the same coordinator, or phase 2 with a new coordinator. @@ -307,6 +800,7 @@ or phase 2 with a new coordinator. 6. When member `P_c` receives `sign(P_j, s_j, m, h)` from `P_j`, they check if `P_j` was in list `Pks = requests[h]`. If yes, they validate `s_j` and add it to the list of signature shares `sks = shares[h]`. +The validation is performed with a tweaked public key share `pk_j' == pk_j + tG`. The coordinator simultaneously executes step 3 again with the `commit(P_j, cc_j', m)` message, possibly following up with step 4 as well. If `s_j` fails the validation, @@ -334,7 +828,7 @@ The below details have been replicated from the FROST paper for convenience: ===== Nonce generation ----- +.... nonce_generate(secret): Inputs: @@ -347,11 +841,11 @@ The below details have been replicated from the FROST paper for convenience: random_bytes = random_bytes(32) secret_enc = G.SerializeScalar(secret) return H3(random_bytes || secret_enc) ----- +.... ===== Polynomial interpolation ----- +.... derive_interpolating_value(x_i, L): Inputs: @@ -381,11 +875,11 @@ The below details have been replicated from the FROST paper for convenience: value = numerator / denominator return value ----- +.... ===== Encode commitments to a byte string ----- +.... Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -404,11 +898,11 @@ The below details have been replicated from the FROST paper for convenience: G.SerializeElement(binding_nonce_commitment) encoded_group_commitment = encoded_group_commitment || encoded_commitment return encoded_group_commitment ----- +.... ===== Extract identifiers from a commitment list ----- +.... Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -424,11 +918,11 @@ The below details have been replicated from the FROST paper for convenience: for (identifier, _, _) in commitment_list: identifiers.append(identifier) return identifiers ----- +.... ===== Extract a blinding factor from a list of blinding factors ----- +.... Inputs: - binding_factor_list = [(i, binding_factor), ...], a list of binding factors for each participant, where each element in the list @@ -446,11 +940,11 @@ The below details have been replicated from the FROST paper for convenience: if identifier == i: return binding_factor raise "invalid participant" ----- +.... ===== Blinding factors computation ----- +.... Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list of commitments issued by each participant, where each element in the list @@ -473,11 +967,11 @@ The below details have been replicated from the FROST paper for convenience: binding_factor = H1(rho_input) binding_factor_list.append((identifier, binding_factor)) return binding_factor_list ----- +.... ===== Group commitment computation ----- +.... Inputs: - commitment_list = [(i, hiding_nonce_commitment_i, binding_nonce_commitment_i), ...], a list @@ -499,11 +993,11 @@ The below details have been replicated from the FROST paper for convenience: group_commitment = group_commitment + hiding_nonce_commitment + G.ScalarMult(binding_nonce_commitment, binding_factor) return group_commitment ----- +.... ===== Signature challenge computation ----- +.... Inputs: - group_commitment, the group commitment, an Element. - group_public_key, the public key corresponding to the group signing key, an @@ -519,11 +1013,11 @@ The below details have been replicated from the FROST paper for convenience: challenge_input = group_comm_enc || group_public_key_enc || msg challenge = H2(challenge_input) return challenge ----- +.... ===== Round one: commitment ----- +.... Inputs: - sk_i, the secret key share, a Scalar. @@ -540,11 +1034,11 @@ The below details have been replicated from the FROST paper for convenience: nonce = (hiding_nonce, binding_nonce) comm = (hiding_nonce_commitment, binding_nonce_commitment) return (nonce, comm) ----- +.... ===== Round two: signature share generation ----- +.... Inputs: - identifier, identifier i of the participant, a NonZeroScalar. - sk_i, Signer secret key share, a Scalar. @@ -583,11 +1077,11 @@ The below details have been replicated from the FROST paper for convenience: sig_share = hiding_nonce + (binding_nonce * binding_factor) + (lambda_i * sk_i * challenge) return sig_share ----- +.... ===== Signature share aggregation ----- +.... Inputs: - commitment_list = [(j, hiding_nonce_commitment_j, binding_nonce_commitment_j), ...], a @@ -614,11 +1108,11 @@ The below details have been replicated from the FROST paper for convenience: for z_i in sig_shares: z = z + z_i return (group_commitment, z) ----- +.... ===== Signature share verification ----- +.... Inputs: - identifier, identifier i of the participant, a NonZeroScalar. - PK_i, the public key for the i-th participant, where PK_i = G.ScalarBaseMult(sk_i), @@ -665,4 +1159,4 @@ The below details have been replicated from the FROST paper for convenience: r = comm_share + G.ScalarMult(PK_i, challenge * lambda_i) return l == r ----- +.... From f713f568b3bc46be563ed921186164bb3b9c52c5 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Tue, 2 May 2023 16:49:09 +0100 Subject: [PATCH 06/22] Add resources and clarify execution --- docs/rfc/rfc-10.adoc | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index c5bb4435c..be36bf0fe 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -767,6 +767,8 @@ whether `t` is the correct TapTweak hash for it, and whether `P_c` has been selected as a coordinator yet. If both checks are good, `P_i` sends `P_c` a message `commit(P_i, cc_i, m)` where `cc_i` is commit data for the Frost protocol. +The commitments, alongside the matching private nonces `nc_i` are stored in +`commitments[hash(P_c || m)] = (nc_i, cc_i)`. `P_i` does this to all coordinators `P_c` passing these checks. 3. When coordinator `P_c` receives a `commit(P_j, cc_j, m)` message, @@ -786,14 +788,15 @@ and sends them to all in Pks as message `signRequest(P_c, ks, msg, t)`. 5. When a member `P_i` who has previously sent a commit message to `P_c` receives a message `signRequest(P_c, ks, m, t)` from `P_c` and `m` and `t` match the message `P_i` intended to sign and its TapTweak, -and `cc_i` in `ks` matches the commitment `P_i` made earlier, +and `cc_i` in `ks` matches the commitment `P_i` made earlier +(check that `commitments[hash(P_c || m) == (_, cc_i)`), `P_i` calculates the signature share `s_i` and new commit data `cc'_i`, and sends `P_c` the messages `sign(P_i, s_i, m, hash(ks))` and `commit(P_i, cc_i', m)`. For this purpose, `P_i` adds `t` to their secret key share `sk_i' == sk_i + t`. If every member performs this correctly, the effect is the same as if `t` was added to the wallet's secret key as adding a constant to a polynomial adds that same constant to all of its points. -The member `P_i` then erases `cc_i` and goes on standby, +The member `P_i` then erases `(nc_i, cc_i)` from `commitments` and goes on standby, waiting to execute phase 5 again if another signing request comes from the same coordinator, or phase 2 with a new coordinator. @@ -1160,3 +1163,30 @@ The below details have been replicated from the FROST paper for convenience: return l == r .... + +== Resources + +link:https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki[BIP-141: Segregated Witness (Consensus layer)] + +link:https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki[BIP-143: Transaction Signature Verification for Version 0 Witness Program] + +link:https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki[BIP-340: Schnorr Signatures for secp256k1] + +link:https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki[BIP-341: Taproot: SegWit version 1 spending rules] + +link:https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki[BIP-342: Validation of Taproot Scripts] + +=== Taproot + +link:https://bitcoinops.org/en/schorr-taproot-workshop/[Bitcoin Optech Schnorr Taproot workshop] + +=== FROST + +link:https://www.youtube.com/watch?v=ReN0kMzDFro[Seminar presentation on FROST] + +=== ROAST + +link:https://github.com/robot-dreams/roast[robot-dreams/roast]; +an example implementation of non-TapTweaked ROAST in Python. + +link:https://www.youtube.com/watch?v=FVW6Hgt_meg[Seminar presentation on ROAST] From 74b4f2ff78a002b43f83a9a6bcac3f7b2d8fd44f Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Tue, 2 May 2023 16:50:22 +0100 Subject: [PATCH 07/22] Escape occurrences of (R) --- docs/rfc/rfc-10.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rfc/rfc-10.adoc b/docs/rfc/rfc-10.adoc index be36bf0fe..0bc0bbe27 100644 --- a/docs/rfc/rfc-10.adoc +++ b/docs/rfc/rfc-10.adoc @@ -82,9 +82,9 @@ The algorithm `Verify(pk, m, sig)` is defined as: * Let `s = int(sig[32:64])`; fail if `s ≥ n`. * Let `e = int(hash(BIP0340/challenge, bytes(r) || bytes(P) || m)) mod n`. * Let `R = s⋅G - e⋅P`. -* Fail if `is_infinite(R)`. -* Fail if not `has_even_y(R)`. -* Fail if `x(R) ≠ r`. +* Fail if `is_infinite\(R)`. +* Fail if not `has_even_y\(R)`. +* Fail if `x\(R) ≠ r`. * Return success iff no failure occurred before reaching this point. For every valid secret key `sk` and message `m`, `Verify(PubKey(sk),m,Sign(sk,m))` will succeed. From ea33c654385722a9198dd11053e30dea080a82f6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jul 2023 12:10:28 +0200 Subject: [PATCH 08/22] Add `getAddressFromScriptPubKey` fn Add the new function that returns the Bitcoin address based on the script pub key placed on the output of a Bitcoin transaction. --- typescript/src/bitcoin.ts | 19 ++++- typescript/test/bitcoin.test.ts | 145 ++++++++++---------------------- typescript/test/data/bitcoin.ts | 65 ++++++++++++++ 3 files changed, 128 insertions(+), 101 deletions(-) create mode 100644 typescript/test/data/bitcoin.ts diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index bb330be3f..7941b802b 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -1,4 +1,4 @@ -import bcoin, { TX, Script } from "bcoin" +import bcoin, { TX, Script, Address } from "bcoin" import wif from "wif" import bufio from "bufio" import hash160 from "bcrypto/lib/hash160" @@ -612,3 +612,20 @@ export function locktimeToNumber(locktimeLE: Buffer | string): number { export function createOutputScriptFromAddress(address: string): Hex { return Hex.from(Script.fromAddress(address).toRaw().toString("hex")) } + +/** + * Returns the Bitcoin address based on the script pub key placed on the output + * of a Bitcoin transaction. + * @param scriptPubKey Scirpt pub key placed on the output of a Bitcoin + * transaction. + * @param network Bitcoin network. + * @returns The Bitcoin address. + */ +export function getAddressFromScriptPubKey( + scriptPubKey: string, + network: BitcoinNetwork = BitcoinNetwork.Mainnet +): string { + return Script.fromRaw(scriptPubKey.toString(), "hex") + .getAddress() + ?.toString(toBcoinNetwork(network)) +} diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index a1bb5da82..7c9795e40 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -12,11 +12,13 @@ import { bitsToTarget, targetToDifficulty, createOutputScriptFromAddress, + getAddressFromScriptPubKey, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" import { Hex } from "../src/hex" import { BigNumber } from "ethers" +import { btcAddresses } from "./data/bitcoin" describe("Bitcoin", () => { describe("compressPublicKey", () => { @@ -467,110 +469,53 @@ describe("Bitcoin", () => { }) describe("createOutputScriptFromAddress", () => { - context("with testnet addresses", () => { - const btcAddresses = { - P2PKH: { - address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", - redeemerOutputScript: - "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - outputScript: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - }, - P2WPKH: { - address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", - redeemerOutputScript: - "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - outputScript: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - }, - P2SH: { - address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", - redeemerOutputScript: - "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - outputScript: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - }, - P2WSH: { - address: - "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", - redeemerOutputScript: - "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - outputScript: - "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - }, - } - - Object.entries(btcAddresses).forEach( - ([ - addressType, - { - address, - redeemerOutputScript: expectedRedeemerOutputScript, - outputScript: expectedOutputScript, - }, - ]) => { - it(`should create correct output script for ${addressType} address type`, () => { - const result = createOutputScriptFromAddress(address) - - expect(result.toString()).to.eq(expectedOutputScript) - // Check if we can build the prefixed raw redeemer output script based - // on the result. - expect(buildRawPrefixedOutputScript(result.toString())).to.eq( - expectedRedeemerOutputScript - ) - }) - } - ) + Object.keys(btcAddresses).forEach((bitcoinNetwork) => { + context(`with ${bitcoinNetwork} addresses`, () => { + Object.entries( + btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] + ).forEach( + ([ + addressType, + { + address, + redeemerOutputScript: expectedRedeemerOutputScript, + scriptPubKey: expectedOutputScript, + }, + ]) => { + it(`should create correct output script for ${addressType} address type`, () => { + const result = createOutputScriptFromAddress(address) + + expect(result.toString()).to.eq(expectedOutputScript) + // Check if we can build the prefixed raw redeemer output script based + // on the result. + expect(buildRawPrefixedOutputScript(result.toString())).to.eq( + expectedRedeemerOutputScript + ) + }) + } + ) + }) }) + }) - context("with mainnet addresses", () => { - const btcAddresses = { - P2PKH: { - address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", - redeemerOutputScript: - "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - outputScript: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - }, - P2WPKH: { - address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", - redeemerOutputScript: - "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", - outputScript: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", - }, - P2SH: { - address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", - redeemerOutputScript: - "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - outputScript: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - }, - P2WSH: { - address: - "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", - redeemerOutputScript: - "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - outputScript: - "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - }, - } - - Object.entries(btcAddresses).forEach( - ([ - addressType, - { - address, - redeemerOutputScript: expectedRedeemerOutputScript, - outputScript: expectedOutputScript, - }, - ]) => { - it(`should create correct output script for ${addressType} address type`, () => { - const result = createOutputScriptFromAddress(address) - - expect(result.toString()).to.eq(expectedOutputScript) - // Check if we can build the prefixed raw redeemer output script based - // on the result. - expect(buildRawPrefixedOutputScript(result.toString())).to.eq( - expectedRedeemerOutputScript + describe("getAddressFromScriptPubKey", () => { + Object.keys(btcAddresses).forEach((bitcoinNetwork) => { + context(`with ${bitcoinNetwork} addresses`, () => { + Object.entries( + btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] + ).forEach(([addressType, { address, scriptPubKey }]) => { + it(`should return correct ${addressType} address`, () => { + const result = getAddressFromScriptPubKey( + scriptPubKey, + bitcoinNetwork === "mainnet" + ? BitcoinNetwork.Mainnet + : BitcoinNetwork.Testnet ) + + expect(result.toString()).to.eq(address) }) - } - ) + }) + }) }) }) }) diff --git a/typescript/test/data/bitcoin.ts b/typescript/test/data/bitcoin.ts new file mode 100644 index 000000000..bc266e1a0 --- /dev/null +++ b/typescript/test/data/bitcoin.ts @@ -0,0 +1,65 @@ +import { BitcoinNetwork } from "../../src/bitcoin-network" + +export const btcAddresses: Record< + Exclude, + { + [addressType: string]: { + address: string + redeemerOutputScript: string + scriptPubKey: string + } + } +> = { + testnet: { + P2PKH: { + address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", + redeemerOutputScript: + "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + scriptPubKey: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + }, + P2WPKH: { + address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + redeemerOutputScript: "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + scriptPubKey: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + }, + P2SH: { + address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", + redeemerOutputScript: + "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + scriptPubKey: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + }, + P2WSH: { + address: "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", + redeemerOutputScript: + "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + scriptPubKey: + "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + }, + }, + mainnet: { + P2PKH: { + address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", + redeemerOutputScript: + "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + scriptPubKey: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + }, + P2WPKH: { + address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", + redeemerOutputScript: "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", + scriptPubKey: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", + }, + P2SH: { + address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", + redeemerOutputScript: + "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + scriptPubKey: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + }, + P2WSH: { + address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + redeemerOutputScript: + "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + scriptPubKey: + "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + }, + }, +} From 5ceed1e5e93c02eca775d559ff94935d066a2a19 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 7 Jul 2023 10:35:49 +0200 Subject: [PATCH 09/22] Remove unnecessary import --- typescript/src/bitcoin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 7941b802b..c3235cdb4 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -1,4 +1,4 @@ -import bcoin, { TX, Script, Address } from "bcoin" +import bcoin, { TX, Script } from "bcoin" import wif from "wif" import bufio from "bufio" import hash160 from "bcrypto/lib/hash160" From 97d56b3fdc0fefb294d141d674a0a13aadce247c Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 13:43:20 +0200 Subject: [PATCH 10/22] Expose `getTransactionHistory` function Here we expose a function allowing to get the transaction history for given Bitcoin address. --- typescript/src/bitcoin.ts | 12 +++++ typescript/src/electrum.ts | 52 ++++++++++++++++++++ typescript/test/electrum.test.ts | 22 +++++++++ typescript/test/utils/mock-bitcoin-client.ts | 24 +++++++++ 4 files changed, 110 insertions(+) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index bb330be3f..735c3dc94 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -333,6 +333,18 @@ export interface Client { address: string ): Promise + /** + * Gets the history of confirmed transactions for given Bitcoin address. + * Returned transactions are sorted from oldest to newest. The returned + * result does not contain unconfirmed transactions living in the mempool + * at the moment of request. + * @param address - Bitcoin address transaction history should be determined for. + * @param limit - Optional parameter that can limit the resulting list to + * a specific number of last transaction. For example, limit = 5 will + * return only the last 5 transactions for the given address. + */ + getTransactionHistory(address: string, limit?: number): Promise + /** * Gets the full transaction object for given transaction hash. * @param transactionHash - Hash of the transaction. diff --git a/typescript/src/electrum.ts b/typescript/src/electrum.ts index a43993209..d467f42f7 100644 --- a/typescript/src/electrum.ts +++ b/typescript/src/electrum.ts @@ -253,6 +253,58 @@ export class Client implements BitcoinClient { ) } + // eslint-disable-next-line valid-jsdoc + /** + * @see {BitcoinClient#getTransactionHistory} + */ + getTransactionHistory( + address: string, + limit?: number + ): Promise { + return this.withElectrum(async (electrum: Electrum) => { + const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + + // eslint-disable-next-line camelcase + type HistoryItem = { height: number; tx_hash: string } + + let historyItems: HistoryItem[] = await this.withBackoffRetrier< + HistoryItem[] + >()(async () => { + return await electrum.blockchain_scripthash_getHistory( + computeScriptHash(script) + ) + }) + + // According to https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history + // unconfirmed items living in the mempool are appended at the end of the + // returned list and their height value is either -1 or 0. That means + // we need to take all items with height >0 to obtain a confirmed txs + // history. + historyItems = historyItems.filter((item) => item.height > 0) + + // The list returned from blockchain.scripthash.get_history is sorted by + // the block height in the ascending order though we are sorting it + // again just in case (e.g. API contract changes). + historyItems = historyItems.sort( + (item1, item2) => item1.height - item2.height + ) + + if ( + typeof limit !== "undefined" && + limit > 0 && + historyItems.length > limit + ) { + historyItems = historyItems.slice(-limit) + } + + const transactions = historyItems.map((item) => + this.getTransaction(TransactionHash.from(item.tx_hash)) + ) + + return Promise.all(transactions) + }) + } + // eslint-disable-next-line valid-jsdoc /** * @see {BitcoinClient#getTransaction} diff --git a/typescript/test/electrum.test.ts b/typescript/test/electrum.test.ts index c500a7efa..09fca2c2d 100644 --- a/typescript/test/electrum.test.ts +++ b/typescript/test/electrum.test.ts @@ -108,6 +108,28 @@ describe("Electrum", () => { }) }) + describe("getTransactionHistory", () => { + it("should return proper transaction history for the given address", async () => { + // https://live.blockcypher.com/btc-testnet/address/tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx + const transactions = await electrumClient.getTransactionHistory( + "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + 5 + ) + + const transactionsHashes = transactions.map((t) => + t.transactionHash.toString() + ) + + expect(transactionsHashes).to.be.eql([ + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1", + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e", + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6", + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6", + "605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44", + ]) + }) + }) + describe("getTransaction", () => { it("should return proper transaction for the given hash", async () => { const result = await electrumClient.getTransaction( diff --git a/typescript/test/utils/mock-bitcoin-client.ts b/typescript/test/utils/mock-bitcoin-client.ts index 95e4305b1..b37dd7820 100644 --- a/typescript/test/utils/mock-bitcoin-client.ts +++ b/typescript/test/utils/mock-bitcoin-client.ts @@ -27,6 +27,7 @@ export class MockBitcoinClient implements Client { position: 0, } private _broadcastLog: RawTransaction[] = [] + private _transactionHistory = new Map() set unspentTransactionOutputs( value: Map @@ -58,6 +59,10 @@ export class MockBitcoinClient implements Client { this._transactionMerkle = value } + set transactionHistory(value: Map) { + this._transactionHistory = value + } + get broadcastLog(): RawTransaction[] { return this._broadcastLog } @@ -80,6 +85,25 @@ export class MockBitcoinClient implements Client { }) } + getTransactionHistory( + address: string, + limit?: number + ): Promise { + return new Promise((resolve, _) => { + let transactions = this._transactionHistory.get(address) as Transaction[] + + if ( + typeof limit !== "undefined" && + limit > 0 && + transactions.length > limit + ) { + transactions = transactions.slice(-limit) + } + + resolve(transactions) + }) + } + getTransaction(transactionHash: TransactionHash): Promise { return new Promise((resolve, _) => { resolve(this._transactions.get(transactionHash.toString()) as Transaction) From 499467257a3214aacf69e586a3cb412e7bd44fd3 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 17:43:16 +0200 Subject: [PATCH 11/22] Expose `determineWalletMainUtxo` function Here we expose a function allowing to determine the wallet main UTXO based on the wallet recent transactions history and the main UTXO hash stored on the Bridge contract. --- typescript/src/wallet.ts | 124 ++++++++++++++++- typescript/test/wallet.test.ts | 246 +++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 typescript/test/wallet.test.ts diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 2a0e0298b..2d1bccbf3 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -1,6 +1,14 @@ import { BigNumber } from "ethers" import { Hex } from "./hex" -import { Event, Identifier } from "./chain" +import { Bridge, Event, Identifier } from "./chain" +import { + Client as BitcoinClient, + createOutputScriptFromAddress, + encodeToBitcoinAddress, + TransactionOutput, + UnspentTransactionOutput, +} from "./bitcoin" +import { BitcoinNetwork } from "./bitcoin-network" /* eslint-disable no-unused-vars */ export enum WalletState { @@ -209,3 +217,117 @@ type DkgResult = { */ membersHash: Hex } + +/** + * Determines the plain-text wallet main UTXO currently registered in the + * Bridge on-chain contract. The returned main UTXO can be undefined if the + * wallet does not have a main UTXO registered in the Bridge at the moment. + * + * WARNING: THIS FUNCTION CANNOT DETERMINE THE MAIN UTXO IF IT COMES FROM A + * BITCOIN TRANSACTION THAT IS NOT ONE OF THE LATEST FIVE TRANSACTIONS + * TARGETING THE GIVEN WALLET PUBLIC KEY HASH. HOWEVER, SUCH A CASE IS + * VERY UNLIKELY. + * + * @param walletPublicKeyHash - Public key hash of the wallet. + * @param bridge - The handle to the Bridge on-chain contract. + * @param bitcoinClient - Bitcoin client used to interact with the network. + * @param bitcoinNetwork - Bitcoin network. + * @returns Promise holding the wallet main UTXO or undefined value. + */ +export async function determineWalletMainUtxo( + walletPublicKeyHash: Hex, + bridge: Bridge, + bitcoinClient: BitcoinClient, + bitcoinNetwork: BitcoinNetwork +): Promise { + const { mainUtxoHash } = await bridge.wallets(walletPublicKeyHash) + + // Valid case when the wallet doesn't have a main UTXO registered into + // the Bridge. + if ( + mainUtxoHash.equals( + Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + ) + ) { + return undefined + } + + // Declare a helper function that will try to determine the main UTXO for + // the given wallet address type. + const determine = async ( + witnessAddress: boolean + ): Promise => { + // Build the wallet Bitcoin address based on its public key hash. + const walletAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + witnessAddress, + bitcoinNetwork + ) + + // Get the wallet transaction history. The wallet main UTXO registered in the + // Bridge almost always comes from the latest BTC transaction made by the wallet. + // However, there may be cases where the BTC transaction was made but their + // SPV proof is not yet submitted to the Bridge thus the registered main UTXO + // points to the second last BTC transaction. In theory, such a gap between + // the actual latest BTC transaction and the registered main UTXO in the + // Bridge may be even wider. The exact behavior is a wallet implementation + // detail and not a protocol invariant so, it may be subject of changes. + // To cover the worst possible cases, we always take the five latest + // transactions made by the wallet for consideration. + const walletTransactions = await bitcoinClient.getTransactionHistory( + walletAddress, + 5 + ) + + // Get the wallet script based on the wallet address. This is required + // to find transaction outputs that lock funds on the wallet. + const walletScript = createOutputScriptFromAddress(walletAddress) + const isWalletOutput = (output: TransactionOutput) => + walletScript.equals(output.scriptPubKey) + + // Start iterating from the latest transaction as the chance it matches + // the wallet main UTXO is the highest. + for (let i = walletTransactions.length - 1; i >= 0; i--) { + const walletTransaction = walletTransactions[i] + + // Find the output that locks the funds on the wallet. Only such an output + // can be a wallet main UTXO. + const outputIndex = walletTransaction.outputs.findIndex(isWalletOutput) + + // Should never happen as all transactions come from wallet history. Just + // in case check whether the wallet output was actually found. + if (outputIndex < 0) { + continue + } + + // Build a candidate UTXO instance based on the detected output. + const utxo: UnspentTransactionOutput = { + transactionHash: walletTransaction.transactionHash, + outputIndex: outputIndex, + value: walletTransaction.outputs[outputIndex].value, + } + + // Check whether the candidate UTXO hash matches the main UTXO hash stored + // on the Bridge. + if (mainUtxoHash.equals(bridge.buildUtxoHash(utxo))) { + return utxo + } + } + + return undefined + } + + // The most common case is that the wallet uses a witness address for all + // operations. Try to determine the main UTXO for that address first as the + // chance for success is the highest here. + const mainUtxo = await determine(true) + if (mainUtxo) { + return mainUtxo + } + + // In case the main UTXO was not found for witness address, there is still + // a chance it exists for the legacy wallet address. + return determine(false) +} diff --git a/typescript/test/wallet.test.ts b/typescript/test/wallet.test.ts new file mode 100644 index 000000000..8042be548 --- /dev/null +++ b/typescript/test/wallet.test.ts @@ -0,0 +1,246 @@ +import { MockBitcoinClient } from "./utils/mock-bitcoin-client" +import { MockBridge } from "./utils/mock-bridge" +import { BitcoinNetwork, BitcoinTransaction, Hex } from "../src" +import { determineWalletMainUtxo, Wallet } from "../src/wallet" +import { expect } from "chai" +import { encodeToBitcoinAddress } from "../src/bitcoin" +import { BigNumber } from "ethers" + +describe("Wallet", () => { + describe("determineWalletMainUtxo", () => { + // Just an arbitrary 20-byte wallet public key hash. + const walletPublicKeyHash = Hex.from( + "e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0" + ) + + // Helper function facilitating creation of mock transactions. + const mockTransaction = ( + hash: string, + outputs: Record // key: locking script, value: amount of locked satoshis + ): BitcoinTransaction => { + return { + transactionHash: Hex.from(hash), + inputs: [], // not relevant in this test scenario + outputs: Object.entries(outputs).map( + ([scriptPubKey, value], index) => ({ + outputIndex: index, + value: BigNumber.from(value), + scriptPubKey: Hex.from(scriptPubKey), + }) + ), + } + } + + // Create a fake wallet witness transaction history that consists of 6 transactions. + const walletWitnessTransactionHistory: BitcoinTransaction[] = [ + mockTransaction( + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6", + { + "00140000000000000000000000000000000000000001": 100000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 200000, // wallet witness output + } + ), + mockTransaction( + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6", + { + "00140000000000000000000000000000000000000001": 100000, + "00140000000000000000000000000000000000000002": 200000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 300000, // wallet witness output + } + ), + mockTransaction( + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b", + { + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214", + { + "00140000000000000000000000000000000000000001": 100000, + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 200000, // wallet witness output + } + ), + ] + + // Create a fake wallet legacy transaction history that consists of 2 transactions. + const walletLegacyTransactionsHistory: BitcoinTransaction[] = [ + mockTransaction( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "00cc0cd13fc4de7a15cb41ab6d58f8b31c75b6b9b4194958c381441a67d09b08", + { + "00140000000000000000000000000000000000000001": 100000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 200000, // wallet legacy output + } + ), + ] + + let bridge: MockBridge + let bitcoinClient: MockBitcoinClient + let bitcoinNetwork: BitcoinNetwork + + beforeEach(async () => { + bridge = new MockBridge() + bitcoinClient = new MockBitcoinClient() + }) + + context("when wallet main UTXO is not set in the Bridge", () => { + beforeEach(async () => { + bridge.setWallet(walletPublicKeyHash.toPrefixedString(), { + mainUtxoHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + } as Wallet) + }) + + it("should return undefined", async () => { + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + + expect(mainUtxo).to.be.undefined + }) + }) + + context("when wallet main UTXO is set in the Bridge", () => { + const tests = [ + { + testName: "recent witness transaction", + // Set the main UTXO hash based on the latest transaction from walletWitnessTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + expectedMainUtxo: { + transactionHash: Hex.from( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + }, + { + testName: "recent legacy transaction", + // Set the main UTXO hash based on the second last transaction from walletLegacyTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + expectedMainUtxo: { + transactionHash: Hex.from( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + }, + { + testName: "old witness transaction", + // Set the main UTXO hash based on the oldest transaction from walletWitnessTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + expectedMainUtxo: undefined, + }, + ] + + tests.forEach(({ testName, actualMainUtxo, expectedMainUtxo }) => { + context(`with main UTXO coming from ${testName}`, () => { + const networkTests = [ + { + networkTestName: "bitcoin testnet", + network: BitcoinNetwork.Testnet, + }, + { + networkTestName: "bitcoin mainnet", + network: BitcoinNetwork.Mainnet, + }, + ] + + networkTests.forEach(({ networkTestName, network }) => { + context(`with ${networkTestName} network`, () => { + beforeEach(async () => { + bitcoinNetwork = network + + const walletWitnessAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + true, + bitcoinNetwork + ) + const walletLegacyAddress = encodeToBitcoinAddress( + walletPublicKeyHash.toString(), + false, + bitcoinNetwork + ) + + // Record the fake transaction history for both address types. + const transactionHistory = new Map< + string, + BitcoinTransaction[] + >() + transactionHistory.set( + walletWitnessAddress, + walletWitnessTransactionHistory + ) + transactionHistory.set( + walletLegacyAddress, + walletLegacyTransactionsHistory + ) + bitcoinClient.transactionHistory = transactionHistory + + bridge.setWallet(walletPublicKeyHash.toPrefixedString(), { + mainUtxoHash: bridge.buildUtxoHash(actualMainUtxo), + } as Wallet) + }) + + it("should return the expected main UTXO", async () => { + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + + expect(mainUtxo).to.be.eql(expectedMainUtxo) + }) + }) + }) + }) + }) + }) + }) +}) From 10ab6dabb5a1e400607ed2b27411a24080569071 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 11 Jul 2023 18:11:53 +0200 Subject: [PATCH 12/22] Make `findWalletForRedemption` use `determineWalletMainUtxo` function The `findWalletForRedemption` function should rely on `determineWalletMainUtxo` wallet while seeking for current wallet main UTXO. Otherwise, it may not find the main UTXO in case there is a wallet state drift between Bitcoin and Ethereum chains. --- typescript/src/redemption.ts | 57 +++++------------- typescript/test/data/redemption.ts | 92 ++++++++++++++++++++---------- typescript/test/redemption.test.ts | 50 ++++------------ 3 files changed, 85 insertions(+), 114 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 05a2a6dda..f8a0665cf 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -7,11 +7,10 @@ import { UnspentTransactionOutput, Client as BitcoinClient, TransactionHash, - encodeToBitcoinAddress, } from "./bitcoin" import { Bridge, Identifier, TBTCToken } from "./chain" import { assembleTransactionProof } from "./proof" -import { WalletState } from "./wallet" +import { determineWalletMainUtxo, WalletState } from "./wallet" import { BitcoinNetwork } from "./bitcoin-network" import { Hex } from "./hex" @@ -445,7 +444,7 @@ export async function findWalletForRedemption( for (const wallet of wallets) { const { walletPublicKeyHash } = wallet - const { state, mainUtxoHash, walletPublicKey, pendingRedemptionsValue } = + const { state, walletPublicKey, pendingRedemptionsValue } = await bridge.wallets(walletPublicKeyHash) // Wallet must be in Live state. @@ -458,18 +457,20 @@ export async function findWalletForRedemption( continue } - if ( - mainUtxoHash.equals( - Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ) - ) - ) { + // Wallet must have a main UTXO that can be determined. + const mainUtxo = await determineWalletMainUtxo( + walletPublicKeyHash, + bridge, + bitcoinClient, + bitcoinNetwork + ) + if (!mainUtxo) { console.debug( - `Main utxo not set for wallet public ` + - `key hash(${walletPublicKeyHash.toString()}). ` + + `Could not find matching UTXO on chains ` + + `for wallet public key hash (${walletPublicKeyHash.toString()}). ` + `Continue the loop execution to the next wallet...` ) + continue } const pendingRedemption = await bridge.pendingRedemptions( @@ -489,38 +490,6 @@ export async function findWalletForRedemption( continue } - const walletBitcoinAddress = encodeToBitcoinAddress( - wallet.walletPublicKeyHash.toString(), - true, - bitcoinNetwork - ) - - // TODO: In case a wallet is working on something (e.g. redemption) and a - // Bitcoin transaction was already submitted by the wallet to the bitcoin - // chain (new utxo returned from bitcoin client), but proof hasn't been - // submitted yet to the Bridge (old main utxo returned from the Bridge) the - // `findWalletForRedemption` function will not find such a wallet. To cover - // this case, we should take, for example, the last 5 transactions made by - // the wallet into account. We will address this issue in a follow-up work. - const utxos = await bitcoinClient.findAllUnspentTransactionOutputs( - walletBitcoinAddress - ) - - // We need to find correct utxo- utxo components must point to the recent - // main UTXO of the given wallet, as currently known on the chain. - const mainUtxo = utxos.find((utxo) => - mainUtxoHash.equals(bridge.buildUtxoHash(utxo)) - ) - - if (!mainUtxo) { - console.debug( - `Could not find matching UTXO on chains ` + - `for wallet public key hash(${walletPublicKeyHash.toString()}). ` + - `Continue the loop execution to the next wallet...` - ) - continue - } - const walletBTCBalance = mainUtxo.value.sub(pendingRedemptionsValue) // Save the max possible redemption amount. diff --git a/typescript/test/data/redemption.ts b/typescript/test/data/redemption.ts index 285169c11..958b9dcc2 100644 --- a/typescript/test/data/redemption.ts +++ b/typescript/test/data/redemption.ts @@ -7,10 +7,11 @@ import { UnspentTransactionOutput, TransactionMerkleBranch, TransactionHash, + createOutputScriptFromAddress, } from "../../src/bitcoin" import { RedemptionRequest } from "../../src/redemption" import { Address } from "../../src/ethereum" -import { Hex } from "../../src" +import { BitcoinTransaction, Hex } from "../../src" import { WalletState } from "../../src/wallet" /** @@ -668,13 +669,14 @@ export const redemptionProof: RedemptionProofTestData = { }, } -interface FindWalletForRedemptionWaleltData { +interface FindWalletForRedemptionWalletData { data: { state: WalletState mainUtxoHash: Hex walletPublicKey: Hex btcAddress: string - utxos: UnspentTransactionOutput[] + mainUtxo: UnspentTransactionOutput + transactions: BitcoinTransaction[] pendingRedemptionsValue: BigNumber } event: { @@ -687,10 +689,10 @@ interface FindWalletForRedemptionWaleltData { } export const findWalletForRedemptionData: { - liveWallet: FindWalletForRedemptionWaleltData - walletWithoutUtxo: FindWalletForRedemptionWaleltData - nonLiveWallet: FindWalletForRedemptionWaleltData - walletWithPendingRedemption: FindWalletForRedemptionWaleltData + liveWallet: FindWalletForRedemptionWalletData + walletWithoutUtxo: FindWalletForRedemptionWalletData + nonLiveWallet: FindWalletForRedemptionWalletData + walletWithPendingRedemption: FindWalletForRedemptionWalletData pendingRedemption: RedemptionRequest } = { liveWallet: { @@ -703,13 +705,28 @@ export const findWalletForRedemptionData: { "0x028ed84936be6a9f594a2dcc636d4bebf132713da3ce4dac5c61afbf8bbb47d6f7" ), btcAddress: "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja", - utxos: [ + mainUtxo: { + transactionHash: Hex.from( + "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" + ), + outputIndex: 0, + value: BigNumber.from("791613461"), + }, + transactions: [ { transactionHash: Hex.from( "0x5b6d040eb06b3de1a819890d55d251112e55c31db4a3f5eb7cfacf519fad7adb" ), - outputIndex: 0, - value: BigNumber.from("791613461"), + inputs: [], // not relevant + outputs: [ + { + outputIndex: 0, + value: BigNumber.from("791613461"), + scriptPubKey: createOutputScriptFromAddress( + "tb1qqwm566yn44rdlhgph8sw8vecta8uutg79afuja" + ), + }, + ], }, ], pendingRedemptionsValue: BigNumber.from(0), @@ -740,15 +757,14 @@ export const findWalletForRedemptionData: { "0x030fbbae74e6d85342819e719575949a1349e975b69fb382e9fef671a3a74efc52" ), btcAddress: "tb1qkct7r24k4wutnsun84rvp3qsyt8yfpvqz89d2y", - utxos: [ - { - transactionHash: Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ), - outputIndex: 0, - value: BigNumber.from("0"), - }, - ], + mainUtxo: { + transactionHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + outputIndex: 0, + value: BigNumber.from("0"), + }, + transactions: [], pendingRedemptionsValue: BigNumber.from(0), }, event: { @@ -778,15 +794,14 @@ export const findWalletForRedemptionData: { "0x02633b102417009ae55103798f4d366dfccb081dcf20025088b9bf10a8e15d8ded" ), btcAddress: "tb1qf6jvyd680ncf9dtr5znha9ql5jmw84lupwwuf6", - utxos: [ - { - transactionHash: Hex.from( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ), - outputIndex: 0, - value: BigNumber.from("0"), - }, - ], + mainUtxo: { + transactionHash: Hex.from( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + outputIndex: 0, + value: BigNumber.from("0"), + }, + transactions: [], pendingRedemptionsValue: BigNumber.from(0), }, event: { @@ -815,13 +830,28 @@ export const findWalletForRedemptionData: { "0x02ab193a63b3523bfab77d3645d11da10722393687458c4213b350b7e08f50b7ee" ), btcAddress: "tb1qx2xejtjltdcau5dpks8ucszkhxdg3fj88404lh", - utxos: [ + mainUtxo: { + transactionHash: Hex.from( + "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" + ), + outputIndex: 0, + value: BigNumber.from("3370000"), // 0.0337 BTC + }, + transactions: [ { transactionHash: Hex.from( "0x81c4884a8c2fccbeb57745a5e59f895a9c1bb8fc42eecc82269100a1a46bbb85" ), - outputIndex: 0, - value: BigNumber.from("3370000"), // 0.0337 BTC + inputs: [], // not relevant + outputs: [ + { + outputIndex: 0, + value: BigNumber.from("3370000"), // 0.0337 BTC + scriptPubKey: createOutputScriptFromAddress( + "tb1qx2xejtjltdcau5dpks8ucszkhxdg3fj88404lh" + ), + }, + ], }, ], pendingRedemptionsValue: BigNumber.from(2370000), // 0.0237 BTC diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 15b321075..e2efa36d1 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -39,6 +39,7 @@ import { BigNumberish, BigNumber } from "ethers" import { BitcoinNetwork } from "../src/bitcoin-network" import { Wallet } from "../src/wallet" import { MockTBTCToken } from "./utils/mock-tbtc-token" +import { BitcoinTransaction } from "../src" chai.use(chaiAsPromised) @@ -1490,9 +1491,9 @@ describe("Redemption", () => { (wallet) => wallet.event ) - const walletsUnspentTransacionOutputs = new Map< + const walletsTransactionHistory = new Map< string, - UnspentTransactionOutput[] + BitcoinTransaction[] >() walletsOrder.forEach((wallet) => { @@ -1501,11 +1502,11 @@ describe("Redemption", () => { mainUtxoHash, walletPublicKey, btcAddress, - utxos, + transactions, pendingRedemptionsValue, } = wallet.data - walletsUnspentTransacionOutputs.set(btcAddress, utxos) + walletsTransactionHistory.set(btcAddress, transactions) bridge.setWallet( wallet.event.walletPublicKeyHash.toPrefixedString(), { @@ -1517,8 +1518,7 @@ describe("Redemption", () => { ) }) - bitcoinClient.unspentTransactionOutputs = - walletsUnspentTransacionOutputs + bitcoinClient.transactionHistory = walletsTransactionHistory }) context( @@ -1545,29 +1545,13 @@ describe("Redemption", () => { }) }) - it("should get wallet data details", () => { - const bridgeWalletDetailsLogs = bridge.walletsLog - - const wallets = Array.from(walletsOrder) - // Remove last live wallet. - wallets.pop() - - expect(bridgeWalletDetailsLogs.length).to.eql(wallets.length) - - wallets.forEach((wallet, index) => { - expect(bridgeWalletDetailsLogs[index].walletPublicKeyHash).to.eql( - wallet.event.walletPublicKeyHash.toPrefixedString() - ) - }) - }) - it("should return the wallet data that can handle redemption request", () => { const expectedWalletData = findWalletForRedemptionData.walletWithPendingRedemption.data expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } @@ -1579,8 +1563,7 @@ describe("Redemption", () => { const amount = BigNumber.from("10000000000") // 1 000 BTC const expectedMaxAmount = walletsOrder .map((wallet) => wallet.data) - .map((wallet) => wallet.utxos) - .flat() + .map((wallet) => wallet.mainUtxo) .map((utxo) => utxo.value) .sort((a, b) => (b.gt(a) ? 0 : -1))[0] @@ -1647,24 +1630,13 @@ describe("Redemption", () => { }) }) - it("should get wallet data details", () => { - const bridgeWalletDetailsLogs = bridge.walletsLog - - expect(bridgeWalletDetailsLogs.length).to.eql(walletsOrder.length) - walletsOrder.forEach((wallet, index) => { - expect(bridgeWalletDetailsLogs[index].walletPublicKeyHash).to.eql( - wallet.event.walletPublicKeyHash.toPrefixedString() - ) - }) - }) - it("should skip the wallet for which there is a pending redemption request to the same redeemer output script and return the wallet data that can handle redemption request", () => { const expectedWalletData = findWalletForRedemptionData.liveWallet.data expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } @@ -1676,7 +1648,7 @@ describe("Redemption", () => { beforeEach(async () => { const wallet = findWalletForRedemptionData.walletWithPendingRedemption - const walletBTCBalance = wallet.data.utxos[0].value + const walletBTCBalance = wallet.data.mainUtxo.value const amount: BigNumber = walletBTCBalance .sub(wallet.data.pendingRedemptionsValue) @@ -1699,7 +1671,7 @@ describe("Redemption", () => { expect(result).to.deep.eq({ walletPublicKey: expectedWalletData.walletPublicKey.toString(), - mainUtxo: expectedWalletData.utxos[0], + mainUtxo: expectedWalletData.mainUtxo, }) }) } From 0e369b2f3ba1b80e23b324c5a59bcd5bf88c699b Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:08:28 +0200 Subject: [PATCH 13/22] Add `console.error` if wallet output not found for wallet transaction --- typescript/src/wallet.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 2d1bccbf3..7f053b79a 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -299,6 +299,9 @@ export async function determineWalletMainUtxo( // Should never happen as all transactions come from wallet history. Just // in case check whether the wallet output was actually found. if (outputIndex < 0) { + console.error( + `wallet output for transaction ${walletTransaction.transactionHash.toString()} not found` + ) continue } From 41f917a7ccce0b998161b80df5447262426e911b Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 11:08:29 +0200 Subject: [PATCH 14/22] Bump tbtc-v2.ts dependency in system-tests --- system-tests/yarn.lock | 405 ++++++++++++++++++++++++++++++----------- 1 file changed, 303 insertions(+), 102 deletions(-) diff --git a/system-tests/yarn.lock b/system-tests/yarn.lock index e30872622..9d178326c 100644 --- a/system-tests/yarn.lock +++ b/system-tests/yarn.lock @@ -1492,16 +1492,16 @@ resolved "https://registry.yarnpkg.com/@keep-network/bitcoin-spv-sol/-/bitcoin-spv-sol-3.4.0-solc-0.8.tgz#8b44c246ffab8ea993efe196f6bf385b1a3b84dc" integrity sha512-KlpY9BbasyLvYXSS7dsJktgRChu/yjdFLOX8ldGA/pltLicCm/l0F4oqxL8wSws9XD12vq9x0B5qzPygVLB2TQ== -"@keep-network/ecdsa@2.1.0-dev.6": - version "2.1.0-dev.6" - resolved "https://registry.yarnpkg.com/@keep-network/ecdsa/-/ecdsa-2.1.0-dev.6.tgz#ccc690f784b6e802a5b80b2dfb7127d96e548a25" - integrity sha512-1D74OPVzzxxVcG8za/niuxmwEdDc5R6KNuHsUvsFkcHNJE1UgQNw+QdrI+k3M2so6YrO4L5lP7vTvIvBDbEMNQ== +"@keep-network/ecdsa@2.1.0-dev.15": + version "2.1.0-dev.15" + resolved "https://registry.yarnpkg.com/@keep-network/ecdsa/-/ecdsa-2.1.0-dev.15.tgz#ee631a42e165f30c75aae8c54aace765b77e272a" + integrity sha512-iUE3SwDSNc/k1oui7Z+fDGhhGyOzpe4/f/oKvDUMHqXx0BQG3QCrOz9KqWuPFXTXMav4LxLbt12WyDITAl/hjw== dependencies: - "@keep-network/random-beacon" "2.1.0-dev.5" + "@keep-network/random-beacon" "2.1.0-dev.15" "@keep-network/sortition-pools" "^2.0.0-pre.16" "@openzeppelin/contracts" "^4.6.0" "@openzeppelin/contracts-upgradeable" "^4.6.0" - "@threshold-network/solidity-contracts" "1.3.0-dev.3" + "@threshold-network/solidity-contracts" "1.3.0-dev.6" "@keep-network/hardhat-helpers@^0.6.0-pre.7": version "0.6.0-pre.7" @@ -1538,15 +1538,25 @@ version "0.0.1" resolved "https://codeload.github.com/keep-network/prettier-config-keep/tar.gz/a1a333e7ac49928a0f6ed39421906dd1e46ab0f3" -"@keep-network/random-beacon@2.1.0-dev.5": - version "2.1.0-dev.5" - resolved "https://registry.yarnpkg.com/@keep-network/random-beacon/-/random-beacon-2.1.0-dev.5.tgz#5ea1a76f57c8171fe3b12ecf4cfcefee38f954ac" - integrity sha512-v3Mqzwx69WqG5bi8qEO4b72PpDMbwl69f5PYHZ0vO3g2pzU1PpVq2nq/vzgdqW2xgztvnHFwOq+lOyN8hx0K3A== +"@keep-network/random-beacon@2.1.0-dev.15": + version "2.1.0-dev.15" + resolved "https://registry.yarnpkg.com/@keep-network/random-beacon/-/random-beacon-2.1.0-dev.15.tgz#541620c469e3bc75a5d1f7649889540b0e032e9e" + integrity sha512-vxBICRtmqSmJtFU5hZMpwB0alhgKchyMbxk4DtLZ7T2zBjd5tjt3CqeKEk+ON09g7yL1mIxY07InP4okviUK4A== dependencies: "@keep-network/sortition-pools" "^2.0.0-pre.16" - "@openzeppelin/contracts" "^4.6.0" + "@openzeppelin/contracts" "4.7.3" + "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" + "@threshold-network/solidity-contracts" "1.3.0-dev.5" + +"@keep-network/random-beacon@2.1.0-dev.16": + version "2.1.0-dev.16" + resolved "https://registry.yarnpkg.com/@keep-network/random-beacon/-/random-beacon-2.1.0-dev.16.tgz#9f2b5c19aa79f6ff1a5498ba7b55eb170463161d" + integrity sha512-o+cG/VDkhUc91W+4bMplYCgOu0twSFICqarpv5k2ES8GcaafaeV8stXGhCxjvHYJjU/sfG8mhlQZhWdZixq+JQ== + dependencies: + "@keep-network/sortition-pools" "^2.0.0-pre.16" + "@openzeppelin/contracts" "4.7.3" "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" - "@threshold-network/solidity-contracts" "1.3.0-dev.3" + "@threshold-network/solidity-contracts" "1.3.0-dev.6" "@keep-network/sortition-pools@1.2.0-dev.1": version "1.2.0-dev.1" @@ -1564,30 +1574,31 @@ "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" "@keep-network/tbtc-v2.ts@development": - version "1.1.0-dev.8" - resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2.ts/-/tbtc-v2.ts-1.1.0-dev.8.tgz#9a3e0cc962681fe14c8b198d2b21e84d27ebdb4b" - integrity sha512-F5PEd+oaZuueQsFVBJ4MJcs8nVBOJJaXqVJJzWnLWw1/lARqT0H5m1dvMofuFTF76y9ShxbHNT1BYQOEX7dlJQ== + version "1.3.0-dev.5" + resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2.ts/-/tbtc-v2.ts-1.3.0-dev.5.tgz#f01516207ddeb40b33f1c9a64022de9b8a216915" + integrity sha512-UmZg56yflaWJWKk7UdXgndc4duK7crnOyBAfiHBdrMJQiFXb0qpNKkbZAsVbpfxD2P1n0VwWitczXTQAPyKNRQ== dependencies: - "@keep-network/ecdsa" "2.1.0-dev.6" - "@keep-network/tbtc-v2" "1.0.3-dev.0" + "@keep-network/ecdsa" "2.1.0-dev.15" + "@keep-network/tbtc-v2" "1.6.0-dev.0" bcoin "git+https://github.com/keep-network/bcoin.git#5accd32c63e6025a0d35d67739c4a6e84095a1f8" bcrypto "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.5.0" bufio "^1.0.6" electrum-client-js "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1" ethers "^5.5.2" + p-timeout "^4.1.0" wif "2.0.6" -"@keep-network/tbtc-v2@1.0.3-dev.0": - version "1.0.3-dev.0" - resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2/-/tbtc-v2-1.0.3-dev.0.tgz#754eab80269ea5a616c92cb8c1f607ec21343e0b" - integrity sha512-RqIFZvJtbLgmPZvPgamIJoTc5UsosrPE2g3879RqU4XqntezF4gr95FIuUFmicjRy0OPsjCupU2HplxlWfQfdw== +"@keep-network/tbtc-v2@1.6.0-dev.0": + version "1.6.0-dev.0" + resolved "https://registry.yarnpkg.com/@keep-network/tbtc-v2/-/tbtc-v2-1.6.0-dev.0.tgz#ba95805cef3f04bde7379d3c3b14e882a9cfa080" + integrity sha512-5N2dMdFSdS+Ljvqnqoscft5xnbIK/U/z8Dc2hNXWULkPhIy0Mx/E7i7I4CpBTV4LazIo1Hq6W4EJtj+lmrekgg== dependencies: "@keep-network/bitcoin-spv-sol" "3.4.0-solc-0.8" - "@keep-network/ecdsa" "2.1.0-dev.6" - "@keep-network/random-beacon" "2.1.0-dev.5" + "@keep-network/ecdsa" "2.1.0-dev.15" + "@keep-network/random-beacon" "2.1.0-dev.16" "@keep-network/tbtc" "1.1.2-dev.1" - "@openzeppelin/contracts" "^4.6.0" - "@openzeppelin/contracts-upgradeable" "^4.6.0" + "@openzeppelin/contracts" "^4.8.1" + "@openzeppelin/contracts-upgradeable" "^4.8.1" "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" "@keep-network/tbtc@1.1.2-dev.1": @@ -1694,25 +1705,30 @@ "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" -"@openzeppelin/contracts-upgradeable@^4.6.0": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.1.tgz#363f7dd08f25f8f77e16d374350c3d6b43340a7a" - integrity sha512-1wTv+20lNiC0R07jyIAbHU7TNHKRwGiTGRfiNnA8jOWjKT98g5OgLpYWOi40Vgpk8SPLA9EvfJAbAeIyVn+7Bw== +"@openzeppelin/contracts-upgradeable@^4.6.0", "@openzeppelin/contracts-upgradeable@^4.8.1": + version "4.9.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.2.tgz#a817c75688f8daede420052fbcb34e52482e769e" + integrity sha512-siviV3PZV/fHfPaoIC51rf1Jb6iElkYWnNYZ0leO23/ukXuvOyoC/ahy8jqiV7g+++9Nuo3n/rk5ajSN/+d/Sg== "@openzeppelin/contracts-upgradeable@~4.5.2": version "4.5.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.5.2.tgz#90d9e47bacfd8693bfad0ac8a394645575528d05" integrity sha512-xgWZYaPlrEOQo3cBj97Ufiuv79SPd8Brh4GcFYhPgb6WvAq4ppz8dWKL6h+jLAK01rUqMRp/TS9AdXgAeNvCLA== +"@openzeppelin/contracts@4.7.3": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e" + integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw== + "@openzeppelin/contracts@^2.4.0": version "2.5.1" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-2.5.1.tgz#c76e3fc57aa224da3718ec351812a4251289db31" integrity sha512-qIy6tLx8rtybEsIOAlrM4J/85s2q2nPkDqj/Rx46VakBZ0LwtFhXIVub96LXHczQX0vaqmAueDqNPXtbSXSaYQ== -"@openzeppelin/contracts@^4.1.0", "@openzeppelin/contracts@^4.3.2", "@openzeppelin/contracts@^4.6.0": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4" - integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ== +"@openzeppelin/contracts@^4.1.0", "@openzeppelin/contracts@^4.3.2", "@openzeppelin/contracts@^4.6.0", "@openzeppelin/contracts@^4.8.1": + version "4.9.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.2.tgz#1cb2d5e4d3360141a17dbc45094a8cad6aac16c1" + integrity sha512-mO+y6JaqXjWeMh9glYVzVu8HYPGknAAnWyxTRhGeckOruyXQMNnlcW6w/Dx9ftLeIQk6N+ZJFuVmTwF7lEIFrg== "@openzeppelin/contracts@~4.5.0": version "4.5.0" @@ -2001,6 +2017,7 @@ eslint-config-prettier "^8.3.0" eslint-plugin-import "^2.23.4" eslint-plugin-jsx-a11y "^6.4.1" + eslint-plugin-no-only-tests "^2.6.0" eslint-plugin-prettier "^4.0.0" eslint-plugin-react "^7.25.2" eslint-plugin-react-hooks "^4.2.0" @@ -2015,10 +2032,20 @@ dependencies: "@openzeppelin/contracts" "^4.1.0" -"@threshold-network/solidity-contracts@1.3.0-dev.3": - version "1.3.0-dev.3" - resolved "https://registry.yarnpkg.com/@threshold-network/solidity-contracts/-/solidity-contracts-1.3.0-dev.3.tgz#aa896b80a083ca8a7cb5219e3c9d1c47e3d86b03" - integrity sha512-BNm5+JKrFvg9hZ02Sp/A+vKs1PQB37rYdcZqLrLhvwDFzHFvL+XA2IXqvN1CznQTeehwnX3DtCcONTVP42i56A== +"@threshold-network/solidity-contracts@1.3.0-dev.5": + version "1.3.0-dev.5" + resolved "https://registry.yarnpkg.com/@threshold-network/solidity-contracts/-/solidity-contracts-1.3.0-dev.5.tgz#f7a2727d627a10218f0667bc0d33e19ed8f87fdc" + integrity sha512-AInTKQkJ0PKa32q2m8GnZFPYEArsnvOwhIFdBFaHdq9r4EGyqHMf4YY1WjffkheBZ7AQ0DNA8Lst30kBoQd0SA== + dependencies: + "@keep-network/keep-core" ">1.8.1-dev <1.8.1-goerli" + "@openzeppelin/contracts" "~4.5.0" + "@openzeppelin/contracts-upgradeable" "~4.5.2" + "@thesis/solidity-contracts" "github:thesis/solidity-contracts#4985bcf" + +"@threshold-network/solidity-contracts@1.3.0-dev.6": + version "1.3.0-dev.6" + resolved "https://registry.yarnpkg.com/@threshold-network/solidity-contracts/-/solidity-contracts-1.3.0-dev.6.tgz#41e34a84f409f63635e59f9a6ce170df1472b8a1" + integrity sha512-U7nMp+86M5qkjW7YUvT3qWgRiEEUIxqE96vkEiARTOkWX5JdLP2CXehkHCkEzjdgOCczmCp3fFtcgKFnQhhZ8A== dependencies: "@keep-network/keep-core" ">1.8.1-dev <1.8.1-goerli" "@openzeppelin/contracts" "~4.5.0" @@ -2128,9 +2155,9 @@ integrity sha512-lIxCk6G7AwmUagQ4gIQGxUBnvAq664prFD9nSAz6dgd1XmBXBtZABV/op+QsJsIyaP1GZsf/iXhYKHX3azSRCw== "@types/debug@^4.1.5": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== dependencies: "@types/ms" "*" @@ -2191,9 +2218,9 @@ "@types/node" "*" "@types/lodash@^4.14.170": - version "4.14.191" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== "@types/lru-cache@^5.1.0": version "5.1.1" @@ -2652,6 +2679,14 @@ array-back@^4.0.1, array-back@^4.0.2: resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2823,8 +2858,8 @@ base64-js@^1.3.1: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== "bcfg@git+https://github.com/bcoin-org/bcfg.git#semver:~0.1.7": - version "0.1.7" - resolved "git+https://github.com/bcoin-org/bcfg.git#05122154b35baa82cd01dc9478ebee7346386ba1" + version "0.1.8" + resolved "git+https://github.com/bcoin-org/bcfg.git#90e1aff3b040160cd73956a500765ffcc823f0c2" dependencies: bsert "~0.0.10" @@ -2869,8 +2904,8 @@ bcrypt-pbkdf@^1.0.0: tweetnacl "^0.14.3" "bcrypto@git+https://github.com/bcoin-org/bcrypto.git#semver:~5.5.0": - version "5.5.0" - resolved "git+https://github.com/bcoin-org/bcrypto.git#34738cf15033e3bce91a4f6f41ec1ebee3c2fdc8" + version "5.5.1" + resolved "git+https://github.com/bcoin-org/bcrypto.git#42bcbd52831042f08cdf178d2cf30eacb62a4446" dependencies: bufio "~1.0.7" loady "~0.0.5" @@ -2908,8 +2943,8 @@ bech32@1.1.4: bsert "~0.0.10" "bfile@git+https://github.com/bcoin-org/bfile.git#semver:~0.2.1": - version "0.2.2" - resolved "git+https://github.com/bcoin-org/bfile.git#c3075133a02830dc384f8353d8275d4499b8bff9" + version "0.2.3" + resolved "git+https://github.com/bcoin-org/bfile.git#c13235d04974f0fa5a487fdbaf74611523e2f4e6" "bfilter@git+https://github.com/bcoin-org/bfilter.git#semver:~2.3.0": version "2.3.0" @@ -3093,7 +3128,7 @@ bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -body-parser@1.20.1, body-parser@^1.16.0: +body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -3111,6 +3146,24 @@ body-parser@1.20.1, body-parser@^1.16.0: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^1.16.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3218,10 +3271,15 @@ bs58check@<3.0.0, bs58check@^2.1.1, bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" -"bsert@git+https://github.com/chjj/bsert.git#semver:~0.0.10", bsert@~0.0.10: +"bsert@git+https://github.com/chjj/bsert.git#semver:~0.0.10": version "0.0.10" resolved "git+https://github.com/chjj/bsert.git#bd09d49eab8644bca08ae8259a3d8756e7d453fc" +bsert@~0.0.10: + version "0.0.12" + resolved "https://registry.yarnpkg.com/bsert/-/bsert-0.0.12.tgz#157c6a6beb1548af3b14d484fcd2a78eb440599d" + integrity sha512-lUB0EMu4KhIf+VQ6RZJ7J3dFdohYSeta+gNgDi00Hi/t3k/W6xZlwm9PSSG0q7hJ2zW9Rsn5yaMPymETxroTRw== + "bsock@git+https://github.com/bcoin-org/bsock.git#semver:~0.1.9", bsock@~0.1.8, bsock@~0.1.9: version "0.1.9" resolved "git+https://github.com/bcoin-org/bsock.git#7cf76b3021ae7929c023d1170f789811e91ae528" @@ -3328,10 +3386,15 @@ bufio@^1.0.6: resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.0.tgz#b9ad1c06b0d9010363c387c39d2810a7086d143f" integrity sha512-UlFk8z/PwdhYQTXSQQagwGAdtRI83gib2n4uy4rQnenxUM2yQi8lBDzF230BNk+3wAoZDxYRoBwVVUPgHa9MCA== -"bufio@git+https://github.com/bcoin-org/bufio.git#semver:~1.0.6", bufio@~1.0.7: +"bufio@git+https://github.com/bcoin-org/bufio.git#semver:~1.0.6": version "1.0.7" resolved "git+https://github.com/bcoin-org/bufio.git#91ae6c93899ff9fad7d7cee9afd2a1c4933ca984" +bufio@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.0.7.tgz#b7f63a1369a0829ed64cc14edf0573b3e382a33e" + integrity sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A== + "bupnp@git+https://github.com/bcoin-org/bupnp.git#semver:~0.2.6": version "0.2.6" resolved "git+https://github.com/bcoin-org/bupnp.git#c44fa7356aa297c9de96e8ad094a6816939cd688" @@ -3341,8 +3404,8 @@ bufio@^1.0.6: bsert "~0.0.10" "bval@git+https://github.com/bcoin-org/bval.git#semver:~0.1.6": - version "0.1.7" - resolved "git+https://github.com/bcoin-org/bval.git#5dcc923f24da9fb7eb96269ef8ce01540da983e7" + version "0.1.8" + resolved "git+https://github.com/bcoin-org/bval.git#f9c44d510bbc5bcc13cbd4b67e9704a24cc5ec0e" dependencies: bsert "~0.0.10" @@ -3377,9 +3440,9 @@ cacheable-request@^6.0.0: responselike "^1.0.2" cacheable-request@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" - integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== dependencies: clone-response "^1.0.2" get-stream "^5.1.0" @@ -3696,10 +3759,10 @@ content-hash@^2.5.2: multicodec "^0.5.5" multihashes "^0.4.15" -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== cookie-signature@1.0.6: version "1.0.6" @@ -3807,11 +3870,11 @@ cross-fetch@3.0.4: whatwg-fetch "3.0.0" cross-fetch@^3.0.6: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== dependencies: - node-fetch "2.6.7" + node-fetch "^2.6.12" cross-spawn@^7.0.2: version "7.0.3" @@ -3914,7 +3977,7 @@ debug@^3.1.0, debug@^3.2.7: decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== decamelize@^4.0.0: version "4.0.0" @@ -4035,7 +4098,15 @@ deferred-leveldown@~5.3.0: abstract-leveldown "~6.2.1" inherits "^2.0.3" -define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: +define-properties@^1.1.2, define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== @@ -4059,9 +4130,9 @@ depd@2.0.0: integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + version "1.1.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" + integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== dependencies: inherits "^2.0.1" minimalistic-assert "^1.0.0" @@ -4314,6 +4385,46 @@ es-abstract@^1.19.1: string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" +es-abstract@^1.21.2: + version "1.21.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" + integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== + dependencies: + array-buffer-byte-length "^1.0.0" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.0" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + es-array-method-boxes-properly@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" @@ -4495,7 +4606,7 @@ eslint-plugin-jsx-a11y@^6.4.1: language-tags "^1.0.5" minimatch "^3.0.4" -eslint-plugin-no-only-tests@^2.3.1: +eslint-plugin-no-only-tests@^2.3.1, eslint-plugin-no-only-tests@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-2.6.0.tgz#19f6c9620bda02b9b9221b436c5f070e42628d76" integrity sha512-T9SmE/g6UV1uZo1oHAqOvL86XWl7Pl2EpRpnLI8g/bkJu+h7XBCB+1LnubRZ2CUQXj805vh4/CYZdnqtVaEo2Q== @@ -4668,7 +4779,7 @@ etag@~1.8.1: eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" - integrity sha1-IprEbsqG1S4MmR58sq74P/D2i88= + integrity sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw== dependencies: idna-uts46-hx "^2.3.1" js-sha3 "^0.5.7" @@ -5383,9 +5494,9 @@ functions-have-names@^1.2.2: integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== futoin-hkdf@^1.0.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.5.1.tgz#141f00427bc9950b38a42aa786b99c318b9b688d" - integrity sha512-g5d0Qp7ks55hYmYmfqn4Nz18XH49lcCR+vvIvHT92xXnsJaGZmY1EtWQWilJ6BQp57heCIXM/rRo+AFep8hGgg== + version "1.5.2" + resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.5.2.tgz#d316623d29f45fe5e6f136f435eccd74096bf676" + integrity sha512-Bnytx8kQJQoEAPGgTZw3kVPy8e/n9CDftPzc0okgaujmbdF1x7w8wg+u2xS0CML233HgruNk6VQW28CzuUFMKw== ganache@7.0.3: version "7.0.3" @@ -5425,6 +5536,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -5562,9 +5683,9 @@ globby@^11.0.3: slash "^3.0.0" google-libphonenumber@^3.2.15, google-libphonenumber@^3.2.4: - version "3.2.31" - resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.31.tgz#d2c4d4c8d7385be70b515086e4d28dd20da50600" - integrity sha512-l3bzAkfN4ITICKvuqEiY7JN06RxDAviOoKMtD2KfGYjGK3btPO8Xav7k0fgmf1Ud/pEm523yBh1/s/xDtKEvnw== + version "3.2.32" + resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.32.tgz#63c48a9c247b64a3bc2eec21bdf3fcfbf2e148c0" + integrity sha512-mcNgakausov/B/eTgVeX8qc8IwWjRrupk9UzZZ/QDEvdh5fAjE7Aa211bkZpZj42zKkeS6MTT8avHUwjcLxuGQ== gopd@^1.0.1: version "1.0.1" @@ -5627,7 +5748,12 @@ got@^7.1.0: url-parse-lax "^1.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.10, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.10: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -5826,9 +5952,9 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-errors@2.0.0: version "2.0.0" @@ -5960,6 +6086,15 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + invariant@2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -6006,6 +6141,15 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" +is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -6217,6 +6361,11 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -6550,10 +6699,15 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -"loady@git+https://github.com/chjj/loady.git#semver:~0.0.1", loady@~0.0.1, loady@~0.0.5: +"loady@git+https://github.com/chjj/loady.git#semver:~0.0.1": version "0.0.5" resolved "git+https://github.com/chjj/loady.git#b94958b7ee061518f4b85ea6da380e7ee93222d5" +loady@~0.0.1, loady@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881" + integrity sha512-uxKD2HIj042/HBx77NBcmEPsD+hxCgAtjEWlYNScuUjIsh/62Uyu39GOR68TBR68v+jqDL9zfftCWoUo4y03sQ== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6836,7 +6990,12 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== @@ -6864,9 +7023,9 @@ mkdirp-promise@^5.0.1: mkdirp "*" mkdirp@*: - version "2.1.3" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.3.tgz#b083ff37be046fd3d6552468c1f0ff44c1545d1f" - integrity sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== mkdirp@0.5.4: version "0.5.4" @@ -7088,7 +7247,14 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@2.6.7, node-fetch@^2.6.7: +node-fetch@^2.6.12: + version "2.6.12" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -7168,7 +7334,7 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.0, object-inspect@^1.12.2, object-inspect@^1.9.0: +object-inspect@^1.12.0, object-inspect@^1.12.2, object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== @@ -7217,14 +7383,15 @@ object.fromentries@^2.0.5: es-abstract "^1.19.1" object.getownpropertydescriptors@^2.0.3: - version "2.1.5" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3" - integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw== + version "2.1.6" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312" + integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ== dependencies: array.prototype.reduce "^1.0.5" call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.21.2" + safe-array-concat "^1.0.0" object.hasown@^1.1.1: version "1.1.1" @@ -7393,6 +7560,11 @@ p-timeout@^1.1.1: dependencies: p-finally "^1.0.0" +p-timeout@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" + integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -7528,7 +7700,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pify@^3.0.0: version "3.0.0" @@ -7733,6 +7905,16 @@ raw-body@2.5.1, raw-body@^2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7756,9 +7938,9 @@ read-pkg@^1.0.0: path-type "^1.0.0" readable-stream@^2.3.0, readable-stream@^2.3.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -7980,6 +8162,16 @@ rxjs@6: dependencies: tslib "^1.9.0" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -8403,6 +8595,15 @@ string.prototype.matchall@^4.0.7: regexp.prototype.flags "^1.4.1" side-channel "^1.0.4" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimend@^1.0.5, string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -9880,9 +10081,9 @@ which-module@^1.0.0: integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== which-typed-array@^1.1.2, which-typed-array@^1.1.9: version "1.1.9" @@ -9920,7 +10121,7 @@ wide-align@1.1.3: wif@2.0.6, wif@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" - integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= + integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ== dependencies: bs58check "<3.0.0" From b56d4db24607830485c1f602639c78c9a3979903 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 11:15:02 +0200 Subject: [PATCH 15/22] Fix system tests Request the redemption via the `depositorBridgeHandle.requestRedemption` directly instead of `requestRedemption` function. After refactoring in `#632`, the `requestRedemption` requests redemption via tBTC token contract. In the current deposit scenario, we do not pass the vault address so the `depositor` does not actually have any tBTC tokens so we can't request redemption via `requestRedemption` fn with changes from `#632`. We are going to add a new scenario where we pass the TBTC vault address to the `revealDeposit` function to test the new mechanism of requesting the redemption via tBTC token contract in follow-up work. --- system-tests/test/deposit-redemption.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system-tests/test/deposit-redemption.test.ts b/system-tests/test/deposit-redemption.test.ts index 6a7ebfbb3..f72a13894 100644 --- a/system-tests/test/deposit-redemption.test.ts +++ b/system-tests/test/deposit-redemption.test.ts @@ -262,12 +262,11 @@ describe("System Test - Deposit and redemption", () => { systemTestsContext.depositorBitcoinKeyPair.publicKey.compressed )}` - await TBTC.requestRedemption( + await depositorBridgeHandle.requestRedemption( systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, sweepUtxo, redeemerOutputScript, requestedAmount, - depositorBridgeHandle ) console.log( From 056cac5b657d984d3ddba4605b879a4e540780fe Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:18:00 +0200 Subject: [PATCH 16/22] Use `createOutputScriptFromAddress` where applicable --- typescript/src/electrum.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typescript/src/electrum.ts b/typescript/src/electrum.ts index d467f42f7..022ccce2a 100644 --- a/typescript/src/electrum.ts +++ b/typescript/src/electrum.ts @@ -2,6 +2,7 @@ import bcoin from "bcoin" import pTimeout from "p-timeout" import { Client as BitcoinClient, + createOutputScriptFromAddress, RawTransaction, Transaction, TransactionHash, @@ -232,7 +233,7 @@ export class Client implements BitcoinClient { ): Promise { return this.withElectrum( async (electrum: Electrum) => { - const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + const script = createOutputScriptFromAddress(address).toString() // eslint-disable-next-line camelcase type UnspentOutput = { tx_pos: number; value: number; tx_hash: string } @@ -262,7 +263,7 @@ export class Client implements BitcoinClient { limit?: number ): Promise { return this.withElectrum(async (electrum: Electrum) => { - const script = bcoin.Script.fromAddress(address).toRaw().toString("hex") + const script = createOutputScriptFromAddress(address).toString() // eslint-disable-next-line camelcase type HistoryItem = { height: number; tx_hash: string } From 7b0b9bdb7fb947614f9ddff195c73f3a001bb7b9 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:20:42 +0200 Subject: [PATCH 17/22] Simplify `determineWalletMainUtxo` return statement --- typescript/src/wallet.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/typescript/src/wallet.ts b/typescript/src/wallet.ts index 7f053b79a..5c9a46ae1 100644 --- a/typescript/src/wallet.ts +++ b/typescript/src/wallet.ts @@ -326,11 +326,8 @@ export async function determineWalletMainUtxo( // operations. Try to determine the main UTXO for that address first as the // chance for success is the highest here. const mainUtxo = await determine(true) - if (mainUtxo) { - return mainUtxo - } // In case the main UTXO was not found for witness address, there is still // a chance it exists for the legacy wallet address. - return determine(false) + return mainUtxo ?? (await determine(false)) } From f2e5b9f938a6e455609a7c5bb9933bd5fa046492 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 12 Jul 2023 11:27:48 +0200 Subject: [PATCH 18/22] Improve unit tests of `determineWalletMainUtxo` --- typescript/test/wallet.test.ts | 47 +++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/typescript/test/wallet.test.ts b/typescript/test/wallet.test.ts index 8042be548..277b07e5a 100644 --- a/typescript/test/wallet.test.ts +++ b/typescript/test/wallet.test.ts @@ -78,8 +78,37 @@ describe("Wallet", () => { ), ] - // Create a fake wallet legacy transaction history that consists of 2 transactions. - const walletLegacyTransactionsHistory: BitcoinTransaction[] = [ + // Create a fake wallet legacy transaction history that consists of 6 transactions. + const walletLegacyTransactionHistory: BitcoinTransaction[] = [ + mockTransaction( + "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267", + { + "00140000000000000000000000000000000000000001": 100000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 200000, // wallet legacy output + } + ), + mockTransaction( + "b11bfc481b95769b8488bd661d5f61a35f7c3c757160494d63f6e04e532dfcb9", + { + "00140000000000000000000000000000000000000001": 100000, + "00140000000000000000000000000000000000000002": 200000, + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 300000, // wallet legacy output + } + ), + mockTransaction( + "7e91580d989f8541489a37431381ff9babd596111232f1bc7a1a1ba503c27dee", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), + mockTransaction( + "5404e339ba82e6e52fcc24cb40029bed8425baa4c7f869626ef9de956186f910", + { + "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output + "00140000000000000000000000000000000000000001": 200000, + } + ), mockTransaction( "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94", { @@ -176,6 +205,18 @@ describe("Wallet", () => { }, expectedMainUtxo: undefined, }, + { + testName: "old legacy transaction", + // Set the main UTXO hash based on the oldest transaction from walletLegacyTransactionHistory. + actualMainUtxo: { + transactionHash: Hex.from( + "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + expectedMainUtxo: undefined, + }, ] tests.forEach(({ testName, actualMainUtxo, expectedMainUtxo }) => { @@ -218,7 +259,7 @@ describe("Wallet", () => { ) transactionHistory.set( walletLegacyAddress, - walletLegacyTransactionsHistory + walletLegacyTransactionHistory ) bitcoinClient.transactionHistory = transactionHistory From c6f94f641dda72a2a8c155d50de2a902daaa24c6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 12:27:26 +0200 Subject: [PATCH 19/22] Rename fn that converts script to BTC address The `scriptPubKey` is a field specific to transaction outputs but basically, this function converts any script to a Bitcoin address. Here we make this function more generic and rename to `createAddressFromOutputScript`. --- typescript/src/bitcoin.ts | 2 +- typescript/test/bitcoin.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index ceffb0fca..4abc66599 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -633,7 +633,7 @@ export function createOutputScriptFromAddress(address: string): Hex { * @param network Bitcoin network. * @returns The Bitcoin address. */ -export function getAddressFromScriptPubKey( +export function createAddressFromOutputScript( scriptPubKey: string, network: BitcoinNetwork = BitcoinNetwork.Mainnet ): string { diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index b581fb78e..ce525b06d 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -12,7 +12,7 @@ import { bitsToTarget, targetToDifficulty, createOutputScriptFromAddress, - getAddressFromScriptPubKey, + createAddressFromOutputScript, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" @@ -493,7 +493,7 @@ describe("Bitcoin", () => { btcAddresses[bitcoinNetwork as keyof typeof btcAddresses] ).forEach(([addressType, { address, scriptPubKey }]) => { it(`should return correct ${addressType} address`, () => { - const result = getAddressFromScriptPubKey( + const result = createAddressFromOutputScript( scriptPubKey, bitcoinNetwork === "mainnet" ? BitcoinNetwork.Mainnet From 1c9b91b210eb22733a35e5549f4a735f8da2fcf3 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 12:36:12 +0200 Subject: [PATCH 20/22] Update `createAddressFromOutputScript` fn param Rename `scriptPubKey` to `script` and update docs. The `scriptPubKey` is a field specific to transaction outputs but basically, this function converts any script to a Bitcoin address. Here we also update type of this param to `Hex` instead of `string`. --- typescript/src/bitcoin.ts | 10 ++++------ typescript/test/bitcoin.test.ts | 2 +- typescript/test/data/bitcoin.ts | 29 ++++++++++++++++++----------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 4abc66599..3d7c32585 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -626,18 +626,16 @@ export function createOutputScriptFromAddress(address: string): Hex { } /** - * Returns the Bitcoin address based on the script pub key placed on the output - * of a Bitcoin transaction. - * @param scriptPubKey Scirpt pub key placed on the output of a Bitcoin - * transaction. + * Creates the Bitcoin address from the output script. + * @param script The unprefixed and not prepended with length output script. * @param network Bitcoin network. * @returns The Bitcoin address. */ export function createAddressFromOutputScript( - scriptPubKey: string, + script: Hex, network: BitcoinNetwork = BitcoinNetwork.Mainnet ): string { - return Script.fromRaw(scriptPubKey.toString(), "hex") + return Script.fromRaw(script.toString(), "hex") .getAddress() ?.toString(toBcoinNetwork(network)) } diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index ce525b06d..27ce2861f 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -478,7 +478,7 @@ describe("Bitcoin", () => { it(`should create correct output script for ${addressType} address type`, () => { const result = createOutputScriptFromAddress(address) - expect(result.toString()).to.eq(expectedOutputScript) + expect(result.toString()).to.eq(expectedOutputScript.toString()) }) } ) diff --git a/typescript/test/data/bitcoin.ts b/typescript/test/data/bitcoin.ts index bc266e1a0..d44b4b737 100644 --- a/typescript/test/data/bitcoin.ts +++ b/typescript/test/data/bitcoin.ts @@ -1,4 +1,5 @@ import { BitcoinNetwork } from "../../src/bitcoin-network" +import { Hex } from "../../src/hex" export const btcAddresses: Record< Exclude, @@ -6,7 +7,7 @@ export const btcAddresses: Record< [addressType: string]: { address: string redeemerOutputScript: string - scriptPubKey: string + scriptPubKey: Hex } } > = { @@ -15,25 +16,28 @@ export const btcAddresses: Record< address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", redeemerOutputScript: "0x1976a9142cd680318747b720d67bf4246eb7403b476adb3488ac", - scriptPubKey: "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + scriptPubKey: Hex.from( + "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac" + ), }, P2WPKH: { address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", redeemerOutputScript: "0x160014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", - scriptPubKey: "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + scriptPubKey: Hex.from("0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), }, P2SH: { address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", redeemerOutputScript: "0x17a914011beb6fb8499e075a57027fb0a58384f2d3f78487", - scriptPubKey: "a914011beb6fb8499e075a57027fb0a58384f2d3f78487", + scriptPubKey: Hex.from("a914011beb6fb8499e075a57027fb0a58384f2d3f78487"), }, P2WSH: { address: "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", redeemerOutputScript: "0x220020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", - scriptPubKey: - "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + scriptPubKey: Hex.from( + "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c" + ), }, }, mainnet: { @@ -41,25 +45,28 @@ export const btcAddresses: Record< address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", redeemerOutputScript: "0x1976a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", - scriptPubKey: "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + scriptPubKey: Hex.from( + "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac" + ), }, P2WPKH: { address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", redeemerOutputScript: "0x1600148d7a0a3461e3891723e5fdf8129caa0075060cff", - scriptPubKey: "00148d7a0a3461e3891723e5fdf8129caa0075060cff", + scriptPubKey: Hex.from("00148d7a0a3461e3891723e5fdf8129caa0075060cff"), }, P2SH: { address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", redeemerOutputScript: "0x17a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", - scriptPubKey: "a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87", + scriptPubKey: Hex.from("a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87"), }, P2WSH: { address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", redeemerOutputScript: "0x220020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", - scriptPubKey: - "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + scriptPubKey: Hex.from( + "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70" + ), }, }, } From 5b786c204cc5f0551dd89c549db973ea625ce405 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 14:39:07 +0200 Subject: [PATCH 21/22] Improve errors in `findWalletForRedemption` fn Throw error when currently, there are no active wallets in the network and when the user requested redemption for all active wallets in the network using the same Bitcoin address - in that case, a user should use another Bitcoin address. --- typescript/src/redemption.ts | 14 ++++++++ typescript/test/redemption.test.ts | 57 +++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index f8a0665cf..e1a631101 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -441,6 +441,7 @@ export async function findWalletForRedemption( } | undefined = undefined let maxAmount = BigNumber.from(0) + let activeWalletsCounter = 0 for (const wallet of wallets) { const { walletPublicKeyHash } = wallet @@ -456,6 +457,7 @@ export async function findWalletForRedemption( ) continue } + activeWalletsCounter++ // Wallet must have a main UTXO that can be determined. const mainUtxo = await determineWalletMainUtxo( @@ -511,6 +513,18 @@ export async function findWalletForRedemption( ) } + if (activeWalletsCounter === 0) { + throw new Error("Currently, there are no active wallets in the network.") + } + + // Cover a corner case when the user requested redemption for all active + // wallets in the network using the same Bitcoin address. + if (!walletData && activeWalletsCounter > 0 && maxAmount.eq(0)) { + throw new Error( + "All active wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." + ) + } + if (!walletData) throw new Error( `Could not find a wallet with enough funds. Maximum redemption amount is ${maxAmount} Satoshi.` diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index e2efa36d1..21af6263d 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1468,7 +1468,7 @@ describe("Redemption", () => { bitcoinClient ) ).to.be.rejectedWith( - "Could not find a wallet with enough funds. Maximum redemption amount is 0 Satoshi." + "Currently, there are no active wallets in the network." ) }) } @@ -1676,6 +1676,61 @@ describe("Redemption", () => { }) } ) + + context( + "when all active wallets has pending redemption for a given Bitcoin address", + () => { + const amount: BigNumber = BigNumber.from("1000000") // 0.01 BTC + const redeemerOutputScript = + findWalletForRedemptionData.pendingRedemption.redeemerOutputScript + + beforeEach(async () => { + const walletPublicKeyHash = + findWalletForRedemptionData.walletWithPendingRedemption.event + .walletPublicKeyHash + + const pendingRedemptions = new Map< + BigNumberish, + RedemptionRequest + >() + + const pendingRedemption1 = MockBridge.buildRedemptionKey( + walletPublicKeyHash.toString(), + redeemerOutputScript + ) + + const pendingRedemption2 = MockBridge.buildRedemptionKey( + findWalletForRedemptionData.liveWallet.event.walletPublicKeyHash.toString(), + redeemerOutputScript + ) + + pendingRedemptions.set( + pendingRedemption1, + findWalletForRedemptionData.pendingRedemption + ) + + pendingRedemptions.set( + pendingRedemption2, + findWalletForRedemptionData.pendingRedemption + ) + bridge.setPendingRedemptions(pendingRedemptions) + }) + + it("should throw an error", async () => { + await expect( + findWalletForRedemption( + amount, + redeemerOutputScript, + BitcoinNetwork.Testnet, + bridge, + bitcoinClient + ) + ).to.be.rejectedWith( + "All active wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." + ) + }) + } + ) }) }) }) From 19e2dfff19b8bed26b98fe9e94a302ab1f183470 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 12 Jul 2023 15:08:43 +0200 Subject: [PATCH 22/22] Adjust a nomenclature in `findWalletForRedemption` There is always only one "active" wallet in the system. This is the one that accepts new deposits. What we are talking about here are "live" wallets that are operable from the Bridge's standpoint. Here we rename the `activeWalletsCounter` variable to `liveWalletsCounter` and adjust the nomenclature everywherre accordingly. --- typescript/src/redemption.ts | 25 +++++++++++++------------ typescript/test/redemption.test.ts | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index e1a631101..f3b2474a0 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -412,11 +412,12 @@ export async function getRedemptionRequest( } /** - * Finds the oldest active wallet that has enough BTC to handle a redemption request. + * Finds the oldest live wallet that has enough BTC to handle a redemption + * request. * @param amount The amount to be redeemed in satoshis. - * @param redeemerOutputScript The redeemer output script the redeemed funds - * are supposed to be locked on. Must be un-prefixed and not prepended - * with length. + * @param redeemerOutputScript The redeemer output script the redeemed funds are + * supposed to be locked on. Must be un-prefixed and not prepended with + * length. * @param bitcoinNetwork Bitcoin network. * @param bridge The handle to the Bridge on-chain contract. * @param bitcoinClient Bitcoin client used to interact with the network. @@ -441,7 +442,7 @@ export async function findWalletForRedemption( } | undefined = undefined let maxAmount = BigNumber.from(0) - let activeWalletsCounter = 0 + let liveWalletsCounter = 0 for (const wallet of wallets) { const { walletPublicKeyHash } = wallet @@ -457,7 +458,7 @@ export async function findWalletForRedemption( ) continue } - activeWalletsCounter++ + liveWalletsCounter++ // Wallet must have a main UTXO that can be determined. const mainUtxo = await determineWalletMainUtxo( @@ -513,15 +514,15 @@ export async function findWalletForRedemption( ) } - if (activeWalletsCounter === 0) { - throw new Error("Currently, there are no active wallets in the network.") + if (liveWalletsCounter === 0) { + throw new Error("Currently, there are no live wallets in the network.") } - // Cover a corner case when the user requested redemption for all active - // wallets in the network using the same Bitcoin address. - if (!walletData && activeWalletsCounter > 0 && maxAmount.eq(0)) { + // Cover a corner case when the user requested redemption for all live wallets + // in the network using the same Bitcoin address. + if (!walletData && liveWalletsCounter > 0 && maxAmount.eq(0)) { throw new Error( - "All active wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." + "All live wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." ) } diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 21af6263d..58bac10cc 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -1468,7 +1468,7 @@ describe("Redemption", () => { bitcoinClient ) ).to.be.rejectedWith( - "Currently, there are no active wallets in the network." + "Currently, there are no live wallets in the network." ) }) } @@ -1726,7 +1726,7 @@ describe("Redemption", () => { bitcoinClient ) ).to.be.rejectedWith( - "All active wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." + "All live wallets in the network have the pending redemption for a given Bitcoin address. Please use another Bitcoin address." ) }) }