diff --git a/.typos.toml b/.typos.toml index 38ee447461..e30e9e6bd4 100644 --- a/.typos.toml +++ b/.typos.toml @@ -15,6 +15,8 @@ extend-ignore-re = [ "ser.*", "prefix.*", "value: .*", + "pqNTRUsign", + "Strnad", ] [default.extend-words] diff --git a/README.mediawiki b/README.mediawiki index ed4e1dd24a..55580ee0aa 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1,5 +1,5 @@ -People wishing to submit a BIP should first describe their idea to the [https://groups.google.com/g/bitcoindev -bitcoindev@googlegroups.com] mailing list to gather feedback on viability and community interest before working on a +People wishing to submit a BIP should first describe their idea to the [https://groups.google.com/g/bitcoindev bitcoindev@googlegroups.com] +mailing list to gather feedback on viability and community interest before working on a formal description. Please open a pull request to this repository only when substantial progress on the draft has been made, preferably when the draft is nearing completion. Authors do not assign a number to their own proposal. After a proposal meets the editorial criteria, a BIP Editor will assign a number to it and publish the proposal by @@ -512,6 +512,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Dmitry Petukhov | Informational | Complete +|- +| [[bip-0089.mediawiki|89]] +| Applications +| Chain Code Delegation +| Jesse Posner, Jurvis Tan +| Specification +| Draft |- style="background-color: #cfffcf" | [[bip-0090.mediawiki|90]] | @@ -617,6 +624,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Gavin Andresen | Specification | Closed +|- +| [[bip-0110.mediawiki|110]] +| Consensus (soft fork) +| Reduced Data Temporary Softfork +| Dathon Ohm +| Specification +| Draft |- style="background-color: #cfffcf" | [[bip-0111.mediawiki|111]] | Peer Services @@ -736,6 +750,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Steven Roose | Specification | Draft +|- +| [[bip-0128.mediawiki|128]] +| Applications +| Timelock-Recovery Storage Format +| Oren Z +| Specification +| Draft |- style="background-color: #ffffcf" | [[bip-0129.mediawiki|129]] | Applications @@ -1171,12 +1192,19 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Specification | Closed |- +| [[bip-0346.md|346]] +| Consensus (soft fork) +| OP_TXHASH +| Steven Roose, Brandon Black +| Specification +| Draft +|- style="background-color: #ffffcf" | [[bip-0347.mediawiki|347]] | Consensus (soft fork) | OP_CAT in Tapscript | Ethan Heilman, Armin Sabouri | Specification -| Draft +| Complete |- | [[bip-0348.md|348]] | Consensus (soft fork) @@ -1209,7 +1237,7 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | [[bip-0352.mediawiki|352]] | Applications | Silent Payments -| josibake, Ruben Somsen +| josibake, Ruben Somsen, Sebastian Falbesoner | Specification | Complete |- style="background-color: #ffffcf" @@ -1219,6 +1247,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Matt Corallo, Bastien Teinturier | Specification | Complete +|- +| [[bip-0360.mediawiki|360]] +| Consensus (soft fork) +| Pay-to-Merkle-Root (P2MR) +| Hunter Beast, Ethan Heilman, Isabel Foxen Duke +| Specification +| Draft |- style="background-color: #cfffcf" | [[bip-0370.mediawiki|370]] | Applications @@ -1346,6 +1381,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Informational | Draft |- +| [[bip-0392.mediawiki|392]] +| Applications +| Silent Payment Output Script Descriptors +| Craig Raw +| Specification +| Draft +|- | [[bip-0431.mediawiki|431]] | Applications | Topology Restrictions for Pinning @@ -1360,6 +1402,20 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Informational | Draft |- +| [[bip-0434.md|434]] +| Peer Services +| Peer Feature Negotiation +| Anthony Towns +| Specification +| Draft +|- +| [[bip-0442.md|442]] +| Consensus (soft fork) +| OP_PAIRCOMMIT +| moonsettler, Brandon Black +| Specification +| Draft +|- | [[bip-0443.mediawiki|443]] | Consensus (soft fork) | OP_CHECKCONTRACTVERIFY diff --git a/bip-0020.mediawiki b/bip-0020.mediawiki index 93594d15df..a0d6e34b1e 100644 --- a/bip-0020.mediawiki +++ b/bip-0020.mediawiki @@ -7,6 +7,7 @@ Type: Specification Assigned: 2011-01-10 License: BSD-2-Clause + Proposed-Replacement: 21 BIP 0020 is based off an earlier document by Nils Schneider. '''And has been replaced by BIP 0021''' diff --git a/bip-0021.mediawiki b/bip-0021.mediawiki index 5f2756a17d..f97fd24348 100644 --- a/bip-0021.mediawiki +++ b/bip-0021.mediawiki @@ -7,6 +7,7 @@ Status: Closed Type: Specification Assigned: 2012-01-29 + Replaces: 20 Proposed-Replacement: 321 diff --git a/bip-0032.mediawiki b/bip-0032.mediawiki index dc7689d00a..558bbaab60 100644 --- a/bip-0032.mediawiki +++ b/bip-0032.mediawiki @@ -50,10 +50,10 @@ Addition (+) of two coordinate pair is defined as application of the EC group op Concatenation (||) is the operation of appending one byte sequence onto another. As standard conversion functions, we assume: -* point(p): returns the coordinate pair resulting from EC point multiplication (repeated application of the EC group operation) of the secp256k1 base point with the integer p. +* point(p): returns the coordinate pair resulting from EC point multiplication (repeated application of the EC group operation) of the secp256k1 base point with the integer p (i.e., the operation used to compute a public key from a private key). * ser32(i): serialize a 32-bit unsigned integer i as a 4-byte sequence, most significant byte first. * ser256(p): serializes the integer p as a 32-byte sequence, most significant byte first. -* serP(P): serializes the coordinate pair P = (x,y) as a byte sequence using SEC1's compressed form: (0x02 or 0x03) || ser256(x), where the header byte depends on the parity of the omitted y coordinate. +* serP(P): serializes the coordinate pair P = (x,y) (i.e., the public key) as a byte sequence using [https://www.secg.org/sec1-v2.pdf SEC1]'s compressed form: (0x02 or 0x03) || ser256(x), where the header byte depends on the parity of the omitted y coordinate. * parse256(p): interprets a 32-byte sequence as a 256-bit number, most significant byte first. diff --git a/bip-0044.mediawiki b/bip-0044.mediawiki index bbf9d1f04a..a94efba2ec 100644 --- a/bip-0044.mediawiki +++ b/bip-0044.mediawiki @@ -9,6 +9,7 @@ Status: Deployed Type: Specification Assigned: 2014-04-24 + Requires: 32, 43 ==Abstract== diff --git a/bip-0069/bip-0069_examples.py b/bip-0069/bip-0069_examples.py old mode 100644 new mode 100755 index be103a727c..d18a8f9ca9 --- a/bip-0069/bip-0069_examples.py +++ b/bip-0069/bip-0069_examples.py @@ -1,4 +1,6 @@ +#!/usr/bin/env python3 import binascii +from functools import cmp_to_key #returns -1 if barr1 is less, 1 if barr1 is greater, and 0 if equal def bytearr_cmp(barr1, barr2): @@ -32,9 +34,10 @@ def input_cmp(input_tuple1, input_tuple2): raise ValueError('Matching previous transaction hash and previous transaction output index for two distinct inputs. Invalid!') def sort_inputs(input_tuples): - return sorted(input_tuples, cmp=input_cmp) + return sorted(input_tuples, key=cmp_to_key(input_cmp)) def print_inputs(ordered_input_tuples): + print("inputs") index = 0 for prev_tx_hash_byte_arr_little_endian, prev_tx_output_index in ordered_input_tuples: prev_tx_hash_hex = binascii.hexlify(bytearray(prev_tx_hash_byte_arr_little_endian)) @@ -52,9 +55,10 @@ def output_cmp(output_tuple1, output_tuple2): return bytearr_cmp(output_tuple1[1], output_tuple2[1]) def sort_outputs(output_tuples): - return sorted(output_tuples, cmp=output_cmp) + return sorted(output_tuples, key=cmp_to_key(output_cmp)) def print_outputs(ordered_output_tuples): + print("outputs") index = 0 for amount, scriptPubKey_byte_arr in ordered_output_tuples: scriptPubKey_hex = binascii.hexlify(bytearray(scriptPubKey_byte_arr)) @@ -82,6 +86,7 @@ def main(): ([0x7d, 0x03, 0x7c, 0xeb, 0x2e, 0xe0, 0xdc, 0x03, 0xe8, 0x2f, 0x17, 0xbe, 0x79, 0x35, 0xd2, 0x38, 0xb3, 0x5d, 0x1d, 0xea, 0xbf, 0x95, 0x3a, 0x89, 0x2a, 0x45, 0x07, 0xbf, 0xbe, 0xeb, 0x3b, 0xa4], 1), ([0x6c, 0x1d, 0x56, 0xf3, 0x1b, 0x2d, 0xe4, 0xbf, 0xc6, 0xaa, 0xea, 0x28, 0x39, 0x6b, 0x33, 0x31, 0x02, 0xb1, 0xf6, 0x00, 0xda, 0x9c, 0x6d, 0x61, 0x49, 0xe9, 0x6c, 0xa4, 0x3f, 0x11, 0x02, 0xb1], 1), ([0xb4, 0x11, 0x2b, 0x8f, 0x90, 0x0a, 0x7c, 0xa0, 0xc8, 0xb0, 0xe7, 0xc4, 0xdf, 0xad, 0x35, 0xc6, 0xbe, 0x5f, 0x6b, 0xe4, 0x6b, 0x34, 0x58, 0x97, 0x49, 0x88, 0xe1, 0xcd, 0xb2, 0xfa, 0x61, 0xb8], 0)] + print("\ntx 0a6a357e2f7796444e02638749d9611c008b253fb55f5dc88b739b230ed0c4c3") tx_0a6a_sorted_input_tuples = sort_inputs(tx_0a6a_input_tuples) print_inputs(tx_0a6a_sorted_input_tuples) @@ -94,10 +99,11 @@ def main(): #reference data: https://blockchain.info/rawtx/28204cad1d7fc1d199e8ef4fa22f182de6258a3eaafe1bbe56ebdcacd3069a5f thanks @quantabytes! tx_2820_input_tuples = [ - # (prev_tx_hash, prev_tx_output_index) - ("35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", 0), - ("35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", 1)] #duplicate prev_tx_hash + # (prev_tx_hash_byte_arr_little_endian, prev_tx_output_index) + ([0x55, 0x60, 0x5d, 0xc6, 0x5f, 0x3c, 0xcc, 0x4d, 0xb1, 0xd8, 0x56, 0x4a, 0x28, 0x04, 0x2b, 0xdb, 0x2c, 0x2b, 0xe3, 0x85, 0xea, 0xb2, 0xeb, 0xea, 0x41, 0x19, 0xee, 0x9c, 0x26, 0x8d, 0x28, 0x35], 0), + ([0x55, 0x60, 0x5d, 0xc6, 0x5f, 0x3c, 0xcc, 0x4d, 0xb1, 0xd8, 0x56, 0x4a, 0x28, 0x04, 0x2b, 0xdb, 0x2c, 0x2b, 0xe3, 0x85, 0xea, 0xb2, 0xeb, 0xea, 0x41, 0x19, 0xee, 0x9c, 0x26, 0x8d, 0x28, 0x35], 1)] #duplicate prev_tx_hash + print("\ntx 28204cad1d7fc1d199e8ef4fa22f182de6258a3eaafe1bbe56ebdcacd3069a5f") tx_2820_sorted_input_tuples = sort_inputs(tx_2820_input_tuples) print_inputs(tx_2820_sorted_input_tuples) @@ -106,7 +112,7 @@ def main(): (100000000, [0x41, 0x04, 0x6a, 0x07, 0x65, 0xb5, 0x86, 0x56, 0x41, 0xce, 0x08, 0xdd, 0x39, 0x69, 0x0a, 0xad, 0xe2, 0x6d, 0xfb, 0xf5, 0x51, 0x14, 0x30, 0xca, 0x42, 0x8a, 0x30, 0x89, 0x26, 0x13, 0x61, 0xce, 0xf1, 0x70, 0xe3, 0x92, 0x9a, 0x68, 0xae, 0xe3, 0xd8, 0xd4, 0x84, 0x8b, 0x0c, 0x51, 0x11, 0xb0, 0xa3, 0x7b, 0x82, 0xb8, 0x6a, 0xd5, 0x59, 0xfd, 0x2a, 0x74, 0x5b, 0x44, 0xd8, 0xe8, 0xd9, 0xdf, 0xdc, 0x0c, 0xac]), (2400000000, [0x41, 0x04, 0x4a, 0x65, 0x6f, 0x06, 0x58, 0x71, 0xa3, 0x53, 0xf2, 0x16, 0xca, 0x26, 0xce, 0xf8, 0xdd, 0xe2, 0xf0, 0x3e, 0x8c, 0x16, 0x20, 0x2d, 0x2e, 0x8a, 0xd7, 0x69, 0xf0, 0x20, 0x32, 0xcb, 0x86, 0xa5, 0xeb, 0x5e, 0x56, 0x84, 0x2e, 0x92, 0xe1, 0x91, 0x41, 0xd6, 0x0a, 0x01, 0x92, 0x8f, 0x8d, 0xd2, 0xc8, 0x75, 0xa3, 0x90, 0xf6, 0x7c, 0x1f, 0x6c, 0x94, 0xcf, 0xc6, 0x17, 0xc0, 0xea, 0x45, 0xaf, 0xac])] tx_2820_sorted_output_tuples = sort_outputs(tx_2820_output_tuples) - print_outputs(tx_2820_output_tuples) + print_outputs(tx_2820_sorted_output_tuples) if __name__ == "__main__": main() diff --git a/bip-0085.mediawiki b/bip-0085.mediawiki index 73ea040ad4..b0f00abbef 100644 --- a/bip-0085.mediawiki +++ b/bip-0085.mediawiki @@ -200,7 +200,7 @@ OUTPUT: ====18 English words==== BIP39 English 18 word mnemonic seed -196 bits of entropy as input to BIP39 to derive 18 word mnemonic +192 bits of entropy as input to BIP39 to derive 18 word mnemonic INPUT: * MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb @@ -443,7 +443,7 @@ BIP32, BIP39 ===2.0.0 (2025-09-19)=== -====Fixed==== +====Fixed==== * Fixed the human-readable datetime string for BIP85 GPG Keys that was incorrectly stated as '2009-01-03 18:05:05' rather than '2009-01-03 18:15:05'. Implementations that relied on the previously incorrect datetime string instead of UNIX Epoch timestamp 1231006505 will produce different key fingerprints. diff --git a/bip-0089.mediawiki b/bip-0089.mediawiki new file mode 100644 index 0000000000..b5ef080d21 --- /dev/null +++ b/bip-0089.mediawiki @@ -0,0 +1,405 @@ +
+  BIP: 89
+  Layer: Applications
+  Title: Chain Code Delegation
+  Authors: Jesse Posner 
+           Jurvis Tan 
+  Comments-Summary: No comments yet.
+  Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0089
+  Status: Draft
+  Type: Specification
+  Assigned: 2025-12-03
+  License: BSD-3-Clause
+  Discussion: https://delvingbitcoin.org/t/chain-code-delegation-private-access-control-for-bitcoin-keys/1837
+  Requires: 32, 340, 341
+
+ +== Abstract == +Chain Code Delegation (CCD) is a method for multi-signature wallets in which a privileged participant withholds BIP32 chain codes from one or more non-privileged participants, and supplies per-input scalar tweaks at signing time. This allows non-privileged participants to co-sign transactions without learning wallet-wide derivations, balances, or signing activity from other spending combinations. CCD defines the tweak exchange needed for verification and signing behavior when the signer does not possess a chain code. + +== Motivation == +In multisig deployments, sharing extended public keys (xpubs) or descriptors enables all participants to scan the chain and infer counterparties' activity. CCD limits that visibility by ensuring non-privileged participants only ever hold a non-extended keypair and only receive the minimum per-spend data needed to sign. The procedure keeps policy enforcement feasible for the non-privileged signer while preserving balance privacy, which is particularly useful in collaborative custody arrangements where the wallet owner wants balance privacy from their custodian. + +== Terminology == +In CCD, the chain code is the object of delegation—not signing authority. A participant who gives up their chain code delegates it to another. + +* A "Delegator" is a participant who delegates their chain code to another party. They hold only a non-extended keypair and receive scalar tweaks from the delegatee when asked to sign. +* A "Delegatee" is a participant who receives and retains a delegated chain code for another participant's public key, and computes derivation tweaks for that participant. +* A "Participant" is any key holder that can co-sign for UTXOs in the wallet (including delegators and delegatees). +* A "Non-hardened derivation" is a BIP32 child derivation where index < 2^31. + +== Overview == +CCD operates by having Delegatees deprive Delegators of BIP32 chain codes during setup and later conveying the aggregated scalar tweak computed as the sum of non-hardened derivation tweaks along the remaining path to the child key used by a given input or change output. A Delegator uses the tweak to compute the child keys for verification and signing without being able to derive or recognize keys for other paths. + +== Specification == + +=== Key material and setup === +* '''Delegator key:''' Each delegator generates a standard (non-extended) secp256k1 keypair and provides the public key to the counterparties. A delegator MUST NOT retain or be provided a chain code for this key. +* '''Delegated chain code:''' A designated delegatee computes and retains a BIP32 chain code bound to the delegator's public key, forming an xpub that MUST NOT be disclosed to the delegator. The delegatee MAY share this xpub with other delegatees. +* '''Other participants:''' Non-delegator participants use conventional extended keys and share the public half as appropriate for the wallet descriptor. +* '''Derivation constraints:''' The delegatee holds an extended public key for the delegator. All derivation from this extended key MUST be non-hardened, as hardened derivation requires the private key, which the delegatee does not possess. + +=== Notation === +The following conventions are used, with constants as defined for [https://www.secg.org/sec2-v2.pdf secp256k1]. We note that adapting this proposal to other elliptic curves is not straightforward and can result in an insecure scheme. +* Lowercase variables represent integers or byte arrays. +** The constant ''p'' refers to the field size, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F''. +** The constant ''n'' refers to the curve order, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141''. +* Uppercase variables refer to points on the curve with equation ''y2 = x3 + 7'' over the integers modulo ''p''. +** ''is_infinite(P)'' returns whether ''P'' is the point at infinity. +** ''x(P)'' and ''y(P)'' are integers in the range ''0..p-1'' and refer to the X and Y coordinates of a point ''P'' (assuming it is not infinity). +** The constant ''G'' refers to the base point, for which ''x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'' and ''y(G) = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8''. +** Addition of points refers to the usual [https://en.wikipedia.org/wiki/Elliptic_curve#The_group_law elliptic curve group operation]. +** [https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication Multiplication (⋅) of an integer and a point] refers to the repeated application of the group operation. +* Functions and operations: +** ''||'' refers to byte array concatenation. +** The function ''x[i:j]'', where ''x'' is a byte array and ''i, j ≥ 0'', returns a ''(j - i)''-byte array with a copy of the ''i''-th byte (inclusive) to the ''j''-th byte (exclusive) of ''x''. +** The function ''bytes(n, x)'', where ''x'' is an integer, returns the n-byte encoding of ''x'', most significant byte first. +** The constant ''empty_bytestring'' refers to the empty byte array. It holds that ''len(empty_bytestring) = 0''. +** The function ''xbytes(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''bytes(32, x(P))''. +** The function ''len(x)'' where ''x'' is a byte array returns the length of the array. +** The function ''has_even_y(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''y(P) mod 2 == 0''. +** The function ''with_even_y(P)'', where ''P'' is a point, returns ''P'' if ''is_infinite(P)'' or ''has_even_y(P)''. Otherwise, ''with_even_y(P)'' returns ''-P''. +** The function ''cbytes(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''a || xbytes(P)'' where ''a'' is a byte that is ''2'' if ''has_even_y(P)'' and ''3'' otherwise. +** The function ''int(x)'', where ''x'' is a 32-byte array, returns the 256-bit unsigned integer whose most significant byte first encoding is ''x''. +** The function ''lift_x(x)'', where ''x'' is an integer in range ''0..2256-1'', returns the point ''P'' for which ''x(P) = x'' + Given a candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. The valid Y coordinates for a given candidate ''x'' are the square roots of ''c = x3 + 7 mod p'' and they can be computed as ''y = ±c(p+1)/4 mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. and ''has_even_y(P)'', or fails if ''x'' is greater than ''p-1'' or no such point exists. The function ''lift_x(x)'' is equivalent to the following pseudocode: +*** Fail if ''x > p-1''. +*** Let ''c = x3 + 7 mod p''. +*** Let ''y' = c(p+1)/4 mod p''. +*** Fail if ''c ≠ y'2 mod p''. +*** Let ''y = y' '' if ''y' mod 2 = 0'', otherwise let ''y = p - y' ''. +*** Return the unique point ''P'' such that ''x(P) = x'' and ''y(P) = y''. +** The function ''cpoint(x)'', where ''x'' is a 33-byte array (compressed serialization), sets ''P = lift_x(int(x[1:33]))'' and fails if that fails. If ''x[0] = 2'' it returns ''P'' and if ''x[0] = 3'' it returns ''-P''. Otherwise, it fails. +** The function ''hash256tag(x)'' where ''tag'' is a UTF-8 encoded tag name and ''x'' is a byte array returns the 32-byte hash ''SHA256(SHA256(tag) || SHA256(tag) || x)''. +** The function ''hash512tag(x)'' where ''tag'' is a UTF-8 encoded tag name and ''x'' is a byte array returns the 64-byte hash ''SHA512(SHA512(tag) || SHA512(tag) || x)''. +* Other: +** Tuples are written by listing the elements within parentheses and separated by commas. For example, ''(2, 3, 1)'' is a tuple. + +=== Tweak Calculation === +To produce CCD tweak data, a delegatee computes a per-participant scalar that aggregates the non-hardened derivation tweaks along the remaining path. Let the extended key retained by the delegatee be P at depth d, and let the target index vector be I = (id+1, …, in) with each ik < 231. + +
+Algorithm ''ComputeBIP32Tweak(P, I)'': +* Inputs: +** ''P'': base public key at depth ''d'' +** ''I = (id+1, …, in)'': ordered sequence of non-hardened child indices +* Let ''t = 0'' and ''E = P''. +* For each index ''i'' in ''I'' (from left to right): +** Run the BIP32 non-hardened derivation ''CKDpub'' on ''E'' with child index ''i'', yielding the child extended key ''Pchild'' and its scalar tweak ''δ'' (the parse256(''IL'') term from BIP32). +** Let ''t = (t + δ) mod n''. +** Let ''E = Pchild''. +* If ''I'' is empty, let ''P′ = P''; otherwise let ''P′ = Pchild'' from the final iteration. +* Return ''(t, P′)''. +
+ +Any attempt to apply a hardened derivation (index ≥ 231) MUST fail. Delegatees MAY discard P′ after extracting t if it is not otherwise required. + +=== Delegation Bundle === +CCD requires the delegatee to provide per-participant tweaks for inputs and (optionally) change outputs. Tweaks for change outputs are only required if a delegator wants to be able to compute the amount of bitcoin they are spending. + +A delegatee MUST provide each delegator with, for every signing context, a collection of tuples (Pi, ti) where Pi is the participant's base public key disclosed to the delegator and ti is the aggregated tweak returned by ''ComputeBIP32Tweak''. The scalar ti MUST be encoded as a 32-byte big-endian integer. + +The transport that carries this bundle is out of scope for this proposal; implementers MAY use PSBT proprietary keys, RPC payloads, or bespoke messages as long as the delegator can authenticate the origin of the data. Delegatees SHOULD attach the witness script (or sufficient script template information) built with the tweaked keys when the delegator is expected to verify the input or enforce spending policy on change outputs. + +Delegators use the supplied CCD tweak bundle during verification (see ''Delegator input and change verification'') and signature generation (see ''DelegatorSign''). The message to be signed is provided separately as part of the standard signing protocol and is not part of the CCD-specific bundle. + +=== Signing Modes === +This BIP supports two modes: +* '''Non‑blinded.''' The delegator receives the tweak for the child public key and the message. The delegator learns only about the specific child keys and transactions it signs for; it does not learn the wider address space. +* '''Blinded.''' The delegator receives only a blinded challenge and parity bits. The delegator learns nothing about the message or child key for which it produces a signature. + +Both modes produce valid BIP340 signatures. + +====Non-Blinded Signing==== +For non-blinded signing, the delegator can produce signatures as usual using the tweaked key. + +=====Delegator input and change verification (Optional)===== +A delegator MAY validate the data it receives before producing signatures. + +For example, input verification reassures the delegator that every tweaked key they are asked to sign for corresponds to a wallet input they recognise. Change verification lets them establish the net outflow and enforce spending policy. + +Both checks rely on the same delegated tweak bundle described above. + +=====Input verification===== +For each input, the delegatee SHOULD disclose the descriptor template, the untweaked participant keys, the input witness script, and the per-participant tweaks. The delegator then applies the following procedure. + +
+Algorithm ''InputVerification(D, W, T)'': +* Inputs: +** ''D'': wallet policy or descriptor template expressed in terms of the untweaked participant keys ''Pi'' +** ''W'': witness script disclosed for the input under review +** ''T'': mapping from each ''Pi'' to a 32-byte big-endian tweak scalar ''ti'' +* For each participant key ''Pi'' referenced in ''D'': +** Retrieve ''ti'' from ''T''; fail if the entry is missing or malformed. +** If the verifier controls the corresponding private key ''di'', let ''d′i = (di + ti) mod n'' and ''P′i = d′i · G''; otherwise let ''P′i = Pi + ti · G''. +* Let ''D′'' be the descriptor formed by substituting every occurrence of ''Pi'' in ''D'' with ''P′i''. +* Derive the witness script ''W′'' from ''D′''. +* Return true if ''W′ = W'', otherwise false. +
+ +Successful verification of an input confirms that the delegator is signing for a script that belongs to the wallet and that the aggregate tweak values align with the expected policy. + +=====Change-output verification===== +When change outputs are disclosed, the delegator can perform an analogous check to ensure the destination script matches their policy template and to calculate outflows. Let D be the descriptor expressed in untweaked keys, W the provided witness script, and T the tweak mapping: + +
+Algorithm ''ChangeOutputVerification(D, W, T)'': +* Inputs: +** ''D'': wallet policy or descriptor template expressed in terms of the untweaked participant keys ''Pi'' +** ''W'': witness script disclosed for the change output +** ''T'': mapping from each ''Pi'' to a 32-byte big-endian tweak scalar ''ti'' +* For each participant key ''Pi'' referenced in ''T'': +** Retrieve ''ti'' from ''T''; fail if the entry is missing or malformed. +** If the verifier controls the corresponding private key ''di'', let ''d′i = (di + ti) mod n'' and ''P′i = d′i · G''; otherwise let ''P′i = Pi + ti · G''. +* Let ''D′'' be the descriptor formed by substituting every occurrence of ''Pi'' in ''D'' with ''P′i''. +* Derive the witness script ''W′'' from ''D′''. +* Return true if ''W′ = W'', otherwise false. +
+ +Successful verification ensures the change output commits to the tweaked participant keys implied by the CCD tweaks, preserving the intended policy. + +The delegator may perform additional application-specific verification on the transaction (e.g., recipient addresses, amounts, compliance checks) using the message ''m''. In the concurrently secure blinded mode, such policies can be enforced via zero-knowledge proofs that encode predicates about ''m''. Specification of such policies is outside the scope of this BIP. + +=====Delegator Signing===== +A delegator that holds only its base secret key x and public key P uses the delegated tweak bundle to derive per-input signing keys. The delegator MAY first call ''InputVerification'' and ''ChangeOutputVerification'' on any input and change output that provides a tweak in order to confirm outflow or policy requirements before signing. + +
+Algorithm ''DelegatorSign(t, x, m)'': +* Inputs: +** ''t'': aggregated tweak for the signing context (scalar mod ''n'') +** ''x'': delegator base secret key +** ''m'': message to be signed (for example, a transaction digest under the desired SIGHASH policy) +* Let ''x′ = (x + t) mod n''. +* Use secret key ''x′'' to produce the required signature ''σ'' under the indicated policy. +* Return ''σ''. +
+ +The delegatee is responsible for inserting ''σ'' into the surrounding protocol (e.g., a PSBT, transaction witness, or adaptor signature exchange). + +====Blinded Signing==== +The delegator learns neither the message, the challenge, or the public key used in the BIP340 signature, only a blinded challenge e'. + +This blind‑signing protocol specifies how a delegator can produce a blind partial Schnorr signature that a delegatee can unblind into a standard [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki BIP340] signature under a possibly tweaked X‑only public key. The notation, algorithmic patterns, and test‑vector style are adapted from [BIP‑327 (MuSig2)] and from the [https://github.com/siv2r/bip-frost-signing FROST Signing BIP]. The design follows the “plain” blind Schnorr flow described in Concurrently Secure Blind Schnorr Signatures ([https://eprint.iacr.org/2022/1676 ePrint 2022/1676]), but without the concurrency hardening from that work. + +The output signature is a BIP340 Schnorr signature valid under an X‑only key obtained by applying a sequence of plain (e.g. BIP32) and X‑only (e.g. Tapscript) tweaks to the signer’s plain public key. Consequently the protocol is compatible with [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341]. The delegator learns neither the message, the challenge, or the public key used in the BIP340 signature, only a blinded challenge e'. + +The plain protocol here is '''not''' concurrently secure. A signer '''MUST NOT''' run multiple blind signing sessions in parallel or interleave state across sessions. A signer '''MUST''' refuse any new blind‑nonce requests while a previous blind‑signature request is outstanding, or '''MUST''' irrevocably discard (and never reuse) any in‑flight blind nonce commitments that have not resulted in a signature, before accepting new ones. + +To obtain concurrency security as in ([https://eprint.iacr.org/2022/1676 ePrint 2022/1676]), the delegatee first sends an encryption of (m, a, b) before the signer commits to the blind nonce; later, the delegatee includes a zero‑knowledge proof binding the produced challenge to that encrypted tuple. That proof can additionally encode policy predicates about m (spend limits, velocity controls, etc.). A complete specification of this variant is outside the scope of this BIP. + +The following sections fully specify the non-concurrent blind signing protocol. + +===== Overview ===== + +* '''Round 1 (blind nonce).''' The delegator runs ''BlindNonceGen'' to produce ''blindsecnonce'' and ''blindpubnonce'' and sends ''blindpubnonce'' to the delegatee. +* '''Round 2 (challenge).''' The delegatee runs ''BlindChallengeGen'' using the message ''m'', ''blindpubnonce'', the base public key ''pk'', and a list of ordinary and X-only tweaks, to produce a ''session context'' (kept locally for unblinding), a ''blindchallenge'', and two booleans ''pk_parity'' and ''nonce_parity''. The delegatee sends ''blindchallenge'', ''pk_parity'', and ''nonce_parity'' to the signer. +* '''Round 3 (blind signature).''' The delegator runs ''BlindSign'' with ''sk'', ''blindchallenge'', ''blindsecnonce'', ''pk_parity'', and ''nonce_parity'' and returns ''blindsignature''. The delegatee completes by calling ''UnblindSignature'' with the stored session context and ''blindsignature'' to obtain the final BIP340 signature ''sig''. + +''BlindSign'' '''MUST NOT''' be executed twice with the same ''blindsecnonce''. As a defense, implementations '''SHOULD''' overwrite the first 64 bytes of ''blindsecnonce'' with zeros after they have been read by ''BlindSign''. + +=====Key Tweaking===== +======Tweak Context====== +The Tweak Context is a data structure consisting of the following elements: +* The point ''Q'' representing the potentially tweaked public key: an elliptic curve point +* The accumulated tweak ''tacc'': an integer with ''0 ≤ tacc < n'' +* The value ''gacc'' : 1 or -1 mod n + +We write "Let ''(Q, gacc, tacc) = tweak_ctx''" to assign names to the elements of a Tweak Context. + +
+Algorithm ''TweakCtxInit(pk)'': +* Input: +** The base public key pk: a 33-byte array +* Let ''Q = cpoint(pk)'' +* Fail if ''is_infinite(Q)'' +* Let ''gacc = 1'' +* Let ''tacc = 0'' +* Return ''tweak_ctx = (Q, gacc, tacc)'' +
+ +
+Algorithm ''ApplyTweak(tweak_ctx, tweak, is_xonly_t)'': +* Inputs: +** The ''tweak_ctx'': a [[#tweak-context|Tweak Context]] data structure +** The ''tweak'': a 32-byte array +** The tweak mode ''is_xonly_t'': a boolean +* Let ''(Q, gacc, tacc) = tweak_ctx'' +* If ''is_xonly_t'' and ''not has_even_y(Q)'': +** Let ''g = -1 mod n'' +* Else: +** Let ''g = 1'' +* Let ''t = int(tweak)''; fail if ''t ≥ n'' +* Let ''Q' = g⋅Q + t⋅G'' +** Fail if ''is_infinite(Q')'' +* Let ''gacc' = g⋅gacc mod n'' +* Let ''tacc' = t + g⋅tacc mod n'' +* Return ''tweak_ctx' = (Q', gacc', tacc')'' +
+ +=====Blind Nonce Generation===== + +
+Algorithm ''BlindNonceGen(sk, pk, aggpk, m, extra_in)'': +* Inputs: +** The base secret signing key ''sk'': a 32-byte array (optional argument) +** The base public key ''pk'': a 33-byte array (optional argument) +** The auxiliary input ''extra_in'': a byte array with ''0 ≤ len(extra_in) ≤ 232-1'' (optional argument) +* Let ''rand' '' be a 32-byte array freshly drawn uniformly at random +* If the optional argument ''sk'' is present: +** Let ''rand'' be the byte-wise xor of ''sk'' and ''hash256CCD/aux(rand')''The random data is hashed (with a unique tag) as a precaution against situations where the randomness may be correlated with the secret signing key itself. It is xored with the secret key (rather than combined with it in a hash) to reduce the number of operations exposed to the actual secret key. +* Else: +** Let ''rand = rand' '' +* If the optional argument ''extra_in'' is not present: +** Let ''extra_in = empty_bytestring'' +* Let ''k' = int(hash256CCD/blindnonce(rand || bytes(1, len(pk)) || pk || bytes(4, len(extra_in)) || extra_in )) mod n'' +* Fail if ''k' = 0'' +* Let ''R' = k'⋅G'' +* Let ''blindpubnonce = cbytes(R')'' +* Let ''blindsecnonce = bytes(32, k' || pk)''The algorithms as specified here assume that the ''blindsecnonce'' is stored as a 65-byte array using the serialization ''blindsecnonce = bytes(32, k') || pk''. The same format is used in the reference implementation and in the test vectors. However, since the ''blindsecnonce'' is not meant to be sent over the wire, compatibility between implementations is not a concern, and this method of storing the ''blindsecnonce'' is merely a suggestion.
+The ''blindsecnonce'' is effectively a local data structure of the signer which comprises the value double ''(k', pk)'', and implementations may choose any suitable method to carry it from ''BlindNonceGen'' (first communication round) to ''BlindSign'' (third communication round). In particular, implementations may choose to hide the ''blindsecnonce'' in internal state without exposing it in an API explicitly, e.g., in an effort to prevent callers from reusing a ''blindsecnonce'' accidentally.
+* Return ''(secnonce, pubnonce)'' +
+ +=====Session Context===== + +The Session Context is a data structure consisting of the following elements: +* The base public key ''pk'': a 33-byte array +* The blind factor ''blindfactor'': a 32-byte array +* The challenge hash ''challenge'': a 32-byte array +* The public nonce ''pubnonce'': a 33-byte array +* The number ''v'' of tweaks with ''0 ≤ v < 2^32'' +* The tweaks ''tweak1..v'': ''v'' 32-byte arrays +* The tweak modes ''is_xonly_t1..v'' : ''v'' booleans + +We write "Let ''(pk, blindfactor, challenge, pubnonce, v, tweak1..v, is_xonly_t1..v) = session_ctx''" to assign names to the elements of a Session Context. + +
+Algorithm ''GetSessionValues(session_ctx)'': +* Let ''(pk, blindfactor, challenge, pubnonce, v, tweak1..v, is_xonly_t1..v) = session_ctx'' +* Let ''tweak_ctx0 = TweakCtxInit(pk)''; fail if that fails +* For ''i = 1 .. v'': +** Let ''tweak_ctxi = ApplyTweak(tweak_ctxi-1, tweaki, is_xonly_ti)''; fail if that fails +* Let ''(Q, gacc, tacc) = tweak_ctxv'' +* Let ''a = int(blindfactor)''; fail if ''a ≥ n'' +* Let ''b = int(blindfactor)''; fail if ''b ≥ n'' +* Let ''e = int(challenge)''; fail if ''e ≥ n'' +* Let ''R = cpoint(pubnonce)''; fail if that fails +* Return ''(Q, gacc, tacc, a, e, R)'' +
+ +=====Blind Challenge Generation===== + +
+Algorithm ''BlindChallengeGen(m, blindpubnonce, pk, tweak1..v, is_xonly1..v, extra_in)'': +* Inputs: +** The message ''m'': a byte array +** The blind public nonce ''blindpubnonce'': a 33-byte array +** The base public key ''pk'': a 33-byte array +** The tweaks ''tweak1..v'': ''v'' 32-byte arrays +** The tweak modes ''is_xonly1..v'': ''v'' booleans +** The auxiliary input ''extra_in'': a byte array with ''0 ≤ len(extra_in) ≤ 232-1'' (optional argument) +* If ''extra_in'' is not present: +** Let ''extra_in = empty_bytestring'' +* Let ''(Q, gacc, tacc) = TweakCtxInit(pk)'' +* For ''i = 1 .. v'': +** Let ''(Q, gacc, tacc) = ApplyTweak((Q, gacc, tacc), tweaki, is_xonlyi)''; fail if that fails +* Let ''cpk = cbytes(Q)'' +* Draw 32 random bytes ''rand'' +* Let ''z = hash512CCD/blindfactor(rand || bytes(1, len(cpk)) || cpk || bytes(1, len(blindpubnonce)) || blindpubnonce || bytes(8, len(m)) || m || bytes(4, len(extra_in)) || extra_in)'' +* Let ''a' = int(z[0:32]) mod n''; fail if ''a' = 0'' +* Let ''b' = int(z[32:64]) mod n''; fail if ''b' = 0'' +* Let ''g = 1'' if ''has_even_y(Q)'', else ''g = −1 mod n'' +* Let ''pk_parity = (g⋅gacc mod n == 1)'' +* Let ''X' = cpoint(pk)''; let ''X = X' '' if ''pk_parity'' else ''−X' '' +* Let ''R' = cpoint(blindpubnonce)'' +* Let ''R = R' + a'⋅G + b'⋅X''; fail if ''is_infinite(R)'' +* Let ''nonce_parity = has_even_y(R)'' +* If ''nonce_parity'': +** Let ''a = a' '', ''b = b' '' +* Else: +** Let ''a = n − a' '', ''b = n − b' '' +* Let ''e = int(hashBIP0340/challenge(xbytes(R) || xbytes(Q) || m)) mod n'' +* Let ''e' = (e + b) mod n'' +* Let ''session_ctx = (pk, bytes(32, a), bytes(32, e), cbytes(R), tweak1..v, is_xonly1..v)'' +* Return ''(session_ctx, bytes(32, e'), pk_parity, nonce_parity)'' +
+ +=====Blind Signing===== + +
+Algorithm ''BlindSign(sk, blindchallenge, blindsecnonce, pk_parity, nonce_parity)'': +* Inputs: +** The secret key ''sk'': a 32-byte array +** The blind challenge ''blindchallenge'': a 32-byte array ''e' '' +** The secret nonce ''blindsecnonce'': a byte array whose first 32 bytes are ''k'' (remaining bytes are implementation-defined) +** ''pk_parity'': boolean (from ''BlindChallengeGen'') +** ''nonce_parity'': boolean (from ''BlindChallengeGen'') +* Let ''d' = int(sk)''; fail if ''d' = 0'' or ''d' ≥ n'' +* Let ''P = d'⋅G''; fail if ''is_infinite(P)'' +* Let ''d = d' '' if ''pk_parity'' else ''n − d' '' +* Let ''e' = int(blindchallenge)''; fail if ''e' ≥ n'' +* Let ''k' = int(blindsecnonce[0:32])''; fail if ''k' = 0'' or ''k' ≥ n'' +* Let ''k = k' '' if ''nonce_parity'' else ''n − k' '' +* Overwrite ''blindsecnonce[0:64]'' with 64 zero bytes This helps prevent accidental nonce reuse. A zeroed ''blindsecnonce'' MUST cause subsequent ''BlindSign'' calls to fail. +* Let ''R' = k'⋅G''; fail if ''is_infinite(R')'' This check holds except with negligible probability. +* Let ''s' = (k + e'⋅d) mod n'' +* If ''VerifyBlindSignature(cbytes(P), cbytes(R'), blindchallenge, bytes(32, s'), pk_parity, nonce_parity)'' returns failure, abort +* Return ''blindsignature = bytes(32, s')'' +
+ +
+Algorithm ''VerifyBlindSignature(pk, blindpubnonce, blindchallenge, blindsignature, pk_parity, nonce_parity)'': +* Inputs: +** ''pk'': a 33-byte compressed public key +** ''blindpubnonce'': the signer’s 33-byte ''R' = k'⋅G'' +** ''blindchallenge'': 32-byte ''e' '' +** ''blindsignature'': 32-byte ''s' '' +** ''pk_parity, nonce_parity'': booleans +* Let ''P' ' = cpoint(pk)''; let ''P = P' '' if ''pk_parity'' else ''−P' '' ; fail if ''is_infinite(P)'' +* Let ''R' ' = cpoint(blindpubnonce)''; let ''R = R' '' if ''nonce_parity'' else ''−R' '' +* Let ''e' = int(blindchallenge)'', ''s' = int(blindsignature)'' +* Return success iff ''s'⋅G == R + e'⋅P'' +
+ +=====Unblinding===== + +
+Algorithm ''UnblindSignature(session_ctx, blindsignature)'': +* Inputs: +** ''session_ctx'': as defined above +** ''blindsignature'': the 32-byte ''s' '' returned by the signer +* Let ''(Q, gacc, tacc, a, e, R) = GetSessionValues(session_ctx)''; fail if that fails +* Let ''g = 1'' if ''has_even_y(Q)'', else ''g = −1 mod n'' +* Let ''s' = int(blindsignature)''; fail if ''s' ≥ n'' +* Let ''s = (s' + a + e⋅g⋅tacc) mod n'' +* Return the BIP340 signature ''sig = xbytes(R) || bytes(32, s)'' +
+ +== Security Considerations == +* Exposure of any delegated tweak scalar t enables signing only for the specific child key(s) that scalar was derived for, and is typically short-lived if disclosed immediately before spending. +* Delegatees MUST ensure every delegated path remains non-hardened and that ''ComputeBIP32Tweak'' yields the correct tweak t; incorrect scalars could render the delegator incapable of producing a signature. +* Delegators MUST verify change outputs when tweak data is provided (for example via ''ChangeOutputVerification'') to avoid authorizing unexpected scripts. +* Reusing the same k' (first 32 bytes in blindsecnonce) across two BlindSign calls allows recovery of the base secret key. +* When using blinded signing, opening multiple sessions concurrently against the same signer can allow an attacker to learn the base secret key. If concurrency is required, use the concurrently secure variant (encryption + ZK) instead (not specified in this BIP). + +== Test Vectors == +A [[bip-0089/vectors|collection of JSON test vectors]] are provided, along with a [[bip-0089/reference.py|python reference implementation]]. +It uses a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab/ secp256k1lab] library +(commit [https://github.com/secp256k1lab/secp256k1lab/commit/a265da139aea27386085a2a8760f8698e1bda64e +a265da139aea27386085a2a8760f8698e1bda64e]). + +You may also find example code of CCD in action [https://github.com/jurvis/chaincode-delegation here]. + +== Changelog == + +* '''0.1.3''' (2026-02-02): Upgrade secp256k1lab and add license file; fix type checker and linter issues; clarify Delegator/Delegatee terminology, derivation constraints, signing modes, and verification scope. +* '''0.1.2''' (2025-12-03): Updated to reflect BIP number assignment. +* '''0.1.1''' (2025-11-30): Fix acknowledgments spelling, BIP3 formatting, and use "Chain Code" with a space throughout. +* '''0.1.0''' (2025-10-14): Publication of draft BIP + +== Acknowledgements == +* Arik Sosman and Wilmer Paulino for the initial discussions and validation of this idea. +* Sanket Kajalkar, Jordan Mecom, Gregory Sanders, ZmnSCPxj, Yuval Kogman, and John Cantrell for code and design review. + +== Copyright == +This BIP is licensed under the BSD 3-Clause license. diff --git a/bip-0089/bip32.py b/bip-0089/bip32.py new file mode 100644 index 0000000000..fc82166c96 --- /dev/null +++ b/bip-0089/bip32.py @@ -0,0 +1,170 @@ +"""BIP32 helpers for the CCD reference implementation.""" + +from __future__ import annotations + +from dataclasses import dataclass +import hmac +from hashlib import new as hashlib_new, sha256, sha512 +from typing import List, Tuple, Mapping, Sequence + +from secp256k1lab.secp256k1 import G, GE, Scalar + +CURVE_N = Scalar.SIZE + +def int_to_bytes(value: int, length: int) -> bytes: + return value.to_bytes(length, "big") + + +def bytes_to_int(data: bytes) -> int: + return int.from_bytes(data, "big") + +def compress_point(point: GE) -> bytes: + if point.infinity: + raise ValueError("Cannot compress point at infinity") + return point.to_bytes_compressed() + + +def decompress_point(data: bytes) -> GE: + return GE.from_bytes_compressed(data) + +def apply_tweak_to_public(base_public: bytes, tweak: int) -> bytes: + base_point = GE.from_bytes_compressed(base_public) + tweaked_point = base_point + (tweak % CURVE_N) * G + if tweaked_point.infinity: + raise ValueError("Tweaked key is at infinity") + return tweaked_point.to_bytes_compressed() + + +def apply_tweak_to_secret(base_secret: int, tweak: int) -> int: + if not (0 < base_secret < CURVE_N): + raise ValueError("Invalid base secret scalar") + return (base_secret + tweak) % CURVE_N + +def decode_path(path_elements: Sequence[object]) -> List[int]: + result: List[int] = [] + for element in path_elements: + if isinstance(element, int): + index = element + else: + element_str = str(element) + hardened = element_str.endswith("'") or element_str.endswith("h") + suffix = element_str[:-1] if hardened else element_str + if not suffix: + raise AssertionError("invalid derivation index") + index = int(suffix) + if hardened: + index |= HARDENED_INDEX + result.append(index) + return result + +HARDENED_INDEX = 0x80000000 + + +def _hash160(data: bytes) -> bytes: + return hashlib_new("ripemd160", sha256(data).digest()).digest() + + +@dataclass +class ExtendedPublicKey: + point: GE + chain_code: bytes + depth: int = 0 + parent_fingerprint: bytes = b"\x00\x00\x00\x00" + child_number: int = 0 + + def fingerprint(self) -> bytes: + return _hash160(compress_point(self.point))[:4] + + def derive_child(self, index: int) -> Tuple[int, "ExtendedPublicKey"]: + tweak, child_point, child_chain = derive_public_child(self.point, self.chain_code, index) + child = ExtendedPublicKey( + point=child_point, + chain_code=child_chain, + depth=self.depth + 1, + parent_fingerprint=self.fingerprint(), + child_number=index, + ) + return tweak, child + + +def derive_public_child(parent_point: GE, chain_code: bytes, index: int) -> Tuple[int, GE, bytes]: + if index >= HARDENED_INDEX: + raise ValueError("Hardened derivations are not supported for delegates") + + data = compress_point(parent_point) + int_to_bytes(index, 4) + il_ir = hmac.new(chain_code, data, sha512).digest() + il, ir = il_ir[:32], il_ir[32:] + tweak = bytes_to_int(il) + if tweak >= CURVE_N: + raise ValueError("Invalid tweak derived (>= curve order)") + + child_point_bytes = apply_tweak_to_public(compress_point(parent_point), tweak) + child_point = decompress_point(child_point_bytes) + return tweak, child_point, ir + + +def parse_path(path: str) -> List[int]: + if not path or path in {"m", "M"}: + return [] + if path.startswith(("m/", "M/")): + path = path[2:] + + components: List[int] = [] + for element in path.split("/"): + if element.endswith("'") or element.endswith("h"): + raise ValueError("Hardened steps are not allowed in CCD derivations") + index = int(element) + if index < 0 or index >= HARDENED_INDEX: + raise ValueError("Derivation index out of range") + components.append(index) + return components + +def parse_extended_public_key(data: Mapping[str, object]) -> ExtendedPublicKey: + compressed_hex = data.get("compressed") + if not isinstance(compressed_hex, str): + raise ValueError("Compressed must be a string") + + chain_code_hex = data.get("chain_code") + if not isinstance(chain_code_hex, str): + raise ValueError("Chain code must be a string") + + depth = data.get("depth") + if not isinstance(depth, int): + raise ValueError("Depth must be an integer") + + child_number = data.get("child_number", 0) + if not isinstance(child_number, int): + raise ValueError("Child number must be an integer") + + parent_fp_hex = data.get("parent_fingerprint", "00000000") + + compressed = bytes.fromhex(compressed_hex) + chain_code = bytes.fromhex(chain_code_hex) + parent_fp = bytes.fromhex(str(parent_fp_hex)) + return build_extended_public_key( + compressed, + chain_code, + depth=depth, + parent_fingerprint=parent_fp, + child_number=child_number, + ) + + +def build_extended_public_key( + compressed: bytes, + chain_code: bytes, + *, + depth: int = 0, + parent_fingerprint: bytes = b"\x00\x00\x00\x00", + child_number: int = 0, +) -> ExtendedPublicKey: + if len(chain_code) != 32: + raise ValueError("Chain code must be 32 bytes") + point = decompress_point(compressed) + return ExtendedPublicKey( + point=point, + chain_code=chain_code, + depth=depth, + parent_fingerprint=parent_fingerprint, + child_number=child_number, + ) diff --git a/bip-0089/descriptor.py b/bip-0089/descriptor.py new file mode 100644 index 0000000000..ab20b356d4 --- /dev/null +++ b/bip-0089/descriptor.py @@ -0,0 +1,42 @@ +"""Helpers for working with minimal SortedMulti descriptor templates.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Sequence + + +@dataclass(frozen=True) +class SortedMultiDescriptorTemplate: + """Minimal representation of a ``wsh(sortedmulti(m, ...))`` descriptor.""" + + threshold: int + + def witness_script(self, tweaked_keys: Sequence[bytes]) -> bytes: + """Return the witness script for ``wsh(sortedmulti(threshold, tweaked_keys))``.""" + + if not tweaked_keys: + raise ValueError("sortedmulti requires at least one key") + if not 1 <= self.threshold <= len(tweaked_keys): + raise ValueError("threshold must satisfy 1 <= m <= n") + + for key in tweaked_keys: + if len(key) != 33: + raise ValueError("sortedmulti keys must be 33-byte compressed pubkeys") + + sorted_keys = sorted(tweaked_keys) + script = bytearray() + script.append(_op_n(self.threshold)) + for key in sorted_keys: + script.append(len(key)) + script.extend(key) + script.append(_op_n(len(sorted_keys))) + script.append(0xAE) # OP_CHECKMULTISIG + return bytes(script) + +def _op_n(value: int) -> int: + if not 0 <= value <= 16: + raise ValueError("OP_N value out of range") + if value == 0: + return 0x00 + return 0x50 + value diff --git a/bip-0089/reference.py b/bip-0089/reference.py new file mode 100644 index 0000000000..f81d5359a2 --- /dev/null +++ b/bip-0089/reference.py @@ -0,0 +1,784 @@ +# BIPXXX reference implementation +# +# WARNING: This implementation is for demonstration purposes only and _not_ to +# be used in production environments. The code is vulnerable to timing attacks, +# for example. + +from typing import Dict, Mapping, Optional, Sequence, Tuple, NewType, NamedTuple, List, Callable, Any, cast +import hashlib +import json +import os +import secrets +import sys + +from bip32 import ( + CURVE_N, + ExtendedPublicKey, + apply_tweak_to_public, + apply_tweak_to_secret, + int_to_bytes, + parse_extended_public_key, + compress_point, + decode_path, +) +from descriptor import SortedMultiDescriptorTemplate + +from secp256k1lab.bip340 import schnorr_sign, schnorr_verify +from secp256k1lab.keys import pubkey_gen_plain +from secp256k1lab.secp256k1 import G, GE, Scalar + +HashFunc = Callable[[bytes], Any] + +PlainPk = NewType('PlainPk', bytes) +XonlyPk = NewType('XonlyPk', bytes) + +def xbytes(P: GE) -> bytes: + return P.to_bytes_xonly() + +def cbytes(P: GE) -> bytes: + return P.to_bytes_compressed() + +def cpoint(x: bytes) -> GE: + return GE.from_bytes_compressed(x) + +TweakContext = NamedTuple('TweakContext', [('Q', GE), + ('gacc', Scalar), + ('tacc', Scalar)]) + +def tweak_ctx_init(pk: PlainPk) -> TweakContext: + Q = cpoint(pk) + if Q.infinity: + raise ValueError('The public key cannot be infinity.') + gacc = Scalar(1) + tacc = Scalar(0) + return TweakContext(Q, gacc, tacc) + +def apply_tweak(tweak_ctx: TweakContext, tweak: bytes, is_xonly: bool) -> TweakContext: + if len(tweak) != 32: + raise ValueError('The tweak must be a 32-byte array.') + Q, gacc, tacc = tweak_ctx + if is_xonly and not Q.has_even_y(): + g = Scalar(-1) + else: + g = Scalar(1) + try: + t = Scalar.from_bytes_checked(tweak) + except ValueError: + raise ValueError('The tweak must be less than n.') + Q_ = g * Q + t * G + if Q_.infinity: + raise ValueError('The result of tweaking cannot be infinity.') + gacc_ = g * gacc + tacc_ = t + g * tacc + return TweakContext(Q_, gacc_, tacc_) + +# Return the plain public key corresponding to a given secret key +def individual_pk(seckey: bytes) -> PlainPk: + return PlainPk(pubkey_gen_plain(seckey)) + +def bytes_xor(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes, hash_func: HashFunc = hashlib.sha256) -> bytes: + tag_hash = hash_func(tag.encode()).digest() + return hash_func(tag_hash + tag_hash + msg).digest() + +def nonce_hash(rand: bytes, pk: PlainPk, extra_in: bytes) -> bytes: + buf = b'' + buf += rand + buf += len(pk).to_bytes(1, 'big') + buf += pk + buf += len(extra_in).to_bytes(4, 'big') + buf += extra_in + return tagged_hash('CCD/blindnonce', buf) + +def blind_nonce_gen_internal(rand_: bytes, sk: Optional[bytes], pk: Optional[PlainPk], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None: + rand = bytes_xor(sk, tagged_hash('CCD/aux', rand_)) + else: + rand = rand_ + if pk is None: + pk = PlainPk(b'') + if extra_in is None: + extra_in = b'' + k = Scalar.from_bytes_wrapping(nonce_hash(rand, pk, extra_in)) + # k == 0 cannot occur except with negligible probability. + assert k != 0 + R = k * G + assert R is not None + blindpubnonce = cbytes(R) + blindsecnonce = bytearray(k.to_bytes() + pk) + return blindsecnonce, blindpubnonce + +def blind_nonce_gen(sk: Optional[bytes], pk: Optional[PlainPk], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None and len(sk) != 32: + raise ValueError('The optional byte array sk must have length 32.') + rand_ = secrets.token_bytes(32) + return blind_nonce_gen_internal(rand_, sk, pk, extra_in) + +SessionContext = NamedTuple('SessionContext', [('pk', PlainPk), + ('blindfactor', bytes), + ('challenge', bytes), + ('pubnonce', bytes), + ('tweaks', List[bytes]), + ('is_xonly', List[bool])]) + +def blind_factor_hash(rand: bytes, cpk: PlainPk, blindpubnonce: bytes, msg: bytes, extra_in: bytes) -> bytes: + buf = b'' + buf += rand + buf += len(cpk).to_bytes(1, 'big') + buf += cpk + buf += len(blindpubnonce).to_bytes(1, 'big') + buf += blindpubnonce + buf += len(msg).to_bytes(8, 'big') + buf += msg + buf += len(extra_in).to_bytes(4, 'big') + buf += extra_in + return tagged_hash('CCD/blindfactor', buf, hashlib.sha512) + +def blind_challenge_gen_internal(rand: bytes, msg: bytes, blindpubnonce: bytes, pk: PlainPk, tweaks: List[bytes], is_xonly: List[bool], extra_in: Optional[bytes]) -> Tuple[SessionContext, bytes, bool, bool]: + if extra_in is None: + extra_in = b'' + Q, gacc, tacc = pubkey_and_tweak(pk, tweaks, is_xonly) + cpk = PlainPk(cbytes(Q)) + k = blind_factor_hash(rand, cpk, blindpubnonce, msg, extra_in) + a_ = Scalar.from_bytes_wrapping(k[0:32]) + assert a_ != 0 + b_ = Scalar.from_bytes_wrapping(k[32:64]) + assert b_ != 0 + + g = Scalar(1) if Q.has_even_y() else Scalar(-1) + pk_parity = g * gacc == 1 + X_ = cpoint(pk) + X = X_ if pk_parity else -X_ + + R_ = cpoint(blindpubnonce) + R = R_ + (a_ * G) + (b_ * X) + if R is None: + raise ValueError('The result of nonce blinding cannot be infinity.') + nonce_parity = R.has_even_y() + if not nonce_parity: + a = -a_ + b = -b_ + else: + a = a_ + b = b_ + + e = Scalar.from_bytes_wrapping(tagged_hash("BIP0340/challenge", xbytes(R) + xbytes(Q) + msg)) + e_ = e + b + + session_ctx = SessionContext(pk, a.to_bytes(), e.to_bytes(), cbytes(R), tweaks, is_xonly) + return session_ctx, e_.to_bytes(), pk_parity, nonce_parity + +def blind_challenge_gen(msg: bytes, blindpubnonce: bytes, pk: PlainPk, tweaks: List[bytes], is_xonly: List[bool], extra_in: Optional[bytes]) -> Tuple[SessionContext, bytes, bool, bool]: + rand = secrets.token_bytes(32) + return blind_challenge_gen_internal(rand, msg, blindpubnonce, pk, tweaks, is_xonly, extra_in) + +def blind_sign(sk: bytes, blindchallenge: bytes, blindsecnonce: bytearray, pk_parity: bool, nonce_parity: bool) -> bytes: + try: + d_ = Scalar.from_bytes_checked(sk) + if d_ == 0: + raise ValueError('The secret key cannot be zero.') + except ValueError: + raise ValueError('The secret key is out of range.') + P = d_ * G + if P.infinity: + raise ValueError('The public key cannot be infinity.') + d = d_ if pk_parity else -d_ + e_ = Scalar.from_bytes_checked(blindchallenge) + k_ = Scalar.from_bytes_checked(bytes(blindsecnonce[0:32])) + k = k_ if nonce_parity else -k_ + # Overwrite the secnonce argument with zeros such that subsequent calls of + # sign with the same secnonce raise a ValueError. + blindsecnonce[:64] = bytearray(b'\x00'*64) + R_ = k_ * G + if R_.infinity: + raise ValueError('The blindpubnonce cannot be infinity.') + s_ = k + (e_ * d) + pk = PlainPk(cbytes(P)) + blindsignature = s_.to_bytes() + assert verify_blind_signature(pk, cbytes(R_), blindchallenge, blindsignature, pk_parity, nonce_parity) + return blindsignature + +def verify_blind_signature(pk: PlainPk, blindpubnonce: bytes, blindchallenge: bytes, blindsignature: bytes, pk_parity: bool, nonce_parity: bool) -> bool: + P_ = cpoint(pk) + P = P_ if pk_parity else -P_ + if P.infinity: + raise ValueError('The public key cannot be infinity.') + R_ = cpoint(blindpubnonce) + R = R_ if nonce_parity else -R_ + e_ = Scalar.from_bytes_checked(blindchallenge) + s_ = Scalar.from_bytes_checked(blindsignature) + R_calc = (s_ * G) + (-e_ * P) + if R_calc.infinity: + return False + return R == R_calc + +def pubkey_and_tweak(pk: PlainPk, tweaks: List[bytes], is_xonly: List[bool]) -> TweakContext: + if len(tweaks) != len(is_xonly): + raise ValueError('The tweaks and is_xonly arrays must have the same length.') + tweak_ctx = tweak_ctx_init(pk) + v = len(tweaks) + for i in range(v): + tweak_ctx = apply_tweak(tweak_ctx, tweaks[i], is_xonly[i]) + return tweak_ctx + +def get_session_values(session_ctx: SessionContext) -> Tuple[GE, Scalar, Scalar, GE, Scalar, Scalar]: + (pk, blindfactor, challenge, pubnonce, tweaks, is_xonly) = session_ctx + Q, gacc, tacc = pubkey_and_tweak(pk, tweaks, is_xonly) + a = Scalar.from_bytes_checked(blindfactor) + e = Scalar.from_bytes_checked(challenge) + R = cpoint(pubnonce) + return Q, a, e, R, gacc, tacc + +def unblind_signature(session_ctx: SessionContext, blindsignature: bytes) -> bytes: + Q, a, e, R, gacc, tacc = get_session_values(session_ctx) + s_ = Scalar.from_bytes_checked(blindsignature) + g = Scalar(1) if Q.has_even_y() else Scalar(-1) + s = s_ + a + (e * g * tacc) + return xbytes(R) + s.to_bytes() + +# +# The following code is only used for testing. +# + +def hx(s: str) -> bytes: + return bytes.fromhex(s) + +def fromhex_all(l): # noqa: E741 + return [hx(l_i) for l_i in l] + + +def get_error_details(tc): + et = tc["error"]["type"] + # Resolve to real class from name + exc_cls = getattr(__builtins__, et, None) or getattr(__import__("builtins"), et) + # Optional message predicate + msg = tc["error"].get("message") + if msg is None: + return exc_cls, (lambda e: True) + return exc_cls, (lambda e: msg in str(e)) + +def assert_raises(exc_cls, fn, pred): + try: + fn() + except Exception as e: + assert isinstance(e, exc_cls), f"Raised {type(e).__name__}, expected {exc_cls.__name__}" + assert pred(e), f"Exception message predicate failed: {e}" + return + assert False, f"Expected {exc_cls.__name__} but no exception was raised" + +def build_session_ctx(obj): + pk = PlainPk(bytes.fromhex(obj["pk"])) + a = bytes.fromhex(obj["blindfactor"]) + e = bytes.fromhex(obj["challenge"]) + R = bytes.fromhex(obj["pubnonce"]) + tweaks = fromhex_all(obj["tweaks"]) + is_xonly = obj["is_xonly"] + return (pk, a, e, R, tweaks, is_xonly) + +def test_blind_nonce_gen_vectors(): + with open(os.path.join(sys.path[0], 'vectors', 'blind_nonce_gen_vectors.json')) as f: + tv = json.load(f) + + for tc in tv["test_cases"]: + def get_bytes(key) -> bytes: + return bytes.fromhex(tc[key]) + + def get_bytes_maybe(key) -> Optional[bytes]: + v = tc.get(key) + return None if v is None else bytes.fromhex(v) + + rand_ = get_bytes("rand_") + sk = get_bytes_maybe("sk") + pk = get_bytes_maybe("pk") + if pk is not None: + pk = PlainPk(pk) + extra_in = get_bytes_maybe("extra_in") + + expected_blindsecnonce = get_bytes("expected_blindsecnonce") + expected_blindpubnonce = get_bytes("expected_blindpubnonce") + + blindsecnonce, blindpubnonce = blind_nonce_gen_internal(rand_, sk, pk, extra_in) + + assert bytes(blindsecnonce) == expected_blindsecnonce + assert blindpubnonce == expected_blindpubnonce + + pk_len = 0 if tc["pk"] is None else 33 + assert len(expected_blindsecnonce) == 32 + pk_len + assert len(expected_blindpubnonce) == 33 + +def test_blind_challenge_gen_vectors(): + with open(os.path.join(sys.path[0], 'vectors', 'blind_challenge_gen_vectors.json')) as f: + tv = json.load(f) + + # ---------- Valid cases ---------- + for tc in tv["test_cases"]: + rand = bytes.fromhex(tc["rand"]) + msg = bytes.fromhex(tc["msg"]) if tc["msg"] != "" else b"" + blindpubnonce = bytes.fromhex(tc["blindpubnonce"]) + pk = PlainPk(bytes.fromhex(tc["pk"])) + tweaks = fromhex_all(tc["tweaks"]) + is_xonly = tc["is_xonly"] + extra_in = None if tc["extra_in"] is None else bytes.fromhex(tc["extra_in"]) + + expected_a = bytes.fromhex(tc["expected_blindfactor"]) + expected_e = bytes.fromhex(tc["expected_challenge"]) + expected_R = bytes.fromhex(tc["expected_pubnonce"]) + expected_e_prime = bytes.fromhex(tc["expected_blindchallenge"]) + expected_pk_parity = bool(tc["expected_pk_parity"]) + expected_nonce_parity = bool(tc["expected_nonce_parity"]) + + session_ctx, blindchallenge, pk_parity, nonce_parity = blind_challenge_gen_internal( + rand, msg, blindpubnonce, pk, tweaks, is_xonly, extra_in + ) + + # Check tuple outputs + assert blindchallenge == expected_e_prime + assert pk_parity == expected_pk_parity + assert nonce_parity == expected_nonce_parity + + # Check session_ctx fields + pk_sc, blindfactor_sc, challenge_sc, pubnonce_sc, tweaks_sc, is_xonly_sc = session_ctx + assert pk_sc == pk + assert blindfactor_sc == expected_a + assert challenge_sc == expected_e + assert pubnonce_sc == expected_R + assert tweaks_sc == tweaks + assert is_xonly_sc == is_xonly + + # Extra sanity: recompute Q and e and compare + Q, gacc, tacc = pubkey_and_tweak(pk, tweaks, is_xonly) + R = cpoint(expected_R) + e_check = tagged_hash("BIP0340/challenge", xbytes(R) + xbytes(Q) + msg) + assert e_check == expected_e + + # Length sanity + assert len(expected_a) == 32 + assert len(expected_e) == 32 + assert len(expected_R) == 33 + assert len(expected_e_prime) == 32 + + # ---------- Error cases ---------- + for tc in tv.get("error_test_cases", []): + rand = bytes.fromhex(tc["rand"]) + msg = bytes.fromhex(tc["msg"]) if tc["msg"] != "" else b"" + blindpubnonce = bytes.fromhex(tc["blindpubnonce"]) + pk = PlainPk(bytes.fromhex(tc["pk"])) + tweaks = fromhex_all(tc["tweaks"]) + is_xonly = tc["is_xonly"] + extra_in = None if tc["extra_in"] is None else bytes.fromhex(tc["extra_in"]) + + err = tc["error"] + err_type = err["type"] + err_message = err.get("message") + + raised = False + try: + _ = blind_challenge_gen_internal(rand, msg, blindpubnonce, pk, tweaks, is_xonly, extra_in) + except Exception as e: + raised = True + # Type check + assert e.__class__.__name__ == err_type + # Optional substring match on message, if provided + if err_message is not None: + assert err_message in str(e) + assert raised, "Expected an exception but none was raised" + +def test_blind_sign_and_verify_vectors(): + with open(os.path.join(sys.path[0], 'vectors', 'blind_sign_and_verify_vectors.json')) as f: + tv = json.load(f) + + # ------------------ Valid ------------------ + for test_case in tv["valid_test_cases"]: + sk = hx(test_case["sk"]) + pk = PlainPk(hx(test_case["pk"])) + blindsecnonce_all = hx(test_case["blindsecnonce"]) + blindpubnonce = hx(test_case["blindpubnonce"]) + blindchallenge = hx(test_case["blindchallenge"]) + pk_parity = bool(test_case["pk_parity"]) + nonce_parity = bool(test_case["nonce_parity"]) + + # R' consistency check: cbytes(k'*G) == blindpubnonce + k_ = Scalar.from_bytes_checked(blindsecnonce_all[0:32]) + R_prime = k_ * G + assert cbytes(R_prime) == blindpubnonce + + expected_sprime = hx(test_case["expected"]["blindsignature"]) + + # Copy because blind_sign zeroizes the first 64 bytes of the buffer + secnonce_buf = bytearray(blindsecnonce_all) + s_prime = blind_sign(sk, blindchallenge, secnonce_buf, pk_parity, nonce_parity) + assert s_prime == expected_sprime + + checks = test_case.get("checks", {}) + if checks.get("secnonce_prefix_zeroed_after_sign", False): + assert all(b == 0 for b in secnonce_buf[:64]) + + if checks.get("verify_returns_true", True): + ok = verify_blind_signature(pk, blindpubnonce, blindchallenge, s_prime, pk_parity, nonce_parity) + assert ok is True + + if checks.get("second_call_raises_valueerror", False): + # Reuse the same (now zeroized) buffer; must raise + def try_again(): + blind_sign(sk, blindchallenge, secnonce_buf, pk_parity, nonce_parity) + raised = False + try: + try_again() + except ValueError: + raised = True + assert raised, "Expected ValueError on nonce reuse" + + # ------------------ Sign errors (exceptions) ------------------ + for test_case in tv.get("sign_error_test_cases", []): + exception, except_fn = get_error_details(test_case) + + sk = hx(test_case["sk"]) + blindsecnonce_all = hx(test_case["blindsecnonce"]) + blindchallenge = hx(test_case["blindchallenge"]) + pk_parity = bool(test_case["pk_parity"]) + nonce_parity = bool(test_case["nonce_parity"]) + repeat = int(test_case.get("repeat", 1)) + + if repeat == 1: + # Single-call error (e.g., out-of-range e') + assert_raises(exception, lambda: blind_sign(sk, blindchallenge, bytearray(blindsecnonce_all), pk_parity, nonce_parity), except_fn) + else: + # Two-call error (nonce reuse) + buf = bytearray(blindsecnonce_all) + # First call should succeed + _ = blind_sign(sk, blindchallenge, buf, pk_parity, nonce_parity) + # Second call must raise + assert_raises(exception, lambda: blind_sign(sk, blindchallenge, buf, pk_parity, nonce_parity), except_fn) + + # ------------------ Verify returns False (no exception) ------------------ + for test_case in tv.get("verify_fail_test_cases", []): + pk = PlainPk(hx(test_case["pk"])) + blindpubnonce = hx(test_case["blindpubnonce"]) + blindchallenge = hx(test_case["blindchallenge"]) + blindsignature = hx(test_case["blindsignature"]) + pk_parity = bool(test_case["pk_parity"]) + nonce_parity = bool(test_case["nonce_parity"]) + + assert verify_blind_signature(pk, blindpubnonce, blindchallenge, blindsignature, pk_parity, nonce_parity) is False + + # ------------------ Verify errors (exceptions) ------------------ + for test_case in tv.get("verify_error_test_cases", []): + exception, except_fn = get_error_details(test_case) + + pk = PlainPk(hx(test_case["pk"])) + blindpubnonce = hx(test_case["blindpubnonce"]) + blindchallenge = hx(test_case["blindchallenge"]) + blindsignature = hx(test_case["blindsignature"]) + pk_parity = bool(test_case["pk_parity"]) + nonce_parity = bool(test_case["nonce_parity"]) + + assert_raises(exception, lambda: verify_blind_signature(pk, blindpubnonce, blindchallenge, blindsignature, pk_parity, nonce_parity), except_fn) + +def test_unblind_signature_vectors(): + with open(os.path.join(sys.path[0], 'vectors', 'unblind_signature_vectors.json')) as f: + tv = json.load(f) + + # ---------- Valid ---------- + for tc in tv["valid_test_cases"]: + session_ctx = build_session_ctx(tc["session_ctx"]) + msg = bytes.fromhex(tc["msg"]) if tc["msg"] != "" else b"" + blindsignature = bytes.fromhex(tc["blindsignature"]) + expected_sig = bytes.fromhex(tc["expected_bip340_sig"]) + + sig = unblind_signature(session_ctx, blindsignature) + assert sig == expected_sig + + # Verify BIP340 with tweaked Q + pk, _, _, _, tweaks, is_xonly = session_ctx + Q, _, _ = pubkey_and_tweak(pk, tweaks, is_xonly) + assert schnorr_verify(msg, xbytes(Q), sig) + + # ---------- Errors ---------- + for tc in tv.get("error_test_cases", []): + session_ctx = build_session_ctx(tc["session_ctx"]) + msg = bytes.fromhex(tc["msg"]) if tc["msg"] != "" else b"" + blindsignature = bytes.fromhex(tc["blindsignature"]) + + err = tc["error"] + err_type = err["type"] + err_msg = err.get("message") + + raised = False + try: + _ = unblind_signature(session_ctx, blindsignature) + except Exception as e: + raised = True + assert e.__class__.__name__ == err_type + if err_msg is not None: + assert err_msg in str(e) + assert raised, "Expected an exception but none was raised" + +def test_sign_and_verify_random(iters: int) -> None: + for _ in range(iters): + sk = Scalar.from_bytes_wrapping(secrets.token_bytes(32)) + pk = individual_pk(sk.to_bytes()) + msg = Scalar.from_bytes_wrapping(secrets.token_bytes(32)) + v = secrets.randbelow(4) + tweaks = [secrets.token_bytes(32) for _ in range(v)] + tweak_modes = [secrets.choice([False, True]) for _ in range(v)] + Q, _, _ = pubkey_and_tweak(pk, tweaks, tweak_modes) + assert not Q.infinity + + # Round 1 + # Signer + extra_in_1 = secrets.token_bytes(32) + blindsecnonce, blindpubnonce = blind_nonce_gen(sk.to_bytes(), pk, extra_in_1) + # User + extra_in_2 = secrets.token_bytes(32) + session_ctx, blindchallenge, pk_parity, nonce_parity = blind_challenge_gen(msg.to_bytes(), blindpubnonce, pk, tweaks, tweak_modes, extra_in_2) + + # Round 2 + # Signer + blindsignature = blind_sign(sk.to_bytes(), blindchallenge, blindsecnonce, pk_parity, nonce_parity) + # User + sig = unblind_signature(session_ctx, blindsignature) + assert schnorr_verify(msg.to_bytes(), xbytes(Q), sig) + +def compute_bip32_tweak(xpub: ExtendedPublicKey, path: Sequence[int]) -> Tuple[int, ExtendedPublicKey]: + """Compute the CCD tweak scalar for a non-hardened derivation path.""" + + aggregate = 0 + current = xpub + for index in path: + tweak, child = current.derive_child(index) + aggregate = (aggregate + tweak) % CURVE_N + current = child + return aggregate, current + +def input_verification( + descriptor_template: SortedMultiDescriptorTemplate, + witness_script: Optional[bytes], + tweaks: Mapping[bytes, int], +) -> bool: + """Check that an input script matches the tweaked policy from CCD data.""" + + return _verify_tweaked_descriptor( + descriptor_template, + witness_script, + tweaks, + ) + + +def change_output_verification( + descriptor_template: SortedMultiDescriptorTemplate, + witness_script: Optional[bytes], + tweaks: Mapping[bytes, int], +) -> bool: + """Validate a change output script using delegated CCD tweak data.""" + + return _verify_tweaked_descriptor( + descriptor_template, + witness_script, + tweaks, + ) + + +def _verify_tweaked_descriptor( + descriptor_template: SortedMultiDescriptorTemplate, + witness_script: Optional[bytes], + tweaks: Mapping[bytes, int], +) -> bool: + if witness_script is None or not tweaks: + return False + + if descriptor_template.threshold > len(tweaks): + return False + + tweaked_keys: List[bytes] = [] + for base_key, tweak in sorted(tweaks.items(), key=lambda item: item[0]): + if len(base_key) != 33: + return False + tweaked_key = apply_tweak_to_public(base_key, tweak % CURVE_N) + tweaked_keys.append(tweaked_key) + + try: + expected_witness_script = descriptor_template.witness_script(tweaked_keys) + except ValueError: + return False + + return witness_script == expected_witness_script + +def delegator_sign( + tweak: int, + base_secret: int, + message: bytes, +) -> bytes: + """Derive the delegated key, sign ``message``, and return signature.""" + child_secret = int_to_bytes(apply_tweak_to_secret(base_secret, tweak), 32) + message_digest = hashlib.sha256(message).digest() + signature = schnorr_sign(message_digest, child_secret, bytes(32)) + return signature + +def test_compute_tweak_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'compute_bip32_tweak_vectors.json')) as f: + data = json.load(f) + + default_xpub_data = data.get("xpub") + if default_xpub_data is None: + raise AssertionError("compute_bip32_tweak_vectors.json missing top-level 'xpub'") + + for case in data.get("valid_test_cases", []): + xpub_data = case.get("xpub", default_xpub_data) + xpub = parse_extended_public_key(xpub_data) + path = decode_path(case.get("path", [])) + expected = case.get("expected") + if not isinstance(expected, Mapping): + raise AssertionError("valid compute_tweak case missing 'expected'") + + tweak_hex = expected.get("tweak") + if not isinstance(tweak_hex, str): + raise AssertionError("expected 'tweak' must be a string") + + derived = expected.get("derived_xpub", {}) + derived_compressed = derived.get("compressed") + if not isinstance(derived_compressed, str): + raise AssertionError("expected 'derived_xpub.compressed' must be a string") + + derived_chain_code = derived.get("chain_code") + if not isinstance(derived_chain_code, str): + raise AssertionError("expected 'derived_xpub.chain_code' must be a string") + + tweak, child = compute_bip32_tweak(xpub, path) + actual_tweak_hex = f"{tweak:064x}" + if actual_tweak_hex != tweak_hex.lower(): + raise AssertionError(f"tweak mismatch: expected {tweak_hex}, got {actual_tweak_hex}") + + actual_compressed = compress_point(child.point).hex() + actual_chain_code = child.chain_code.hex() + if actual_compressed != derived_compressed.lower(): + raise AssertionError("derived public key mismatch") + if actual_chain_code != derived_chain_code.lower(): + raise AssertionError("derived chain code mismatch") + + for case in data.get("error_test_cases", []): + xpub_data = case.get("xpub", default_xpub_data) + xpub = parse_extended_public_key(xpub_data) + path = decode_path(case.get("path", [])) + error_spec = case.get("error", {}) + exc_type, message = resolve_error_spec(error_spec) + + try: + compute_bip32_tweak(xpub, path) + except exc_type as exc: + if message and message.lower() not in str(exc).lower(): + raise AssertionError(f"expected error containing '{message}' but got '{exc}'") + else: + raise AssertionError("expected failure but case succeeded") + +def test_delegator_sign_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'delegator_sign_vectors.json')) as f: + data = json.load(f) + + for case in data.get("test_cases", []): + base_secret_hex = case.get("base_secret") + tweak_hex = case.get("tweak") + message_hex = case.get("message") + + base_secret = int(base_secret_hex, 16) + tweak = int(tweak_hex, 16) + message = message_hex.encode('utf-8') + + expected = case.get("expected") + if not isinstance(expected, Mapping): + raise AssertionError("delegator_sign case missing 'expected'") + expected_signature_hex = expected.get("signature") + if not isinstance(expected_signature_hex, str): + raise AssertionError("expected 'signature' must be a string") + expected_signature = bytes.fromhex(expected_signature_hex) + + signature = delegator_sign( + tweak, + base_secret, + message, + ) + + if signature != expected_signature: + raise AssertionError("signature mismatch") + + +def test_input_verification_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'input_verification_vectors.json')) as f: + data = json.load(f) + + + for case in data.get("test_cases", []): + descriptor = SortedMultiDescriptorTemplate(threshold=2) + witness_hex = case.get("witness_script") + # Get the tweak map of the bare public keys to the BIP 32 tweak + tweaks_raw = case.get("tweak_map", {}) + tweaks = parse_tweak_map(tweaks_raw) + expected_bool = bool(case.get("expected", False)) + + result = input_verification( + descriptor, + bytes.fromhex(witness_hex), + tweaks, + ) + if result != expected_bool: + raise AssertionError( + f"input_verification result {result} did not match expected {expected_bool}" + ) + +def test_change_output_verification_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'change_output_verification_vectors.json')) as f: + data = json.load(f) + + for case in data.get("test_cases", []): + descriptor = SortedMultiDescriptorTemplate(threshold=2) + witness_hex = case.get("witness_script") + # Get the tweak map of the bare public keys to the BIP 32 tweak + tweaks_raw = case.get("tweak_map", {}) + tweaks = parse_tweak_map(tweaks_raw) + expected_bool = bool(case.get("expected", False)) + + result = change_output_verification( + descriptor, + bytes.fromhex(witness_hex), + tweaks, + ) + if result != expected_bool: + raise AssertionError( + f"change_output_verification result {result} did not match expected {expected_bool}" + ) + +def parse_tweak_map(raw: Mapping[str, object]) -> Dict[bytes, int]: + tweaks: Dict[bytes, int] = {} + for key_hex, tweak_hex in raw.items(): + base_key = bytes.fromhex(key_hex) + if not isinstance(tweak_hex, str): + raise ValueError(f"tweak value for key {key_hex} must be a string") + tweak_value = int(tweak_hex, 16) + tweaks[base_key] = tweak_value % CURVE_N + return tweaks + +def resolve_error_spec(raw: object) -> Tuple[type[Exception], Optional[str]]: + mapping: Dict[str, type[Exception]] = {"value": ValueError, "assertion": AssertionError, "runtime": RuntimeError} + if not isinstance(raw, dict): + return ValueError, None + + raw_dict = cast(Dict[str, Any], raw) + name = str(raw_dict.get("type", "value")).lower() + message = raw_dict.get("message") + exc_type = mapping.get(name, ValueError) + return exc_type, None if message is None else str(message) + +if __name__ == '__main__': + test_blind_nonce_gen_vectors() + test_blind_challenge_gen_vectors() + test_blind_sign_and_verify_vectors() + test_unblind_signature_vectors() + test_sign_and_verify_random(6) + test_compute_tweak_vectors() + test_delegator_sign_vectors() + test_input_verification_vectors() + test_change_output_verification_vectors() + print("All tests passed") \ No newline at end of file diff --git a/bip-0089/secp256k1lab/COPYING b/bip-0089/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0089/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bip-0089/secp256k1lab/__init__.py b/bip-0089/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0089/secp256k1lab/bip340.py b/bip-0089/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0089/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/bip-0089/secp256k1lab/ecdh.py b/bip-0089/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0089/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/bip-0089/secp256k1lab/keys.py b/bip-0089/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0089/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/bip-0089/secp256k1lab/py.typed b/bip-0089/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0089/secp256k1lab/secp256k1.py b/bip-0089/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..0526878d91 --- /dev/null +++ b/bip-0089/secp256k1lab/secp256k1.py @@ -0,0 +1,483 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +from __future__ import annotations +from typing import Self + +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE: int + + def __init__(self, a: int | Self = 0, b: int | Self = 1) -> None: + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, type(self)): + num = a._num + den = a._den + else: + assert isinstance(a, int) + num = a % self.SIZE + den = 1 + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE + else: + assert isinstance(b, int) + den = (den * b) % self.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num: int = num + self._den: int = den + + def __add__(self, a: int | Self) -> Self: + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented + + def __radd__(self, a: int) -> Self: + """Compute the sum of an integer and a field element.""" + return type(self)(a) + self + + @classmethod + def sum(cls, *es: Self) -> Self: + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) + + def __sub__(self, a: int | Self) -> Self: + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented + + def __rsub__(self, a: int) -> Self: + """Compute the difference of an integer and a field element.""" + return type(self)(a) - self + + def __mul__(self, a: int | Self) -> Self: + """Compute the product of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented + + def __rmul__(self, a: int) -> Self: + """Compute the product of an integer with a field element.""" + return type(self)(a) * self + + def __truediv__(self, a: int | Self) -> Self: + """Compute the ratio of two field elements (second may be int).""" + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented + + def __pow__(self, a: int) -> Self: + """Raise a field element to an integer power.""" + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) + + def __neg__(self) -> Self: + """Negate a field element.""" + return type(self)(-self._num, self._den) + + def __int__(self) -> int: + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE + self._den = 1 + return self._num + + def sqrt(self) -> Self | None: + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError + + def is_square(self) -> bool: + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self) -> bool: + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a: object) -> bool: + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + elif isinstance(a, int): + return (self._num - self._den * a) % self.SIZE == 0 + return False # for other types + + def to_bytes(self) -> bytes: + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @classmethod + def from_int_checked(cls, v: int) -> Self: + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v: int) -> Self: + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b: bytes) -> Self: + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) + + def __str__(self) -> str: + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self) -> str: + """Get a string representation of this field element.""" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self) -> Self | None: + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + @classmethod + def from_int_nonzero_checked(cls, v: int) -> Self: + """Convert an integer to a scalar (no zero or overflow allowed).""" + if not (0 < v < cls.SIZE): + raise ValueError + return cls(v) + + @classmethod + def from_bytes_nonzero_checked(cls, b: bytes) -> Self: + """Convert a 32-byte array to a scalar (BE byte order, no zero or overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_nonzero_checked(v) + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + GE objects are immutable. + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = Scalar.SIZE + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + @property + def infinity(self) -> bool: + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self) -> FE: + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self) -> FE: + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + + def __init__(self, x: int | FE | None = None, y: int | FE | None = None) -> None: + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self._infinity = True + else: + # Initialize as point on the curve (and check that it is). + assert x is not None + assert y is not None + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self._infinity = False + self._x = fx + self._y = fy + + def __add__(self, a: GE) -> GE: + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def sum(*ps: GE) -> GE: + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps: tuple[Scalar, GE]) -> GE: + """Compute a (batch) scalar group element multiplication. + + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(int(a), p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a: int | Scalar) -> GE: + """Multiply an integer or scalar with a group element.""" + if self == G: + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) + + def __neg__(self) -> GE: + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def __sub__(self, a: GE) -> GE: + """Subtract a group element from another.""" + return self + (-a) + + def __eq__(self, a: object) -> bool: + """Check if two group elements are equal.""" + if not isinstance(a, type(self)): + return False + return (self - a).infinity + + def has_even_y(self) -> bool: + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + + def to_bytes_compressed(self) -> bytes: + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_compressed_with_infinity(self) -> bytes: + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + + def to_bytes_uncompressed(self) -> bytes: + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self) -> bytes: + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x: int | FE) -> GE: + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + raise ValueError + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes_compressed(b: bytes) -> GE: + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_compressed_with_infinity(b: bytes) -> GE: + """Convert a compressed to a group element, mapping zeros to infinity.""" + if b == 33 * b"\x00": + return GE() + else: + return GE.from_bytes_compressed(b) + + @staticmethod + def from_bytes_uncompressed(b: bytes) -> GE: + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + + @staticmethod + def from_bytes(b: bytes) -> GE: + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + return GE.from_bytes_compressed(b) + else: + return GE.from_bytes_uncompressed(b) + + @staticmethod + def from_bytes_xonly(b: bytes) -> GE: + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r + + @staticmethod + def is_valid_x(x: int | FE) -> bool: + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self) -> str: + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self) -> str: + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + + def __hash__(self) -> int: + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p: GE) -> None: + self.table: list[GE] = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a: Scalar | int) -> GE: + result = GE() + a_ = int(a) + for bit in range(a_.bit_length()): + if a_ & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/bip-0089/secp256k1lab/util.py b/bip-0089/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0089/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() diff --git a/bip-0089/vectors/blind_challenge_gen_vectors.json b/bip-0089/vectors/blind_challenge_gen_vectors.json new file mode 100644 index 0000000000..e25b0281fc --- /dev/null +++ b/bip-0089/vectors/blind_challenge_gen_vectors.json @@ -0,0 +1,51 @@ +{ + "test_cases": [ + { + "rand": "92950940B9C21B956D2950EA4C2CBD966D5DCF32517D2419636C3B434E7E7243", + "msg": "33DF4B220B36836C25198D4AFCFD25D1EE2E7B237C3021D7A0EDBA137E70958C", + "blindpubnonce": "02866A953BB982D4755FC9DCF0E09CC8EA56E2F75040DCAFE0C17A2A6FB5D4AC6E", + "pk": "0232D9E2657C0AA02A6E5AFF67175757832D1B3260A915970EA1CD95E2C9838B52", + "tweaks": ["7F91E8EA5D4FD39AAEB0FCDE90ABAAA8681D2610AF0FDDF132DEFBD5E1183580", "8F4ECAB71A22CDB15945BD2898DF005A8623B8DC50013F12700E678E92837406", "FD890EE6226ECA9EFB889DC1EC77B5D59FE0AF1D876C35F2CBE9F25F6B8FB760"], + "is_xonly": [true, true, false], + "extra_in": "FD8AA0C64B66C38EA627FABB0CFCCE5BB905D130470101ED88771E0A62331AC9", + + "expected_blindfactor": "545AB2AAB17406BE3270D0DFB7B13568F9ED5FAD5ABC5E9ACBAFC8D17131CC37", + "expected_challenge": "AC03DF1F1DA05BFD6E01E11BD7B95E3A6A0752BBB0E31EA26251675CECCE3A15", + "expected_pubnonce": "0367E34DAB4F1377CD8F3E7C5CD3E1E4A4D3B27BEAB9C0C0DC6717C9C52275D03B", + "expected_blindchallenge": "B5B3A3D63771818E930E55D3F91EBF11ED16BCDB11E0F1B5DF06F636F870DFB5", + "expected_pk_parity": true, + "expected_nonce_parity": false + } + ], + "error_test_cases": [ + { + "rand": "2B01EE16681AE0C2D8845C5F1D3F05F92453E95E7AC053DD5CABC736322B6CA3", + "msg": "6C22FC98FEEB69347A04BDE44B99FA50428689608E63B307D9F5904F86FE0B28", + "blindpubnonce": "02D9F53C5816BD205B8208A11491530CD6BD1EC35FFA31F026AD3444EFEA329440", + "pk": "03E9EBFEEAF165FBA6CD394EB1DBD514AE45CE8EA0AE56D54C8B5D7931D79FFBAF", + "tweaks": ["E3DD85653AAFDF2D94312FB8133D6B7E12DFC94B1B82A4E98D85E69D6F2F179A"], + "is_xonly": [true, false], + "extra_in": "C8BB4B046334864F71173C39BDE2A305289AA1AB5C0E0C624EC2D30A0A182310", + + "error": { + "type": "ValueError", + "message": "The tweaks and is_xonly arrays must have the same length." + }, + "comment": "mismatched arrays" + }, + { + "rand": "A8F932BD0BAC6F31824002482A42493B7AA1CAC2814D80D470A716D47ADCDF86", + "msg": "1776037E19AA1A2BF2C9DB770CA12A5AB683E2D7B436090BAC8CE48CB22582E0", + "blindpubnonce": "04411898DF38979F1DA000CEFF9166EE258AB6B0F696B8537F90E551751AA3C6F2", + "pk": "0333438C1C269BD73BADE95C62EDA258F74B093DA359DEDBF990E923CEC95BD6A4", + "tweaks": [], + "is_xonly": [], + "extra_in": null, + + "error": { + "type": "ValueError" + }, + "comment": "invalid blindpubnonce encoding" + } + ] +} diff --git a/bip-0089/vectors/blind_nonce_gen_vectors.json b/bip-0089/vectors/blind_nonce_gen_vectors.json new file mode 100644 index 0000000000..64810a98d9 --- /dev/null +++ b/bip-0089/vectors/blind_nonce_gen_vectors.json @@ -0,0 +1,22 @@ +{ + "test_cases": [ + { + "rand_": "0F6166D1645791EAD551572348A43CA9293E02CF0ED32B17EA5E1AEC6BC41931", + "sk": "F22F1B584D8B5CE15ED8F561DAD077B3FB743E6AABB97DBA758AFD88852DB490", + "pk": "0204B445C4EF4E822DA5842965BC03CBDC865EF846774FD27ACDE063F40CD7812C", + "extra_in": "887BEFE686260D09F471715719B7CB2D48E4116BD346319D9C002A4FC9D82857", + "expected_blindsecnonce": "A4B954BBCB05059CF0ACE8BC2C82BEA5ABD0D2C39B03D7A7205DB41E9BE9CA610204B445C4EF4E822DA5842965BC03CBDC865EF846774FD27ACDE063F40CD7812C", + "expected_blindpubnonce": "0355A32C1B472EE1874924CD9A1BF2536D6A2B214413684FBDFC5B84870EFDCEF8", + "comment": "All params present" + }, + { + "rand_": "D4B20323E12CEC7E21B41A4FD2395844F93D4B3E9F3FED13CF3234C32702A242", + "sk": null, + "pk": null, + "extra_in": null, + "expected_blindsecnonce": "78ACDD864846BB5C18017A421E792CC771D63EDA6B63A6CDC3825F298CAC7788", + "expected_blindpubnonce": "025CA329F7676AECEAC10C29566D9C7883A661DB2574454AE491476EADEE3CD430", + "comment": "Every optional parameter is absent" + } + ] +} diff --git a/bip-0089/vectors/blind_sign_and_verify_vectors.json b/bip-0089/vectors/blind_sign_and_verify_vectors.json new file mode 100644 index 0000000000..c1c131f657 --- /dev/null +++ b/bip-0089/vectors/blind_sign_and_verify_vectors.json @@ -0,0 +1,76 @@ +{ + "valid_test_cases": [ + { + "sk": "E4E64DB308215A81F1F41969624B9A6265D50F479BA6789E40190027AC6C72A8", + "pk": "03E812BE6ED9A2B180FA21B682D5FB35158A9542399D389B736AEDC930CAED04AA", + "blindsecnonce": "D05EC853CBCFC49EAEB5DF5AED030C880C1FB59414AD4ECC3D0E5C50CD7B906803E812BE6ED9A2B180FA21B682D5FB35158A9542399D389B736AEDC930CAED04AA", + "blindpubnonce": "03E97BD8C531CB0B40AC13857BCDCA6E9FF33889148BA5C9C02E0BE93D79560186", + "blindchallenge": "64FD1082FA5E7C5BF1267A5AB5BC3F4BD41167427E4D4A4166876709857E92EB", + "pk_parity": true, + "nonce_parity": false, + + "expected": { + "blindsignature": "8632B771A6A923FF1561B3513C4841F2D88795B05D99BC581ABCA201EED86EC5" + }, + + "checks": { + "verify_returns_true": true, + "secnonce_prefix_zeroed_after_sign": true, + "second_call_raises_valueerror": true + } + } + ], + + "sign_error_test_cases": [ + { + "sk": "5D2E5F8FD68D31B28F14334CA3E2DF8B85C2F31DBBD5C3E583DBFF90E2024286", + "blindsecnonce": "EDBA15E0F013E5323F22998F324B5ABF75D8FEB5EF4FD4BBD7B706B057BF1F08036E3F9DB8CD5E6461E8C23F80F4A67F7006011A1AE3DBDD863213E73D1534D5DC", + "blindchallenge": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "pk_parity": false, + "nonce_parity": true, + + "error": { "type": "ValueError" }, + "repeat": 1, + "comment": "e' out of range" + }, + { + "sk": "8C3975176DD4A9A2CFDFBBF50243C29E6C889D3867BE5D3C3BEBCD00B1BC6469", + "blindsecnonce": "E1B7C8E2750577A638D26BCABE96F66C7AE5DCCC6BF429E167686CC1BCDC07AF037C1AAEF6EEDEA6DBB123DC76D8C4AF9210E33EB26D7BBA95123680E0632F7F65", + "blindchallenge": "93EF4DEE1C3EC61665D94448715FC756363FC775A10B6CBB158B089404E3CB1E", + "pk_parity": true, + "nonce_parity": false, + + "error": { "type": "ValueError" }, + "repeat": 2, + "comment": "nonce reuse: second call must raise" + } + ], + + "verify_fail_test_cases": [ + { + "pk": "03E812BE6ED9A2B180FA21B682D5FB35158A9542399D389B736AEDC930CAED04AA", + "blindpubnonce": "03E97BD8C531CB0B40AC13857BCDCA6E9FF33889148BA5C9C02E0BE93D79560186", + "blindchallenge": "64FD1082FA5E7C5BF1267A5AB5BC3F4BD41167427E4D4A4166876709857E92EB", + "blindsignature": "9632B771A6A923FF1561B3513C4841F2D88795B05D99BC581ABCA201EED86EC5", + "pk_parity": true, + "nonce_parity": false, + + "expected_valid": false, + "comment": "Verify should return False (no exception)" + } + ], + + "verify_error_test_cases": [ + { + "pk": "03E812BE6ED9A2B180FA21B682D5FB35158A9542399D389B736AEDC930CAED04AA", + "blindpubnonce": "04E97BD8C531CB0B40AC13857BCDCA6E9FF33889148BA5C9C02E0BE93D79560186", + "blindchallenge": "64FD1082FA5E7C5BF1267A5AB5BC3F4BD41167427E4D4A4166876709857E92EB", + "blindsignature": "8632B771A6A923FF1561B3513C4841F2D88795B05D99BC581ABCA201EED86EC5", + "pk_parity": true, + "nonce_parity": false, + + "error": { "type": "ValueError" }, + "comment": "Bad blindpubnonce encoding" + } + ] +} diff --git a/bip-0089/vectors/change_output_verification_vectors.json b/bip-0089/vectors/change_output_verification_vectors.json new file mode 100644 index 0000000000..0159af4ce5 --- /dev/null +++ b/bip-0089/vectors/change_output_verification_vectors.json @@ -0,0 +1,43 @@ +{ + "test_cases": [ + { + "comment": "Change output verification 2-of-3 (path [1,5])", + "expected": true, + "tweak_map": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923c9": "ee665bd369e95c42180fc3e4a504ce4f19173deb6ee7ed1b2c05df7d37d8ed1e", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "2102aa7f5b2acf81e86d9fa841acdfe8e08d1faa800a318679ad7423dc615a2b", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "96c3196aaa0af9b79148d58ff2f58dc7291d4007202722602ddbd29e6cd6c018" + }, + "witness_script": "52210202573f6f0cd23e1d68894ddf5a50f65970833b75d7c1d5b862cbe17166d48850210206df37b85a2393162f1efd561297c37165dc7d8958ab4c5553ddf2e08108784d21037579ad42e47027db0734e66894863f31287b663695f643eb655873baf761a20453ae" + }, + { + "comment": "Witness script mismatch", + "expected": false, + "tweaks": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923c9": "ee665bd369e95c42180fc3e4a504ce4f19173deb6ee7ed1b2c05df7d37d8ed1e", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "2102aa7f5b2acf81e86d9fa841acdfe8e08d1faa800a318679ad7423dc615a2b", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "96c3196aaa0af9b79148d58ff2f58dc7291d4007202722602ddbd29e6cd6c018" + }, + "witness_script": "52210202573f6f0cd23e1d68894ddf5a50f65970833b75d7c1d5b862cbe17166d48850210206df37b85a2393162f1efd561297c37165dc7d8958ab4c5553ddf2e08108784d21037579ad42e47027db0734e66894863f31287b663695f643eb655873baf761a20453af" + }, + { + "comment": "Missing participant tweak", + "expected": false, + "tweaks": { + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "2102aa7f5b2acf81e86d9fa841acdfe8e08d1faa800a318679ad7423dc615a2b", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "96c3196aaa0af9b79148d58ff2f58dc7291d4007202722602ddbd29e6cd6c018" + }, + "witness_script": "52210202573f6f0cd23e1d68894ddf5a50f65970833b75d7c1d5b862cbe17166d48850210206df37b85a2393162f1efd561297c37165dc7d8958ab4c5553ddf2e08108784d21037579ad42e47027db0734e66894863f31287b663695f643eb655873baf761a20453ae" + }, + { + "comment": "Invalid base key length in tweak map", + "expected": false, + "tweaks": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923": "ee665bd369e95c42180fc3e4a504ce4f19173deb6ee7ed1b2c05df7d37d8ed1e", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "2102aa7f5b2acf81e86d9fa841acdfe8e08d1faa800a318679ad7423dc615a2b", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "96c3196aaa0af9b79148d58ff2f58dc7291d4007202722602ddbd29e6cd6c018" + }, + "witness_script": "52210202573f6f0cd23e1d68894ddf5a50f65970833b75d7c1d5b862cbe17166d48850210206df37b85a2393162f1efd561297c37165dc7d8958ab4c5553ddf2e08108784d21037579ad42e47027db0734e66894863f31287b663695f643eb655873baf761a20453ae" + } + ] +} diff --git a/bip-0089/vectors/compute_bip32_tweak_vectors.json b/bip-0089/vectors/compute_bip32_tweak_vectors.json new file mode 100644 index 0000000000..959d1a750c --- /dev/null +++ b/bip-0089/vectors/compute_bip32_tweak_vectors.json @@ -0,0 +1,32 @@ +{ + "xpub": { + "compressed": "0296928602758150d2b4a8a253451b887625b94ab0a91f801f1408cb33b9cf0f83", + "chain_code": "433cf1154e61c4eb9793488880f8a795a3a72052ad14a7367852542425609640", + "depth": 0, + "parent_fingerprint": "71348c8a", + "child_number": 0 + }, + "valid_test_cases": [ + { + "comment": "Delegatee tweak aggregation for a two-step path", + "path": ["0", "1"], + "expected": { + "tweak": "d81d8e239630639ac24f3976257d9e4d905272b3da3a6507841c1ec80b04b91b", + "derived_xpub": { + "compressed": "03636eb334a6ffdfc4b975a61dae12f49e7f94461690fa4688632db8eed5601b03", + "chain_code": "299bc0ad44ab883a5be9601918badd2720c86c48a6d8b9d17e1ae1c3b0ad975d" + } + } + } + ], + "error_test_cases": [ + { + "comment": "Hardened path should raise an error", + "path": ["0", "2147483648"], + "error": { + "type": "value", + "message": "Hardened derivations are not supported for delegates" + } + } + ] +} diff --git a/bip-0089/vectors/delegator_sign_vectors.json b/bip-0089/vectors/delegator_sign_vectors.json new file mode 100644 index 0000000000..f11e6b2aa4 --- /dev/null +++ b/bip-0089/vectors/delegator_sign_vectors.json @@ -0,0 +1,13 @@ +{ + "test_cases": [ + { + "comment": "Delegator signing with provided CCD tweak over arbitrary message", + "base_secret": "9303c68c414a6208dbc0329181dd640b135e669647ad7dcb2f09870c54b26ed9", + "tweak": "d81d8e239630639ac24f3976257d9e4d905272b3da3a6507841c1ec80b04b91b", + "message": "Chain Code Delegation", + "expected": { + "signature": "2f558d1519106f6cffdcfce09954c6ae328b98308718a0903e3efed103b457cd563c315fe6c6b5ffe6f71f413ce68ba22ee793238ab73fd2cef9d5881ae80017" + } + } + ] +} diff --git a/bip-0089/vectors/input_verification_vectors.json b/bip-0089/vectors/input_verification_vectors.json new file mode 100644 index 0000000000..5275f38772 --- /dev/null +++ b/bip-0089/vectors/input_verification_vectors.json @@ -0,0 +1,43 @@ +{ + "test_cases": [ + { + "comment": "Input verification for wsh(sortedmulti) 2-of-3 (path [0,5])", + "expected": true, + "tweak_map": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923c9": "6e4dd29833f7b88751dad6ea6ff536959122f2d07074006657d0e2ef26af3ef6", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "b30d8530e3464dc71ed6e20897ef5c3c9d1149ecc11f332336520addab1454f3", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "c1efff9fb89227d09e54b403ae269f1991003e964f66f412e8302f8bb1c71644" + }, + "witness_script": "5221034ebf1d6b674fbf3d7ff09e4bc44b23e17745188b4aac3e2e101bd210cd8f3ed42103a0d8aed25b77d286d7bf7a668b452f18def89f2e2285acd315fc00668fe0a70b2103bd4632ebd0de4573710722bf73b4bbb76713734c4756b830302b8492f29a6aae53ae" + }, + { + "comment": "Witness script mismatch", + "expected": false, + "tweak_map": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923c9": "6e4dd29833f7b88751dad6ea6ff536959122f2d07074006657d0e2ef26af3ef6", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "b30d8530e3464dc71ed6e20897ef5c3c9d1149ecc11f332336520addab1454f3", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "c1efff9fb89227d09e54b403ae269f1991003e964f66f412e8302f8bb1c71644" + }, + "witness_script": "5221034ebf1d6b674fbf3d7ff09e4bc44b23e17745188b4aac3e2e101bd210cd8f3ed42103a0d8aed25b77d286d7bf7a668b452f18def89f2e2285acd315fc00668fe0a70b2103bd4632ebd0de4573710722bf73b4bbb76713734c4756b830302b8492f29a6aae53af" + }, + { + "comment": "Missing participant tweak", + "expected": false, + "tweak_map": { + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "b30d8530e3464dc71ed6e20897ef5c3c9d1149ecc11f332336520addab1454f3", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "c1efff9fb89227d09e54b403ae269f1991003e964f66f412e8302f8bb1c71644" + }, + "witness_script": "5221034ebf1d6b674fbf3d7ff09e4bc44b23e17745188b4aac3e2e101bd210cd8f3ed42103a0d8aed25b77d286d7bf7a668b452f18def89f2e2285acd315fc00668fe0a70b2103bd4632ebd0de4573710722bf73b4bbb76713734c4756b830302b8492f29a6aae53ae" + }, + { + "comment": "Invalid base key length in tweak map", + "expected": false, + "tweak_map": { + "02a047233eec59cf06b9a5ee62d9088eeb8127201423f88637443ff7ee591923": "6e4dd29833f7b88751dad6ea6ff536959122f2d07074006657d0e2ef26af3ef6", + "0386623c88ed79ef5d9aacd24f227a0cd845f5840b861a25118c1200cccd046e0f": "b30d8530e3464dc71ed6e20897ef5c3c9d1149ecc11f332336520addab1454f3", + "03c3c01af1d84ec032f7f8d6decd48d74cbbd62253e12691debd064e8b41cb0945": "c1efff9fb89227d09e54b403ae269f1991003e964f66f412e8302f8bb1c71644" + }, + "witness_script": "5221034ebf1d6b674fbf3d7ff09e4bc44b23e17745188b4aac3e2e101bd210cd8f3ed42103a0d8aed25b77d286d7bf7a668b452f18def89f2e2285acd315fc00668fe0a70b2103bd4632ebd0de4573710722bf73b4bbb76713734c4756b830302b8492f29a6aae53ae" + } + ] +} diff --git a/bip-0089/vectors/unblind_signature_vectors.json b/bip-0089/vectors/unblind_signature_vectors.json new file mode 100644 index 0000000000..77a52c2d4e --- /dev/null +++ b/bip-0089/vectors/unblind_signature_vectors.json @@ -0,0 +1,63 @@ +{ + "valid_test_cases": [ + { + "session_ctx": { + "pk": "03A1B69A6C047657AA6A0DF9ED43E5B0CA75097260F065048606D0946B2B89A6AD", + "blindfactor": "D08134A1CA8F716EE99EE69179BD939CF2DCD29D3EB1827124BAEB1364088AA9", + "challenge": "0AB1D307369FB4D994A8DEDE3D503FDC7B8AF459AECE3C69B5C22F5BFA293618", + "pubnonce": "02ED7E7EB4E886F9A9DF4E375F5F9321DCF5AA909B85A028B7EBB14F2ED80AE3BD", + "tweaks": ["1956DF466B657FFA287B6BFC63219BB6BF3D5A72ECE44E43E14091CBF15100BB", "2CB93A737A3B9A86D678DD8060ECA5443978B87BA54CFC21AE1341B47C2640B9"], + "is_xonly": [false, true] + }, + "msg": "28431125D79E16223AAF5401267447B8729324613B74A3A1DFD4EE8E277B5C40", + "blindsignature": "6180428458B0EDA605A2D897A45784C399D310060FD0BE701DA4AE5B2EEB7A40", + + "expected_bip340_sig": "ED7E7EB4E886F9A9DF4E375F5F9321DCF5AA909B85A028B7EBB14F2ED80AE3BD1A606D2DE092BD1A05B82532BDEA7F11493D00EB1109CF1EF30A8D8E2FF2721C" + } + ], + + "error_test_cases": [ + { + "session_ctx": { + "pk": "03A1B69A6C047657AA6A0DF9ED43E5B0CA75097260F065048606D0946B2B89A6AD", + "blindfactor": "D08134A1CA8F716EE99EE69179BD939CF2DCD29D3EB1827124BAEB1364088AA9", + "challenge": "0AB1D307369FB4D994A8DEDE3D503FDC7B8AF459AECE3C69B5C22F5BFA293618", + "pubnonce": "04ED7E7EB4E886F9A9DF4E375F5F9321DCF5AA909B85A028B7EBB14F2ED80AE3BD", + "tweaks": ["1956DF466B657FFA287B6BFC63219BB6BF3D5A72ECE44E43E14091CBF15100BB", "2CB93A737A3B9A86D678DD8060ECA5443978B87BA54CFC21AE1341B47C2640B9"], + "is_xonly": [false, true] + }, + "msg": "28431125D79E16223AAF5401267447B8729324613B74A3A1DFD4EE8E277B5C40", + "blindsignature": "6180428458B0EDA605A2D897A45784C399D310060FD0BE701DA4AE5B2EEB7A40", + "error": { "type": "ValueError" }, + "comment": "Bad pubnonce encoding" + }, + { + "session_ctx": { + "pk": "03A1B69A6C047657AA6A0DF9ED43E5B0CA75097260F065048606D0946B2B89A6AD", + "blindfactor": "D08134A1CA8F716EE99EE69179BD939CF2DCD29D3EB1827124BAEB1364088AA9", + "challenge": "0AB1D307369FB4D994A8DEDE3D503FDC7B8AF459AECE3C69B5C22F5BFA293618", + "pubnonce": "04ED7E7EB4E886F9A9DF4E375F5F9321DCF5AA909B85A028B7EBB14F2ED80AE3BD", + "tweaks": ["1956DF466B657FFA287B6BFC63219BB6BF3D5A72ECE44E43E14091CBF15100BB", "2CB93A737A3B9A86D678DD8060ECA5443978B87BA54CFC21AE1341B47C2640B9"], + "is_xonly": [true] + }, + "msg": "28431125D79E16223AAF5401267447B8729324613B74A3A1DFD4EE8E277B5C40", + "blindsignature": "6180428458B0EDA605A2D897A45784C399D310060FD0BE701DA4AE5B2EEB7A40", + "error": { "type": "ValueError", "message": "must have the same length" }, + "comment": "tweaks/is_xonly length mismatch" + }, + { + "session_ctx": { + "pk": "03A1B69A6C047657AA6A0DF9ED43E5B0CA75097260F065048606D0946B2B89A6AD", + "blindfactor": "D08134A1CA8F716EE99EE69179BD939CF2DCD29D3EB1827124BAEB1364088AA9", + "challenge": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "pubnonce": "04ED7E7EB4E886F9A9DF4E375F5F9321DCF5AA909B85A028B7EBB14F2ED80AE3BD", + "tweaks": ["1956DF466B657FFA287B6BFC63219BB6BF3D5A72ECE44E43E14091CBF15100BB", "2CB93A737A3B9A86D678DD8060ECA5443978B87BA54CFC21AE1341B47C2640B9"], + "is_xonly": [true] + }, + "msg": "28431125D79E16223AAF5401267447B8729324613B74A3A1DFD4EE8E277B5C40", + "blindsignature": "6180428458B0EDA605A2D897A45784C399D310060FD0BE701DA4AE5B2EEB7A40", + "error": { "type": "ValueError" }, + "comment": "challenge out of range" + } + ] +} diff --git a/bip-0110.mediawiki b/bip-0110.mediawiki new file mode 100644 index 0000000000..5bc6dea841 --- /dev/null +++ b/bip-0110.mediawiki @@ -0,0 +1,353 @@ +
+  BIP: 110
+  Layer: Consensus (soft fork)
+  Title: Reduced Data Temporary Softfork
+  Authors: Dathon Ohm 
+  Status: Draft
+  Type: Specification
+  Assigned: 2025-12-03
+  License: BSD-3-Clause
+  Discussion: https://groups.google.com/g/bitcoindev/c/nOZim6FbuF8
+
+ +==Abstract== + +Temporarily limit the size of data fields at the consensus level, in order to correct distorted incentives caused by standardizing support for arbitrary data, and to refocus priorities on improving Bitcoin as money. + +==Copyright== + +This document is licensed under the 3-clause BSD license. + +==Specification== + +Blocks during a temporary, one-year deployment are checked with these additional rules: + +# New output scriptPubKeys exceeding 34 bytes are invalid, unless the first opcode is OP_RETURN, in which case up to 83 bytes are valid. +# OP_PUSHDATA* payloads and [[#script-argument-witness-items|script argument witness items]] exceeding 256 bytes are invalid, except for the redeemScript push in BIP16 scriptSigs. +# Spending undefined witness (or Tapleaf) versions (ie, not Witness v0/BIP 141, Taproot/BIP 341, or P2A) is invalid. (Creating outputs with undefined witness versions is still valid.) +# Witness stacks with a Taproot annex are invalid. +# Taproot control blocks larger than 257 bytes (a merkle tree with 128 script leaves) are invalid. +# Tapscripts including OP_SUCCESS* opcodes anywhere (even unexecuted) are invalid. +# Tapscripts executing the OP_IF or OP_NOTIF instruction (regardless of result) are invalid. + +Inputs spending UTXOs that were created before the activation height are exempt from the new rules. +Once the softfork expires, UTXOs of all heights are once again unrestricted. + +===GetBlockTemplate=== + +This deployment uses the GBT deployment name "reduced_data" as defined in [[bip-0009.mediawiki#getblocktemplate_changes|BIP 9]]. During mandatory signaling, the deployment's bit is included in "vbrequired". + +==Motivation== + +In order to protect Bitcoin's intended function as the Internet's native money, the Bitcoin community has historically treated techniques for embedding arbitrary data into Bitcoin transactions with antagonism. + +Such data embedding must be resisted at all times in order to ensure it doesn't become load-bearing and start to produce negative externalities, especially for node operators. + +Starting with the "inscription" hack first exploited in 2022, a trend has emerged around embedding arbitrary data into Bitcoin transactions, creating significant unnecessary burdens on node operators and diverting development focus and incentives away from Bitcoin's fundamental purpose of being sound, permissionless, borderless money. + +This BIP aims to set Bitcoin back on the path to becoming the world's money by rejecting the standardization of data storage as a supported use case at the consensus level. + +It achieves this by temporarily invalidating several of the most harmful methods of data abuse by consensus, while preserving all known monetary use cases. + +Specifically, this proposal invalidates all methods of embedding contiguous arbitrary data larger than 256 bytes; it also invalidates large scriptPubKey and Tapleaf formats that are abused almost exclusively for data embedding; and finally, it restores, in consensus, the long-established 83-byte policy limit on OP_RETURN outputs. + +In software development, it is a common practice to disable unsupported use cases, because not doing so quickly becomes unsustainable and causes development to grind to a standstill. + +Activation of these new rules thus sends a clear message that arbitrary data storage will continue to be actively resisted, and that such unsupported usage should not be permitted to derail network priorities. + +It also reinforces Bitcoin's core function as a censorship-resistant payment system, because data storage competes unfairly with payments, making Bitcoin payments unnecessarily costly. This encourages reliance on third-party payment processors, making Bitcoin payments easier to censor. + +Finally, it improves decentralization of the node network by re-establishing the long-held commitment towards minimizing the cost (financial or otherwise) of operating a node. + +Bitcoin should "do one thing, and do it well". By rejecting data storage, this BIP liberates Bitcoin developers from endless scope creep, enabling them to focus on what's really important: Bitcoin's success as money. + +==Rationale== + +===Specification nuance=== + +'''Why limit scriptPubKeys to 34 bytes?''' + +Unspent scriptPubKeys (OP_RETURN excepted) must be stored indefinitely in the UTXO set, which nodes must store on fast media and cannot prune. +Fast storage (usually RAM) is much more costly per byte than slower, non-volatile storage, so as the UTXO set size increases, the burden on node operators increases, harming decentralization. +They are also a direct cost to the sender rather than the receiver. +For these reasons, modern usage is all 34 bytes or smaller in practice; actual spending conditions have been moved to the witness, and the scriptPubKey simply commits to them in advance with a hash. +Furthermore, large scriptPubKeys, in addition to being a data embedding vector, can be abused to create malicious transactions and blocks ("poison blocks") that take a long time to validate. +Large scriptPubKeys, thus carrying a large abuse potential and no benefit, are invalidated by this BIP. + +'''What about OP_RETURN? Why not get rid of it entirely?''' + +OP_RETURN outputs are provably unspendable, and nodes do not need to store them in the UTXO set. +Historically, up to 83 bytes have been tolerated only to avoid unprovably unspendable spam in other output scripts, and with the possible exception of commitment schemes that use OP_RETURN in coinbase transaction outputs (notably Segwit), using OP_RETURN is not the optimal solution to any known use case. +With the advent of pay-to-contract and Taproot, it is now also possible to commit to external data in the Taptree, making even hypothetical use of OP_RETURN deprecated. +However, to avoid breaking legacy protocols that still include such outputs, this proposal allows these outputs. + +'''Why limit other data to 256/257 bytes?''' + +With modern compression, it is plausible to represent images in as few as 300-400 bytes. Images are likely the most harmful use case for data storage, as they have huge demand and supporting them can engender high fees and UTXO-set bloat, as well as content that a large majority of node operators might object to. + +256 bytes (2048 bits) is also more than sufficient for reasonably large numbers that might be potentially needed in legitimate cryptography, reinforcing Bitcoin's intended purpose as a monetary network. + +'''Won't spammers just spread their data over multiple fields?''' + +While it is impossible to fully prevent steganography, limiting data sizes ensures such abuses are non-contiguous and obfuscated within another intended meaning (script code, structure, etc). +As far as Bitcoin is concerned, the data has some meaning other than the spammers' misinterpretation, and any external code to "reassemble" the unintended data is responsible for producing it +(it is possible to write code that transforms *any* data into any other data - what matters is that Bitcoin has a well-defined meaning that is distinct from the unsupported one). +Requiring users to divide their files into chunks of at most 256 bytes, raising the cost both in fees and in effort, sends a clear message that data storage abuses in general are unwelcome rather than sanctioned or supported. + + +'''What are "script argument witness items", referred to in rule 2?''' + +Script argument witness items are the witness stack elements that are placed on the script interpreter's stack as inputs before the script is executed - the witness equivalent of data pushes in scriptSigs. + +The following witness stack elements are '''not''' script argument witness items, and are thus excluded from rule 2 (though some are limited/invalidated by other rules): + +* Some items are part of a spend that invokes the script interpreter, but are popped from the witness stack before the remaining elements are passed to it as inputs: +** Witness scripts and Tapleaf scripts (not limited) +** Control blocks (limited by rule 5) +** Annexes (invalidated by rule 4) +* Others are not part of a script-path spend at all, so the script interpreter is not invoked: +** Taproot key path signatures (limited to exactly 64 or 65 bytes by BIP 341 signature validation) +** Witness stacks in spends of undefined witness versions (invalidated by rule 3) + +All '''other''' witness stack elements are script argument witness items, and are thus limited to 256 bytes by rule 2. + +'''Why are BIP16 redeemScripts, witness scripts, and Tapleaf scripts exempt from rule 2?''' + +BIP16 redeemScripts, witness scripts, and Tapleaf scripts are scripts, not data. Their contents are subject to the same OP_PUSHDATA* restrictions during execution. +Restricting them is not only unnecessary, but would reduce the ability to make use of the intended script capabilities, and could impact legitimate real-world usage. + +'''Why make spending undefined witness/Tapleaf versions invalid?''' + +Since they are undefined, witness stacks spending these versions are completely unlimited currently to allow maximum flexibility in future upgrades. +Any future upgrade, however, would need more than a year of coordination, so this softfork will not actually restrict it, and only safeguards against abuse in the meantime. + +'''Why not make it invalid to send to undefined witness versions?''' + +This would require the senders of transactions to check the witness version prior to sending, and require additional coordination when a new witness version is intended to become used. + +'''Why not allow spending undefined witness versions with an empty witness?''' + +This has no use case, but would require nodes to track these UTXOs in case of potential spending. +By making spending invalid, it is possible for nodes to store them instead in slow memory not needed until this softfork expires. +(With proper planning, it also makes it possible for a future softfork making use of these witness versions to allow users to receive with an upgraded wallet even prior to activation of the upgrade.) + +'''Why make the Taproot annex invalid?''' + +The annex is currently undefined data with unlimited size. +It exists for future upgrades, but has no legitimate usage today. +Any future upgrade, however, would need more than a year of coordination, so this softfork will not actually restrict it, and only safeguards against abuse in the meantime. + +'''Why is the Taproot control block limited to 257 bytes instead of 256?''' + +The control block is a series of hashes proving the Tapscript is part of the Taptree, plus a single byte with the leaf version and parity bit. +See BIP 341 for details. + +'''Why make OP_SUCCESS* invalid?''' + +OP_SUCCESS* is meant for future upgrades. See above regarding undefined witness versions. + +'''Why make OP_IF/OP_NOTIF invalid?''' + +OP_IF/OP_NOTIF originated in pre-Taproot Bitcoin script language as a way to execute different subscripts based on a condition. +With Taproot, the conditions can instead be evaluated off-chain, revealing only the intended verification execution path. +Furthermore, when the conditions are met, the intent is that the keypath spend path should be used instead, avoiding publishing any scripts at all. + +OP_IF is not only redundant for Tapscript, it is also commonly abused today to inject spam that gets skipped at execution. +While it is impossible to fully prevent steganography, closing this gap eliminates one common abuse today basically for free, and sends a message that such abuses are not welcome. +There are some potential experimental use cases for OP_IF in Tapscript. See the Tradeoffs section for more details. + +'''Why is the proposal so simple?''' + +A more complicated proposal could be envisioned that better balances innovation with safety, but implementing this properly would require extensive refactoring and review, delaying deployment when the change is already urgent. +The rules proposed herein have been intentionally kept very simple to minimise review time and avoid unnecessary risks of overlooking unexpected side effects. + +'''Why is this softfork temporary?''' + +The impact of these restrictions would severely constrain future upgrades, potentially forcing them to be designed as a hardfork instead of a softfork. +Some restrictions are also not ideal, but an improved limit would be more complicated to develop and test - +by deploying these simpler restrictions now, we avoid making the perfect the enemy of the good enough, while still allowing for upgrading the limits to better variants in the future. + +Over the next year, interested developers can implement and propose a longer-term solution to address the needs of the protocol without the tradeoffs or blunt/simplified changes. + +===Tradeoffs=== + +'''Are there any tradeoffs?''' + +Yes: + +# Limiting Taproot control blocks to 257 bytes directly constrains the size of the on-chain, consensus-enforced script tree. This could complicate or possibly even impede advanced smart contracting like BitVM, which relies on a large number of executable scripts. In the worst case scenario, these use cases may just need to wait until this softfork expires. As they are still in early development, testnet and sidechains should be sufficient for the next year while a more scalable rule is implemented. +# Upgrade hooks are not available for other softforks. As softforks adding new opcodes typically need at least a year to activate, this shouldn't be a practical issue. +# Some wallet software such as Miniscript habitually creates Tapleaves containing OP_IF. To mitigate the risk of these funds being frozen for a year, this proposal exempts inputs that spend outputs that were created before activation, and provides a two-week grace period between lock-in and activation, to give users time to prepare. + +'''Isn't the limit on Taproot control blocks too restrictive?''' + +Possibly. +The previous limit allows for 340,282,366,920,938,463,463,374,607,431,768,211,456 scripts, which is obviously way more than anyone could ever need. +257 bytes allows for 128 scripts, which is sufficient for modern and complex transactions. +However, it may prove too limiting for advanced off-chain functionality such as used by BitVM. +This is an unfortunate tradeoff that (if this softfork is accepted) we have chosen to accept in the short-term for the immediate benefits of this softfork. +The intent is to relax this restriction later, when this softfork expires, with a new approach allowing larger trees, yet to be developed. + +Do note that non-script (or non-Bitcoin-L1 scripts) usage of the taptree does not have this same limitation: +just a single of the 128 leaves could very well be an extension of the merkle tree to greater depths than enforced by this softfork. + +'''Aren't Taptrees intended to be unbalanced?''' + +While it is true that optimal use of Taptrees may often be unbalanced to favour more-likely-executed scripts, this is optional, and the full capacity (in this case, 128 scripts) can still be used if needed. +Additionally, in ideal/ordinary circumstances, neither the Taptree nor a merkle branch through it is ever published: +all counterparties ought to evaluate the conditions for spending off-chain and rebroadcast the transaction using the keypath spending. +Tapscripts are designed to be used when one or more parties is unreachable or uncooperative; their existence mainly only serves to deter intentional non-cooperation by making it pointless. +An exception to this is protocols employing a NUMS point to restrict an output to only being spendable via the script path. + +'''Is there any risk of funds being frozen or lost?''' + +In theory, yes. This proposal goes to great pains to make sure it does not affect any known use cases, and it is reasonably certain that no one will be affected. However, there are a couple of experimental use cases involving pre-signed Taproot transactions that could end up being affected. + +Specifically: + +* The restriction on OP_IF/OP_NOTIF could temporarily invalidate some edge-case Tapleaves produced by the current version of the Miniscript compiler. There are no verified uses of these constructions currently in production, but there are some popular wallets that could, in theory, produce them. +* The restriction on Taptree depth will invalidate any Tapleaves deeper than 7 levels. Since Taptrees are usually designed with the more common spending conditions positioned higher in the tree, any funds encumbered by such a Taptree will almost certainly be easily spendable by Tapleaves higher up in the tree. + +In both scenarios, funds are spendable either by other Tapleaves in the tree, or by the keypath (unless the keypath is provably invalidated using a NUMS point). + +Funds in either scenario could end up being frozen or lost ''only'' if ''all'' of the following conditions are met: + +* The UTXO is pay-to-Taproot (P2TR); +* The UTXO is in a pre-signed transaction; +* The UTXO being spent ''must'' be confirmed ''and'' spent during the temporary, one-year deployment of these new rules; +* The Tapleaf the user selects to spend the UTXO contains an OP_IF/OP_NOTIF or exists at a depth greater than 7; +* The keypath is unusable to spend the funds, AND there are no other suitable Tapleaves in the tree to spend the UTXO (in which case funds are frozen), OR there are other Tapleaves that ''can'' spend the UTXO in unexpected ways (in which case funds are lost). + +In other words, funds are completely unaffected if: + +* They do not use Taproot; +* They use Taproot in standard and well-supported ways; +* UTXOs needed during the temporary deployment are confirmed before the fork activates; +* UTXOs in pre-signed transactions do not lock funds using Tapleaves that violate the new rules; +* UTXOs in pre-signed transactions that lock funds using Tapleaves that violate the new rules do not need to be confirmed ''and'' spent during the deployment; +* UTXOs in pre-signed transactions that lock funds using Tapleaves that violate the new rules and need to be confirmed ''and'' spent during the deployment can be spent either via the keypath OR by other, expected Tapleaves in the tree. + +This proposal does everything possible to try to avoid funds being frozen or lost, but ultimately it is impossible to prove that absolutely no one will be affected. +It is therefore up to the Bitcoin community to activate these new rules only if they feel that rejecting data storage is worth this tradeoff. +To prepare for activation, it is recommended that users begin migrating any affected funds now. +In the event that these new rules are activated, there will be at least a two-week period between lock-in and activation, during which all users will have the chance to migrate any remaining funds. + + +===Alternatives / Alsos=== +'''Why not let the fee market manage data storage?''' + +The fee market is designed to prioritize transactions based on economic urgency. + +However, the market for data storage on the blockchain is a completely different market from the market for payments, with completely different incentives. + +Specifically, the fee for a monetary transaction incentivises a miner to include the transaction in a block, representing a one-time transfer of monetary value, i.e., a payment. The miner thus provides the one-time service of securing a payment, for a one-time fee. + +Once the payment is secured, the payor does not receive any additional benefit from the Bitcoin network, besides the integrity of Bitcoin's transaction history (a service to which all node operators are happy to contribute, because Bitcoin would not function as money otherwise). + +Conversely, the fee for a data storage transaction still goes only to the miner who includes the data in a block, but the burden of storing the data falls on all node operators, who never received even a part of the fee, yet are forced to continue downloading, storing, and serving the data forever. + +In this case, the miner accepts a one-time fee, and in exchange, the priceless service of highly-available, uncensorable data storage is provided in perpetuity ''for free'' by node operators. + +The problem becomes even worse when the data is objectionable to node operators, as this represents an even larger, unexpected cost for them. + +'''How about OP_RETURN2/"blobspace" making the data optional for nodes?''' + +This has been attempted multiple times in the past. +There is perhaps no harm in trying yet again, and this proposal does not prevent doing so, +but ultimately these schemes depend on the cooperation of the sender, who usually wants to explicitly force the content on non-consenting node operators +(or they would be using other existing distribution methods already). +These other ideas also do not solve the problem of objectionable content. + +'''Why not ban PUSHDATA opcodes/Eliminate the witness discount/Apply a length limit to the Annex rather than eliminating it/Add limits to overall witness or transaction sizes/Make the softfork permanent instead of temporary/Remove the witness discount/Make OP_RETURN cheaper?''' + +These are all interesting ideas, but they all increase the complexity of the implementation, and this proposal was optimized to be simple and easy to review for fast deployment. If the community decides to do any of these things, this proposal encourages them to do so once it expires. + +'''Why not eliminate one or more of the restrictions?''' + +This proposal, as is, represents the strongest possible rejection of the arbitrary data storage use case, while minimizing complexity. Loosening any of the rules would make it less effective at achieving this objective, for not much benefit. + +'''Shouldn't spam be fought in policy? Does this proposal affirm that policy is ineffective?''' + +It remains true that policy is still the best place to fight spam. +However, it is also true that policy cannot guarantee 100% effectiveness, particularly against bad actors who are mining. +This softfork minimises the impact of such malicious miners, closing the worst-case risks. + +'''Does this proposal solve spam completely?''' + +No. +It is impossible to solve spam completely, and typically spam is best fought with policy/filters, not consensus. +What this softfork does is require users wanting to store large unencrypted files in the blockchain to disguise the data as financial data and/or break it up into multiple data pushes. Obviously doing so is considered an abuse of bitcoin and should be avoided, but if it does happen, this BIP strengthens the argument that data storage is not a supported use case. + +'''Why doesn't this proposal address non-Bitcoin tokens?''' + +There are a wide variety of non-Bitcoin tokens, mostly scams, that a significant portion of the community considers spam. +However, these schemes are best countered in policy rather than consensus, and besides, this proposal does not aim to eliminate spam entirely. + +'''Is this a slippery slope? If we make rules against data today, will we start banning use cases we don't like tomorrow?''' + +No. +These rules may be new at the consensus level, but they are merely enshrining long-standing principles of Bitcoin, as necessary to address a threat to the decentralization of the network and its usability for monetary purposes. + +This softfork does not attempt to impose restrictions on monetary activity or the validity of monetary transactions themselves. +By restricting the data storage use case as much as possible, this proposal reinforces Bitcoin's guarantee of sound, permissionless money for the long-term. +This clear distinction between mitigating a systemic risk from non-monetary data abuse and interfering with actual monetary use cases provides a strong barrier against future overreach. + +The explicitly temporary nature of the softfork further reinforces that this is a targeted intervention to mitigate a specific crisis, not a commitment or proposal of a new direction of development. +If no further action is taken by you, it will expire in a year. +Even if a followup softfork is proposed for that time, you retain the right to reject it. + +'''Doesn't this proposal break user space?''' +Yes, this proposal intentionally breaks user space, specifically the data storage user space. This is necessary in order to communicate that data storage is not supported. +Pains have been taken to avoid breaking monetary use cases, and it is unlikely that any such use cases have been affected, but in theory they might be. See the Tradeoffs section for more details. + +==Backwards compatibility== + +There are a couple of very unlikely scenarios in which funds could theoretically be frozen or lost. See the Tradeoffs section for more details. + +If this proposal activates, the Miniscript compiler will need to be modified not to produce scripts that violate the new rules, at least while the new rules are active. + +Users storing arbitrary data in the Bitcoin blockchain should start looking for other places to store their data, such as Nostr, IPFS, BitTorrent, cloud storage, etc, as the Bitcoin network will not support this use case going forward. + +All other known use cases are not affected. + +==Reference implementation== + +https://github.com/bitcoinknots/bitcoin/compare/29.x-knots...dathonohm:bitcoin:uasf-modified-bip9 + +==Deployment== + +This deployment uses a modified version of BIP9 with the following parameters: + +* '''name''': reduced_data +* '''bit''': 4 +* '''starttime''': 1764547200 (~December 1, 2025) +* '''timeout''': NO_TIMEOUT +* '''min_activation_height''': 0 +* '''max_activation_height''': 965664 (~September 1, 2026) +* '''active_duration''': 52416 blocks (~1 year after activation) +* '''threshold''': 1109/2016 (55%) + +===Deviations from BIP9=== + +This deployment deviates from standard BIP9 in five ways: + +'''Reduced threshold (55% instead of 95%)''': The standard BIP9 threshold of 95% is designed for permanent consensus changes where near-universal miner readiness is desirable. Since rejecting data storage is a matter of urgency, and since this softfork is temporary and expires after one year, a lower threshold is ideal. + +'''No timeout, using max_activation_height instead''': Standard BIP9 uses a time-based timeout that transitions to FAILED if the threshold is not reached. This deployment sets timeout to NO_TIMEOUT and instead uses a BIP8-like, height-based max_activation_height. The deployment transitions to LOCKED_IN at height 963648 (one retarget period before max_activation_height), then to ACTIVE at height 965664. + +'''Mandatory signaling period''': Similar to BIP8, this deployment enforces mandatory signaling during the retarget period immediately before mandatory lock-in (blocks 961632 to 963647; lock-in happens no later than block 963648). During this window, blocks that do not signal bit 4 are rejected as invalid. Mandatory signaling ends once the deployment reaches the LOCKED_IN state. + +'''Expiry via active_duration''': Standard BIP9 deployments are permanent once ACTIVE. This proposal includes an active_duration of 52416 blocks (~1 year), making the deployment temporary rather than permanent. After active_duration blocks, the state machine transitions from ACTIVE to EXPIRED, a new terminal state. Once EXPIRED, the rules cease to be enforced and UTXOs are once again unrestricted. + +'''State transitions''': The state machine follows this progression: +# DEFINED: Initial state until starttime +# STARTED: After starttime; miners may signal with bit 4 +# LOCKED_IN: Entered at the start of the first retarget period where the threshold was reached in the previous period; mandatory signaling (blocks 961632-963647) ensures this happens no later than height 963648 +# ACTIVE: Entered one retarget period after LOCKED_IN; rules enforced for active_duration blocks +# EXPIRED: Entered when block height >= activation_height + active_duration; rules no longer enforced (terminal state) + +The FAILED state is never reached because timeout is disabled. EXPIRED is the final state; there are no transitions after it. Miner signaling after expiry has no effect because the deployment is no longer in the STARTED state, and miner signaling only governs the STARTED -> LOCKED_IN transition. + +==Credits== + +Original draft and advice: Luke-Jr diff --git a/bip-0117.mediawiki b/bip-0117.mediawiki index cea3a13cd5..b301083f38 100644 --- a/bip-0117.mediawiki +++ b/bip-0117.mediawiki @@ -192,3 +192,5 @@ The v0 segwit rules prohibit leaving anything on the stack, so for v0 parameters [5] [https://github.com/bitcoin/bips/blob/master/bip-0114.mediawiki BIP114: Merkelized Abstract Syntax Tree] [6] [https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki BIP68: Relative lock-time using consensus-enforced sequence numbers] + +[9] [https://github.com/bitcoin/bips/blob/master/bip-0008.mediawiki BIP8: Version bits with lock-in by height] diff --git a/bip-0128.mediawiki b/bip-0128.mediawiki new file mode 100644 index 0000000000..c89ef32032 --- /dev/null +++ b/bip-0128.mediawiki @@ -0,0 +1,275 @@ +
+  BIP: 128
+  Layer: Applications
+  Title: Timelock-Recovery Storage Format
+  Authors: Oren Z 
+  Status: Draft
+  Type: Specification
+  Assigned: 2026-02-05
+  License: BSD-2-Clause
+  Discussion: https://groups.google.com/g/bitcoindev/c/K1NpJp9_BYk
+
+ +== Abstract == + +This document proposes a standard format for saving timelock-recovery plans, to allow different +wallets to generate them, and different services to monitor/execute them. + +== Motivation == + +Pre-signed transactions are one way to create a recovery-plan, for use in case of seed loss or +inheritance. +The most common example is a single pre-signed transaction with an nLocktime set to a +future date, as explained in [[bip-0065.mediawiki|BIP-65]]. +One limitation of this approach is that in the happy-flow scenario, when the seed is not lost, +and the nLocktime is about to be reached, the user must access their wallet and spend +one of its UTXOs - in order to revoke the pre-signed transaction and prevent it from being able to +move the funds with no cancellation period. +This could be frustrating, for example, for users that split their seed over multiple geographic +locations. + +''Timelock-Recovery plans'' are a way to pre-sign a pair of transactions that eventually move the +funds to one or more secondary wallets - with a special nSequence relative-locktime +in the second transaction, so that the user always has a cancellation-period. + +Executing and monitoring a ''Timelock-Recovery plan'' thus requires more than broadcasting and +monitoring a single transaction. It also requires mechanisms for accelerating the first +transaction (which does not move most funds to the secondary wallet), for checking whether +the relative-timelock has passed, and a more nuanced handling of reorgs. + +This BIP proposes a standard format for exporting ''Timelock-Recovery plans'' from the wallet that +generated them, and importing them into apps/services for monitoring/execution. + +=== Comparison with Script-Based Wallets === + +Script-based wallets are another way to create recovery mechanisms, and can use absolute and +relative locktimes using OP_CHECKLOCKTIMEVERIFY ([[bip-0065.mediawiki|BIP-65]]) and +OP_CHECKSEQUENCEVERIFY ([[bip-0112.mediawiki|BIP-112]]). +For example, we can build a script that allows one main key to spend the funds at any time, +and a secondary key to spend the funds only in transactions with nLocktime above a certain +date/block-height, or only in transactions with nSequence above a certain relative +time-gap/number-of-blocks. +This makes the secondary key useful only after an absolute date/block-height, or after +a relative time since the funds were received (each UTXO independently). +This approach does have some advantages over pre-signed transactions, for example the +recovery-mechanism automatically applies to new funds received into the wallet. + +However, script-based wallets have some disadvantages over a sequence of +pre-signed transactions: + +* Script-based wallets are harder to implement correctly by hardware wallets, and harder to backup properly (i.e. users may forget to backup wallet-descriptors even for basic multisig wallets). +* As of the time of writing, scripts can limit when secondary-keys can be used, but not how they can be used: if the user doesn't touch the wallets' UTXOs for long-enough time, the secondary key will eventually become useable and could move the funds anywhere. This is true whether we measure the time in absolute terms (OP_CHECKLOCKTIMEVERIFY) or relative terms compared to when the wallets' UTXOs were created (OP_CHECKSEQUENCEVERIFY). This means that even in the happy-flow scenario of an untouched wallet, where no recovery is needed, the user must periodically "renew" the recovery-mechanism by spending the UTXO to a new wallet/address. This may be inconvenient in ultra-cold-storage scenarios (i.e. multisig with main keys hidden in different geographic locations). New opcode suggestions, such as OP_CHECKTEMPLATEVERIFY ([[bip-0119.mediawiki|BIP-119]]) and OP_CHECKCONTRACTVERIFY ([[bip-0443.mediawiki|BIP-443]]), discuss possible recovery-mechanisms in which in order for a secondary key to have full control over the funds, some onchain operations must be performed, with a required time-gap between them - giving the user enough time to revoke the whole process and move the funds elsewhere (assuming they still have the main key and the recovery-mechanism was triggered unintentionally). However, these suggestions are still in the discussion phase and even if ever implemented, their adoption may be slow. +* New Bitcoiners today typically don't think of such recovery-mechanisms in advance, and start with a P2WPKH wallet. They can pre-sign transactions with this wallet, but to utilize script-based features they would need to create a new wallet and move the funds there - an operation that might seem intimidating for large amounts. + +== Specification == + +A ''Timelock-Recovery plan'' consists of two transactions: + +* ''Alert Transaction'': A mostly-consolidation transaction that keeps most funds in the original wallet, except for a fee and a small fixed amount that goes to ''anchor-addresses'' - addresses which can be used to accelerate the ''Alert Transaction'' via CPFP. The majority of funds should remain on the original wallet, in a new previously-unused address which we call the ''alert-address''. We use the term ''Alert Transaction'' because monitoring the blockchain and looking for it should alert the user that the recovery-plan has been initiated (intentionally, unintentionally or maliciously). +* ''Recovery Transaction'': The transaction that moves the funds from the alert-address UTXO from the ''Alert Transaction'' to one or more addresses of secondary wallets (each may receive a different amount). This transaction should have a special nSequence relative-locktime according to the size of cancellation-period requested by the user, following the rules of [[bip-0068.mediawiki|BIP-68]]. + +With a reliable tool to monitor the blockchain for the ''Alert Transaction'' +or the ''Alert Address'', the user can safely store online backups of the recovery plan's +JSON file (or, even without a tool, by checking the blockchain manually from time to time). +If the presigned transactions leak and the ''Alert Transaction'' is broadcast +unintentionally, the user has the cancellation period (expected to be at least a +few days) to prevent most funds from moving by sending them to a new address, thereby +invalidating the ''Recovery Transaction''. + +It is important that the ''Alert Transaction'' will be non-malleable (e.g. by using +[[bip-0140.mediawiki|BIP-140]]). +If a malleable ''Alert Transaction'' is used, a malicious miner could replace the +''Alert Transaction'' with a similar transaction with a different txid, +making the ''Recovery Transaction'' invalid (pointing to a non-existent UTXO). + +The nLocktime of both transactions should not be higher than the current +block height. + +The ''anchor-addresses'' mentioned above, which are used for CPFP acceleration, could possibly +be P2A addresses (described in [[bip-0433.mediawiki|BIP-433]]), or other addresses under the +participants' control (i.e. addresses from the secondary wallets). +As of the time of writing, P2A is not widely adopted, and less-technical users may +struggle using them for CPFP acceleration - so we currently recommend using regular addresses. + +=== nSequence calculation === + +Users will specify the cancellation-period in whole days between 2-388. + +Following [[bip-0068.mediawiki|BIP-68]], the nSequence can represent a timespan in +units of 512 seconds, when bit (1 << 22) is set. An example calculation is provided below: + + +n_sequence = (1 << 22) | round(cancellation_period_days * 24 * 60 * 60 / 512) + + +Users should be notified that the cancellation-period is not guaranteed to be exact (due to miners' +manipulation of block-timestamps). + +Less than 2 days of cancellation-period and partial-days are not supported, as they are not useful. + +More than 388 days of cancellation-period will overflow the nSequence field bits +allocated for the relative-locktime, and is not supported. + +=== JSON format === + +For simplicity, this BIP proposes that a ''Timelock-Recovery plan'' will be saved as a JSON +object. + +The JSON object will have the following fields: + +* kind (mandatory): must be "timelock-recovery-plan". +* id (mandatory): a non-empty string of up to 100 characters, to represent the plan uniquely (i.e. a UUID, or a server generated ID). +* name (optional): a name for the plan, decided by the user. A string of up to 200 characters. +* description (optional): a description for the plan, decided by the user. A string of up to 10,000 characters. +* created_at (mandatory): an ISO 8601 timestamp of the plan creation time, including timezone offset ('Z' if the timezone is UTC). +* plugin_version (optional): The version of the plugin that generated the plan. A string of up to 100 characters. +* wallet_version (mandatory): The version of the wallet that generated the plan. A string of up to 100 characters. +* wallet_name (mandatory): The human-readable name of the wallet app that generated the plan. A string of up to 100 characters. +* wallet_kind (mandatory): The internal name of the wallet app that generated the plan. A string of up to 100 characters. +* timelock_days (mandatory): The cancellation period in whole days. A number between 2 and 388. +* anchor_amount_sats (mandatory): The amount in satoshis sent to each anchor address in the Alert Transaction. We recommend using 600 sats, which is above the dust limit. +* anchor_addresses (mandatory): An array of up to 10,000 Bitcoin addresses that receive the anchor amount in the Alert Transaction. Each address is a string of up to 100 characters. +* alert_address (mandatory): The Bitcoin address that receives the majority of funds in the Alert Transaction. A string of up to 100 characters. +* alert_inputs (mandatory): An array of up to 2439 inputs spent by the Alert Transaction. Each input is a string in the format "txid:vout" where txid is a 64-character lowercase hexadecimal string and vout is a decimal number of up to 6 digits. The maximal length of 2439 is calculated from a standard transaction of 400,000 wu where each input contains at least 41 bytes. +* alert_tx (mandatory): The raw Alert Transaction in uppercase hexadecimal format. A string of up to 800,000 characters. +* alert_txid (mandatory): The transaction ID of the Alert Transaction. A 64-character lowercase hexadecimal string. +* alert_fee (mandatory): The total fee paid by the Alert Transaction in satoshis. A non-negative integer. +* alert_weight (mandatory): The weight of the Alert Transaction. A positive integer, not higher than 400,000. +* recovery_tx (mandatory): The raw Recovery Transaction in uppercase hexadecimal format. A string of up to 800,000 characters. +* recovery_txid (mandatory): The transaction ID of the Recovery Transaction. A 64-character lowercase hexadecimal string. +* recovery_fee (mandatory): The total fee paid by the Recovery Transaction in satoshis. A non-negative integer. +* recovery_weight (mandatory): The weight of the Recovery Transaction. A positive integer, not higher than 400,000. +* recovery_outputs (mandatory): An array of up to 10,000 outputs from the Recovery Transaction. Each output is a tuple containing: [address, amount_sats, label?] where: +** address is a mandatory Bitcoin address string (up to 100 characters). +** amount_sats is a mandatory positive integer representing the amount in satoshis. +** label is an optional string of up to 200 characters. +* metadata (optional): A string of up to 10,000 characters for additional metadata, for example a digital-signature. +* checksum (mandatory): A checksum for verifying the integrity of the plan. A string of 8 to 64 characters. + +=== Checksum Calculation === +Notice that besides the top-level JSON object, all the internal values are either primitive or +arrays. +This is intentional, so a conversion of the values to JSON strings will be deterministic. + +The checksum is calculated by converting the top-level JSON object to an array of +[key, value] pairs, sorting the array, stringifying, calculating the +SHA256 hash of the result in lowercase hexadecimal format, and taking a prefix of at least 8 +characters. + +"stringifying" in this context means converting the array to a JSON string following +[https://262.ecma-international.org/16.0/index.html#sec-json.stringify ECMAScript's JSON.stringify function specification] +(without additional replacer or space arguments). +This specification covers special cases such as non-ascii characters and newlines. + +Optional fields without a value should not be included in the key-value pairs array. +Also, there should not be a key-value pair for the "checksum" field itself. + +Code example: + +delete recoveryPlanJson.checksum; // In case the checksum was calculated previously and the object was then modified +const checksumData = new TextEncoder().encode( + JSON.stringify(Object.entries(recoveryPlanJson).sort()), +); +const checksum = new Uint8Array(await crypto.subtle.digest('SHA-256', checksumData)); +recoveryPlanJson.checksum = Array.from(checksum).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 8); + + +Checksum hex string should be at least 8 characters long. Wallets may choose to use a longer +checksum. + +== Rationale == + +The JSON object will contain the raw transactions, in addition to other information - some of +which could technically be extracted from the raw transactions. This is intentional, to let +frontend UIs display the plan before uploading it to any service, without the need for +complicated parsing in the frontend. + +Backend services that receive the JSON object for monitoring/execution are expected to validate +that the information is consistent with the raw transactions. + +Also, if some wallet apps did not implement the specifications correctly, the services could +write custom code based on the wallet_kind, wallet_version and +plugin_version fields. + +Servers may decide to put more restrictions on JSON objects, for example to refuse +storing very large transactions. + +Notice that the raw transactions (alert_tx and recovery_tx) are expected +to be in uppercase hexadecimal format. +This is useful for frontend UIs to display them as QR codes, which are more compact when using +uppercase-only alphanumeric characters. + +=== Monitoring Timelock-Recovery Plans === + +Checking whether the Alert Transaction is valid is trivial, via the +testmempoolaccept RPC call in bitcoin core 0.17+. + +However, checking whether the Recovery Transaction is valid is more complex, +since it depends on a UTXO created by the Alert Transaction. + +The testmempoolaccept RPC can receive a list of transactions in which the later +transactions may depend on earlier transactions - however in our case the +Recovery Transaction has an nSequence relative-locktime, and therefore +calling testmempoolaccept 'alert-tx' 'recovery-tx' will fail, claiming that the +Alert Transaction UTXO is not confirmed (and the required time window has not passed). + +We recommend services that want to verify the entire Timelock-Recovery plan to parse +the Recovery Transaction and check its signatures manually, and reject complicated +spending scripts. Discovering that the Recovery Transaction is invalid only at the +time of execution, could lead to funds being locked forever. + +== Reference Implementation == + +JSON files can be generated using the Timelock Recovery plugin on +[https://electrum.org Electrum Wallet]: + +https://github.com/spesmilo/electrum/tree/master/electrum/plugins/timelock_recovery + +Demo Video: https://drive.google.com/file/d/10uXRouQbH1kz_HC14WnmRnYHa3gPZY8l/preview + +Example JSON file: + + +{ + "kind": "timelock-recovery-plan", + "id": "exported-692452189b301b561ed57cbe", + "name": "Recovery Plan ac300e72-7612-497e-96b0-df2fdeda59ea", + "description": "RITREK APP 1.1.0: Trezor Account #1", + "created_at": "2025-11-24T12:39:53.532Z", + "plugin_version": "1.0.1", + "wallet_version": "1.0.1", + "wallet_name": "RITREK Service", + "wallet_kind": "RITREK BACKEND", + "timelock_days": 2, + "anchor_amount_sats": 600, + "anchor_addresses": [ + "bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk" + ], + "alert_address": "bc1qj0f9sjenwyjs0u7mlgvptjp05z3syzq7mru3ep", + "alert_inputs": [ + "a265a485df4c6417019b91379257eb387bceeda96f7bb6311794b8ed358cf104:0", + "2f621c2151f33173983133cbc1000e3b603b8a18423b0379feffe8513171d5d3:0" + ], + "alert_tx": "0200000000010204F18C35EDB8941731B67B6FA9EDCE7B38EB579237919B0117644CDF85A465A20000000000FDFFFFFFD3D5713151E8FFFE79033B42188A3B603B0E00C1CB3331987331F351211C622F0000000000FDFFFFFF0258020000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD205600000000000016001493D2584B33712507F3DBFA1815C82FA0A302081E02483045022100DCDBAE77C35EB4A0B3ED0DE5484206AB6B07041BE99B2BBAF0243C125916523C0220396959C3C52B2B1F9E472AEEE7C5D9540531B131C3221DE942754C6D0941397D012103C08FF3ADBA14B742646572BCA6F07AEB910666FB28E4DDDC40E33755E7C869D30248304502210089084472FDA3CF82D6ABC11BF1A5E77C9B423617C8B840F58C02746035B3BA6302203942AA1FA13F952F49FB114D48130A9AAF70151E7D09036D15734DB1F41A8B6001210397064EDED7DAD7D662290DC2847E87C5C27DA8865B89DDB58FDE9A006BA7DB3900000000", + "alert_txid": "f1413fedadaf30697820bcd8f6a393fcc73ea00a15bea3253f89d5658690d2f7", + "alert_fee": 231, + "alert_weight": 834, + "recovery_tx": "02000000000101F7D2908665D5893F25A3BE150AA03EC7FC93A3F6D8BC20786930AFADED3F41F101000000005201400001A6550000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD0247304402204AFF87C2127F5697F300C6522067A8D5E5290CA8D140D2E5BCEF4A36606C5FE5022056673BEC5BB459DFFBD4D266EE95AEF0D701383ED80BD433A02C3C486A826D76012102774DBCD59F2D08EFF718BC09972ADC609FBC31C26B551B3E4EA30A1D43EEDB9700000000", + "recovery_txid": "bc304610e8f282036345e87163d4cba5b16488a3bf2e4d738379d7bda3a0bca3", + "recovery_fee": 122, + "recovery_weight": 437, + "recovery_outputs": [ + [ + "bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk", + 21926, + "My Backup Wallet" + ] + ], + "metadata": "sig:825d6b3858c175c7fc16da3134030e095c4f9089c3c89722247eeedc08a7ef4f", + "checksum": "92f8b3da" +} + + +== Copyright == + +This document is licensed under the 2-clause BSD license. diff --git a/bip-0129.mediawiki b/bip-0129.mediawiki index 2e19c9fb18..a7ebae4bd6 100644 --- a/bip-0129.mediawiki +++ b/bip-0129.mediawiki @@ -11,6 +11,7 @@ Type: Specification Assigned: 2020-11-10 License: BSD-2-Clause + Requires: 32, 174, 322, 380, 381, 382, 383 ==Introduction== diff --git a/bip-0174.mediawiki b/bip-0174.mediawiki index 275ce6389e..ad9c7a153d 100644 --- a/bip-0174.mediawiki +++ b/bip-0174.mediawiki @@ -507,11 +507,11 @@ The currently defined per-input types are defined as follows: |- | MuSig2 Participant Public Keys | PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a -| <33 byte plain aggregate pubkey> -| The MuSig2 aggregate plain public key from the KeyAgg algorithm. This key may or may not +| <33 byte aggregate pubkey (compressed)> +| The MuSig2 aggregate public key (compressed) from the KeyAgg algorithm. This key may or may not be in the script directly (as x-only). It may instead be a parent public key from which the public keys in the script were derived. -| <33 byte compressed pubkey>* +| <33 byte participant pubkey (compressed)>* | A list of the compressed public keys of the participants in the MuSig2 aggregate key in the order required for aggregation. If sorting was done, then the keys must be in the sorted order. | @@ -521,11 +521,11 @@ required for aggregation. If sorting was done, then the keys must be in the sort |- | MuSig2 Public Nonce | PSBT_IN_MUSIG2_PUB_NONCE = 0x1b -| <33 byte compressed pubkey> <33 byte plain pubkey> <32 byte hash or omitted> -| The compressed public key of the participant providing this nonce, followed by the plain public +| <33 byte participant pubkey (compressed)> <33 byte aggregate pubkey (compressed)> <32 byte hash or omitted> +| The compressed public key of the participant providing this nonce, followed by the compressed aggregate public key the participant is providing the nonce for, followed by the BIP 341 tapleaf hash of the Taproot leaf script that will be signed. If the aggregate key is the taproot internal key or the -taproot output key, then the tapleaf hash must be omitted. The plain public key must be +taproot output key, then the tapleaf hash must be omitted. The compressed participant public key must be the key found in the script and not the aggregate public key that it was derived from, if it was derived from an aggregate key. | <66 byte public nonce> @@ -537,11 +537,11 @@ derived from an aggregate key. |- | MuSig2 Participant Partial Signature | PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c -| <33 byte compressed pubkey> <33 byte plain pubkey> <32 byte hash or omitted> +| <33 byte participant pubkey (compressed)> <33 byte aggregate pubkey (compressed)> <32 byte hash or omitted> | The compressed public key of the participant providing this partial signature, followed by the -plain public key the participant is providing the signature for, followed by the BIP 341 tapleaf hash -of the Taproot leaf script that will be signed. If the aggregate key is the taproot internal key or -the taproot output key, then the tapleaf hash must be omitted. Note that the plain public key must +compressed public key the participant is providing the signature for, followed by the BIP 341 tapleaf hash +of the Taproot leaf script that will be signed. If the aggregate key is the Taproot internal key or +the Taproot output key, then the tapleaf hash must be omitted. Note that the compressed participant public key must be be the key found in the script and not the aggregate public key that it was derived from, if it was derived from an aggregate key. | <32 byte partial signature> @@ -691,11 +691,11 @@ determine which outputs are change outputs and verify that the change is returni |- | MuSig2 Participant Public Keys | PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 -| <33 byte plain aggregate pubkey> -| The MuSig2 aggregate plain public key from the KeyAgg algorithm. This key may or may not +| <33 byte aggregate pubkey (compressed)> +| The MuSig2 compressed aggregate public key from the KeyAgg algorithm. This key may or may not be in the script directly. It may instead be a parent public key from which the public keys in the script were derived. -| <33 byte compressed pubkey>* +| <33 byte participant pubkey (compressed)>* | A list of the compressed public keys of the participants in the MuSig2 aggregate key in the order required for aggregation. If sorting was done, then the keys must be in the sorted order. | diff --git a/bip-0310.mediawiki b/bip-0310.mediawiki index 1c9ce0aa02..6b3b0d1736 100644 --- a/bip-0310.mediawiki +++ b/bip-0310.mediawiki @@ -180,6 +180,9 @@ Example result (unknown extension): to be stable for the whole mining session. A miner doesn't have to send the mask, in this case a default full mask is used. +* '''"version-rolling.min-bit-count"''' (REQUIRED, ''Integer'', >= 0) +::- The miner also provides a minimum number of bits that it needs for efficient version rolling in hardware. Note that this parameter provides important diagnostic information to the pool server. If the requested bit count exceeds the limit of the pool server, the miner always has the chance to operate in a degraded mode without using full hashing power. The pool server SHOULD NOT terminate miner connection if this rare mismatch case occurs. + '''Extension return values''': * '''"version-rolling"''' (REQUIRED, ''TExtensionResult'') @@ -189,9 +192,6 @@ send the mask, in this case a default full mask is used. ::- Bits set to 1 are allowed to be changed by the miner. If a miner changes bits with mask value 0, the server will reject the submit. ::- The server SHOULD return the largest mask possible (as many bits set to 1 as possible). This can be useful in a mining proxy setup when a proxy needs to negotiate the best mask for its future clients. There is a [https://github.com/bitcoin/bips/pull/661/files Draft BIP] describing available nVersion bits. The server SHOULD pick a mask that preferably covers all bits specified in the BIP. -* '''"version-rolling.min-bit-count"''' (REQUIRED, ''TMask'') -::- The miner also provides a minimum number of bits that it needs for efficient version rolling in hardware. Note that this parameter provides important diagnostic information to the pool server. If the requested bit count exceeds the limit of the pool server, the miner always has the chance to operate in a degraded mode without using full hashing power. The pool server SHOULD NOT terminate miner connection if this rare mismatch case occurs. - ===Notification '''"mining.set_version_mask"'''=== Server notifies the miner about a new mask valid for the diff --git a/bip-0324.mediawiki b/bip-0324.mediawiki index 6b6803a3f4..e0bdd8a584 100644 --- a/bip-0324.mediawiki +++ b/bip-0324.mediawiki @@ -10,6 +10,7 @@ Type: Specification Assigned: 2019-03-08 License: BSD-3-Clause + Version: 1.0.2 Replaces: 151 @@ -530,7 +531,9 @@ v2 Bitcoin P2P transport layer packets use the encrypted message structure shown If the first byte of message_type is b'\x00', the following 12 bytes are interpreted as an ASCII message type (as in the v1 P2P protocol), trailing padded with b'\x00' as necessary. If the first byte of message_type is in the range ''1..255'', it is interpreted as a message type ID. This structure results in smaller messages than the v1 protocol, as most messages sent/received will have a message type ID. We recommend reserving 1-byte type IDs for message types that are sent more than once per direction per connection.'''How do the lengths between v1 and v2 compare?''' For messages that use the 1-byte short message type ID, v2 packets use 3 bytes less per message than v1.'''Why not allow variable length long message type IDs?''' Allowing for variable length long IDs reduces the available 1-byte ID space by 12 (to encode the length itself) and incentivizes less descriptive message types. In addition, limiting message types to fixed lengths of 1 or 13 hampers traffic analysis. -The following table lists currently defined message type IDs: +The value of message_length is '''length''' minus the size of the message_type. + +The following table lists currently defined message type IDs and the 12-byte ASCII message type (trimmed of trailing padding) that they are treated as equivalent to: {| class="wikitable" |- @@ -541,35 +544,36 @@ The following table lists currently defined message type IDs: !3 |- !+0 -|(12 bytes follow)||ADDR||BLOCK||BLOCKTXN +|(12 bytes follow)||addr||block||blocktxn |- !+4 -|CMPCTBLOCK||FEEFILTER||FILTERADD||FILTERCLEAR +|cmpctblock||feefilter||filteradd||filterclear |- !+8 -|FILTERLOAD||GETBLOCKS||GETBLOCKTXN||GETDATA +|filterload||getblocks||getblocktxn||getdata |- !+12 -|GETHEADERS||HEADERS||INV||MEMPOOL +|getheaders||headers||inv||mempool |- !+16 -|MERKLEBLOCK||NOTFOUND||PING||PONG +|merkleblock||notfound||ping||pong |- !+20 -|SENDCMPCT||TX||GETCFILTERS||CFILTER +|sendcmpct||tx||getcfilters||cfilter |- !+24 -|GETCFHEADERS||CFHEADERS||GETCFCHECKPT||CFCHECKPT +|getcfheaders||cfheaders||getcfcheckpt||cfcheckpt |- !+28 -|ADDRV2 +|addrv2 |- !≥29 || colspan="4" | (undefined) |} +When a message type has both a 1-byte encoding and a 13-byte encoding defined, peers that support receiving that message type should accept messages using either encoding (e.g., if the "getblocktxn" message type is supported, then both the 1-byte b'\x0a' encoding and the 13-byte b'\x00getblocktxn\x00' should be supported, and behavior should not depend on which of the two encodings is received). -Additional message types may be added separately after BIP finalization. +Additional message type IDs may be defined by other BIPs. They should be added to the [[bip-0324/message_type_ids.md|message type IDs table]] to ease coordination. === Signaling specification === ==== Signaling v2 support ==== @@ -583,6 +587,15 @@ For development and testing purposes, we provide a collection of test vectors in * [[bip-0324/xswiftec_inv_test_vectors.csv|XSwiftECInv vectors]] provide examples of ''(u, x)'' pairs, and the various ''t'' values that ''xswiftec_inv'' maps them to. * [[bip-0324/packet_encoding_test_vectors.csv|Packet encoding vectors]] illustrate the lifecycle of the authenticated encryption scheme proposed in this document. +== Changelog == + + * 1.0.2 (2026-01-30) + * Add message type ID table in auxiliary file + * 1.0.1 (2026-01-16) + * Specify equivalence of 1-byte and 13-byte `message_type` + * 1.0.0 (2024-07-10) + * Marked as Final + == Rationale and References == diff --git a/bip-0324/message_type_ids.md b/bip-0324/message_type_ids.md new file mode 100644 index 0000000000..4ececd208b --- /dev/null +++ b/bip-0324/message_type_ids.md @@ -0,0 +1,24 @@ + +# One-byte message type allocations + +This table lists additional message type IDs that various BIPs have +assigned or proposed for assignment. + +Type | Message | Proposal +---- | ------------ | -------- + 29 | uproof | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 30 | getuproof | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 31 | uttls | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 32 | getuttls | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 33 | usummary | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 34 | utreexotx | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 35 | uroot | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 36 | geturoot | [BIP 183 / PR#1923](https://github.com/bitcoin/bips/pull/1923) + 37 | feature | [BIP 434](https://github.com/bitcoin/bips/blob/master/bip-0434.md) + +Note that this table is not authoritative but instead is reflective +of the BIPs proposing the changes. If multiple BIPs make conflicting +assignments for message type IDs, that may lead to multiple entries for +the same message type ID in this table. BIPs that are in Draft status +may be included in the table, and as a result, the IDs and messages they +define may be changed in future. diff --git a/bip-0346.md b/bip-0346.md new file mode 100644 index 0000000000..3d67012e8f --- /dev/null +++ b/bip-0346.md @@ -0,0 +1,441 @@ +``` + BIP: 346 + Layer: Consensus (soft fork) + Title: OP_TXHASH + Authors: Steven Roose + Brandon Black + Status: Draft + Type: Specification + Assigned: 2024-04-24 + License: BSD-3-Clause +``` + +# Abstract + +This BIP proposes a new opcode `OP_TXHASH`, to be activated as a change to the +semantics of `OP_SUCCESS189` in tapscript contexts. + +This opcode provides a generalized method for introspecting certain details of +the spending transaction, which enables non-interactive enforcement of certain +properties of the transaction spending a certain UTXO. + +Together with an opcode like `OP_CHECKSIGFROMSTACK`, this opcode effectively +provides a fully generalized signature hash construction, fully supporting +all existing SIGHASH flags, the proposed sighash flags from +[BIP-118](bip-0118.mediawiki) (`SIGHASH_ANYPREVOUT`) and many other new signature hash +combinations. + +The constructions specified in this BIP also open up the way for other +potential updates; see Motivation section for more details. + + +# Specification + + +## OP_TXHASH + +`OP_TXHASH` redefines the `OP_SUCCESS189` tapscript opcode (`0xbd`) as a soft +fork upgrade. This opcode is only active in tapscript contexts. + +Note that `OP_SUCCESS187` is used by [BIP-443](bip-0443.mediawiki) (`OP_CHECKCONTRACTVERIFY`) and +`OP_SUCCESS188` was used by [BIP-345](bip-0345.mediawiki) (`OP_VAULT`) at the time of first draft +of this BIP, making `OP_SUCCESS189` the next available opcode for this purpose. + +It has the following semantics: + +* There is at least one element on the stack, fail otherwise. +* The element is interpreted as the TxFieldSelector and is popped off the stack. +* The TxFieldSelector must be valid, fail otherwise. +* The 32-byte TxHash of the transaction at the current input index, calculated + using the given TxFieldSelector is pushed onto the stack. + +## TxFieldSelector + +The TxFieldSelector has the following encoding. We will give a brief conceptual +summary, followed by a reference implementation of the CalculateTxHash function. + +In the following specifications, the `|` operator is used for the bitwise OR +operation. + +* There are two special cases for the TxFieldSelector: + * the empty value, zero bytes long: it is set equal to `TXFS_SPECIAL_TEMPLATE`, + the de-facto default value which means everything except the prevouts and + the prevout scriptPubkeys and amounts. + + Special case `TXFS_SPECIAL_TEMPLATE` is 4 bytes long, as follows: + * 1: `TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX` + * 2: `TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL` + * 3: `TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL` + * 4: `TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL` + + * If the TxFieldSelector has exactly 1 byte, we use a _short notation_. + It has its 8 bits assigned as follows, from lowest to highest: + * 2/1: Inputs + * 00: `TXFS_INOUT_SELECTION_NONE` + * 01: `TXFS_INOUT_SELECTION_CURRENT` + * 11: `TXFS_INOUT_SELECTION_ALL` + * 4/3: Outputs + * 00: `TXFS_INOUT_SELECTION_NONE` + * 01: `TXFS_INOUT_SELECTION_CURRENT` + * 11: `TXFS_INOUT_SELECTION_ALL` + * 5: `TXFS_INPUTS_PREVOUTS` + * 6: `TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES` + * 7: `TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_SPENTSCRIPT | TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS` + * 8: `TXFS_CURRENT_INPUT_IDX` + + Additionally, it includes `TXFS_VERSION | TXFS_LOCKTIME | TXFS_CONTROL | TXFS_CURRENT_INPUT_TAPROOT_ANNEX` + as global fields and `TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL` + as input and output fields. + + These 1-byte selections allow the TxFieldSelector to emulate current + signature hashing modes and those defined in [BIP-118](bip-0118.mediawiki): + +| BIP-341/118 sighash type | 1-byte TxFieldSelector | +| :--------------------------- | :--------------------- | +| `ALL` | `0b11111111` | +| `SINGLE` | `0b11110111` | +| `NONE` | `0b11110011` | +| `ALL\|ANYONECANPAY` | `0b11111101` | +| `SINGLE\|ANYONECANPAY` | `0b11110101` | +| `NONE\|ANYONECANPAY` | `0b11110001` | +| `ALL\|ANYPREVOUT` | `0b11101101` | +| `SINGLE\|ANYPREVOUT` | `0b11100101` | +| `NONE\|ANYPREVOUT` | `0b11100001` | +| `ALL\|ANYPREVOUTANYSCRIPT` | `0b11001101` | +| `SINGLE\|ANYPREVOUTANYSCRIPT`| `0b11000101` | +| `NONE\|ANYPREVOUTANYSCRIPT` | `0b11000001` | + + +* If the TxFieldSelector is longer than one byte, the first byte of the TxFieldSelector + has its 8 bits assigned as follows, from lowest to highest: + * 1: version (`TXFS_VERSION`) + * 2: locktime (`TXFS_LOCKTIME`) + * 3: current input index (`TXFS_CURRENT_INPUT_IDX`) + * 4: current input control block (`TXFS_CURRENT_INPUT_CONTROL_BLOCK`) + * 5: current input spent script (`TXFS_CURRENT_INPUT_SPENTSCRIPT`) + * 6: current script last `OP_CODESEPARATOR` position (or 0xffffffff) + (`TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS`) + * 7: current input annex including prefix byte (or empty) (`TXFS_CURRENT_INPUT_TAPROOT_ANNEX`) + * 8: `TXFS_CONTROL` (i.e. include TxFieldSelector into hash) + +* The highest bit of the first byte (`TXFS_CONTROL`), we will call the + "control bit", and it can be used to control the behavior of the opcode. For + `OP_TXHASH`, the control bit is used to determine + whether the TxFieldSelector itself has to be included in the resulting hash. + (For potential other uses of the TxFieldSelector (like a hypothetical + `OP_TX`), this bit can be repurposed.) + +* The second byte will be used to indicate fields from the inputs and outputs. + The 8 bits are assigned the following variables, from lowest to highest: + * Specifying which fields of the inputs will be selected: + * 1: prevouts (`TXFS_INPUTS_PREVOUTS`) + * 2: sequences (`TXFS_INPUTS_SEQUENCES`) + * 3: scriptSigs (`TXFS_INPUTS_SCRIPTSIGS`) + * 4: prevout scriptPubkeys (`TXFS_INPUTS_PREV_SCRIPTPUBKEYS`) + * 5: prevout values (`TXFS_INPUTS_PREV_VALUES`) + * 6: taproot annexes (`TXFS_INPUTS_TAPROOT_ANNEXES`) + + * Specifying which fields of the outputs will be selected: + * 7: scriptPubkeys (`TXFS_OUTPUTS_SCRIPTPUBKEYS`) + * 8: values (`TXFS_OUTPUTS_VALUES`) + +* We define as follows: + * `TXFS_ALL = TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX | TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS | TXFS_CONTROL` + * `TXFS_INPUTS_ALL = TXFS_INPUTS_PREVOUTS | TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES | TXFS_INPUTS_TAPROOT_ANNEXES` + * `TXFS_OUTPUTS_ALL = TXFS_OUTPUTS_SCRIPTPUBKEYS | TXFS_OUTPUTS_VALUES` + + +* For both inputs and then outputs, expect an additional byte as follows: + * The highest bit (`TXFS_INOUT_NUMBER`) indicates whether the "number of + in-/outputs" should be committed to. + * For the remaining bits, there are three exceptional values: + * 0x00 (`TXFS_INOUT_SELECTION_NONE`) means "no in/outputs" (hence only the + number of them as `0x80` (`TXFS_INOUT_NUMBER`)). + * `0x40` (`TXFS_INOUT_SELECTION_CURRENT`) means "select only the in/output + of the current input index" (it is invalid when current index exceeds + number of outputs). + * `0x3f` (`TXFS_INOUT_SELECTION_ALL`) means "select all in/outputs". + + * The second highest bit (`TXFS_INOUT_SELECTION_MODE`) is the "specification mode": + * Set to 0 it means "leading mode". + * Set to 1 it means "individual mode". + + * In "leading mode", the third highest bit (`TXFS_INOUT_LEADING_SIZE`) is + used to indicate the "index size", i.e. the number of bytes will be used to + represent the number of in/output. + * With "index size" set to 0, the remaining lowest 5 bits of the first byte + will be interpreted as the number of leading in/outputs to select. + * With "index size" set to 1, the remaining lowest 5 bits of the first byte + together with the 8 bits of the next byte will be interpreted as the + number of leading in/outputs to select. + + * In "individual mode", the third highest bit (`TXFS_INOUT_INDIVIDUAL_MODE`) + indicates whether we are passing absolute indices (0) or indices relative + to the current input (1), the remaining lowest 5 bits will be interpreted + as `n`, the number of individual in/outputs follow. + * In absolute mode (second highest bit is 0), for each of the `n` indices, + at least one extra byte is expected. + * If that byte's highest bit is set to 0, the remaining 7 bits represent + the absolute index to select. + * If that byte's highest bit is set to 1, the remaining 7 bits, together + with the next byte's 8 bits represent the absolute index to select. + * In relative mode (second highest bit is 1), for each of the `n` indices, + at least one extra byte is expected. + * If that byte's highest bit is set to 0, the remaining 7 bits represent + the relative index in two's complement. + * If that byte's highest bit is set to 1, the remaining 7 bits, together + with the next byte's 8 bits represent the relative index in two's + complement. + + +Effectively, this allows a user to select +* all in/outputs +* the current input index +* the leading in/outputs up to 8,191 +* up to 32 individually selected in/outputs +** using absolute indices up to 32,767 +** using indices relative to the current input index from -16382 to +16383. + + +### TxFieldSelector malleability + +It is possible to represent the same selected data using multiple different +TxFieldSelectors. For this reason, users are advised to always set the +`TXFS_CONTROL` field flag that commits to the TxFieldSelector that was used +to get the hash. + + +### Visualization + +* first byte + +``` +1 1 1 1 1 1 1 1 +| | | | | | | ^ version +| | | | | | ^ locktime +| | | | | ^ current input index +| | | | ^ current input control block +| | | ^ current input spent script +| | ^ current script last OP_CODESEPARATOR +| ^ current input taproot annex +^ control bit (ie. include TXFS in hash) +``` + +* second byte + +``` +<-> outputs +| | <---------> inputs +1 1 1 1 1 1 1 1 +| | | | | | | ^ prevouts +| | | | | | ^ sequences +| | | | | ^ scriptSigs +| | | | ^ prevout scriptPubkeys +| | | ^ prevout values +| | ^ taproot annexes +| ^ scriptPubkeys +^ values +``` + +* in/output selector byte + +"leading 3 in/outputs" +``` +1 0 0 0 0 0 1 1 +| | | <-------> integer 0b00011 == 3 +| | ^ index size: single byte +| ^ leading mode +^ commit the number of in/outputs +``` + +"leading 257 in/outputs" +``` +1 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 +| | | <------------------------> integer 0b00001 00000001 == 257 +| | ^ index size 1: two bytes +| ^ leading mode +^ commit the number of in/outputs +``` + +"indices 1 and 3" +``` +0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 +| | | | <--------------> second idx: 3 +| | | | <-------------> first idx: 1 +| | | | <-----> selection count: 0b0010 == 2 indices +| | | ^ index size: single byte per index +| | ^ absolute index +| ^ individual mode +^ don't commit the number of in/outputs +``` + +* total example + +``` +ff ff c2 01 03 83 + | | ^ commit number of outputs + leading 3 outputs + | | <------> commit number of inputs + inputs at indices 1 and 3 + | ^ all input and output fields + ^ all regular fields +``` + + +## Resource limits + +Using the same validation budget ("sigops budget") introduced in BIP-0342, +each TransactionHash decreases the validation budget by 25. If this brings the +budget below zero, the script fails immediately.
The following +considerations should be made: + +* All fields that can be of arbitrary size are cacheable as TransactionHash + always hashes their hashed values. +* In "individual mode", a user can at most commit 32 inputs or outputs, + which we don't consider excessive for potential repeated use. +* In "leading mode", a caching strategy can be used where the SHA256 context + is stored every N in/outputs so that multiple executions of the + TransactionHash function can use the caches and only have to hash an + additional N-1 items at most. + + +# Motivation + +This BIP specifies a basic transaction introspection primitive that is useful +to either reduce interactivity in multi-user protocols or to enforce some basic +constraints on transactions. + +Additionally, the constructions specified in this BIP can lay the groundwork for +some potential future upgrades: +* The TxFieldSelector construction would work well with a hypothetical opcode + `OP_TX` that allows for directly introspecting the transaction by putting the + fields selected on the stack instead of hashing them together. +* The TransactionHash obtained by `OP_TXHASH` can be combined with `OP_CHECKSIGFROMSTACK` + (see [BIP-348](bip-0348.md)) to effectively create an + incredibly flexible signature hash, which would enable constructions like + `SIGHASH_ANYPREVOUT`. +* The TransactionHash obtained by `OP_TXHASH` can be introduced as a native + sighash calculation in a future segwit upgrade, so that signatures using the + TransactionHash as their sighash can be used in keyspend context. + + +## Comparing with some alternative proposals + +* This proposal strictly generalizes BIP-119's `OP_CHECKTEMPLATEVERIFY`, as the + default mode of our TxFieldSelector is semantically the same (though not + byte-for-byte identical) as what `OP_CTV` accomplishes, without costing any + additional bytes. Additionally, using `OP_TXHASH` allows for more + flexibility which can help in the case for + * enabling adding fees to a transaction without breaking a multi-tx protocol; + * multi-user protocols where users are only concerned about their own inputs and outputs. + +* Constructions like `OP_IN_OUT_VALUE` used with `OP_EQUALVERIFY` can be + emulated by two `OP_TXHASH` instances by using the TxFieldSelector to select + a single input value first and a single output value second and enforcing + equality on the hashes. Neither of these alternatives can be used to enforce + small value differences without the availability of 64-bit arithmetic in + Script. + +* Like mentioned above, `SIGHASH_ANYPREVOUT` can be emulated using `OP_TXHASH` + when combined with `OP_CHECKSIGFROMSTACK`: + ` OP_TXHASH OP_CHECKSIGFROMSTACK` effectively emulates `SIGHASH_ANYPREVOUT`. + + +# Reference Implementation + +A reference implementation in Rust is provided attached as part of this BIP +together with a JSON file of test vectors generated using the reference +implementation. + + +# Design Considerations + +This specification in in _Draft_ and there is definitely still room for feedback +and improvements. Some considerations that were made but could be revisited: + +- The `0b10` in/output selector for the shorthand is unused. + Could possibly be filled in with "current + next", "current + previous" or + any other semantics. +- The individual index selection semantics allow for absolute indices up to ~32k + and relative ones up to +/- ~16k, which is probably excessive. When removing + the second byte there would reduce that to just 256 and +/- 128 respectively. +- Similar to `OP_TEMPLATEHASH` (BIP number to be assigned, [PR](https://github.com/bitcoin/bips/pull/1974)), + we could not support `scriptSigs` anymore and remove + `TXFS_INPUTS_SCRIPTSIGS`. This field could then possibly be repurposed. + + +# Backwards Compatibility + +`OP_TXHASH` replaces `OP_SUCCESS189`. The `SUCCESS` opcodes were +introduced in taproot (BIP-342) to support changing the semantics of opcodes in +ways that do allow the new semantics to change the stack. For this reason, +`OP_TXHASH` only works in tapscript context. Since it is overriding a `SUCCESS` +opcode, any older version of the software will always accept any script that +uses the opcode, while the new versions of the software will validate the +scripts according to the semantics outlined in this BIP. As such, this is also a +soft fork change. + +## Interactions with other BIPs + +This proposal interacts with several other BIPs: + +* **[BIP-118](bip-0118.mediawiki) (SIGHASH_ANYPREVOUT)**: `OP_TXHASH` can be combined with + `OP_CHECKSIGFROMSTACK` (BIP-348) to emulate `SIGHASH_ANYPREVOUT` and other + signature hash modes defined in BIP-118. The 1-byte TxFieldSelector format + explicitly supports these modes. + +* **[BIP-119](bip-0119.mediawiki) (OP_CHECKTEMPLATEVERIFY)**: `OP_TXHASH` with the empty + TxFieldSelector produces a hash semantically equivalent to BIP-119's + `OP_CHECKTEMPLATEVERIFY`, making this proposal a generalization of BIP-119. + +* **[BIP-347](bip-0347.mediawiki) (OP_CAT)**: When combined with `OP_CAT`, `OP_TXHASH` enables + powerful transaction introspection capabilities. The bit encoding format is + designed to be explicit about endianness to ensure correct interaction with + concatenation operations. + +* **[BIP-348](bip-0348.md) (OP_CHECKSIGFROMSTACK)**: Together with `OP_CHECKSIGFROMSTACK`, + `OP_TXHASH` provides a fully generalized signature hash construction, + enabling flexible covenant designs and multi-user protocols. + + +# Implementation + +A reference implementation is included as part of the BIP, see +[here](./bip-0346/ref-impl/src/main.rs). This implementation focusses on clarity +and correctness, not on efficiency. A rudimentary set of test vectors is also +generated from this implementation and included +[here](./bip-0346/ref-impl/txhash_vectors.json). + +Furthermore, following other implementation attempts exist: + +* A proposed implementation for Bitcoin Core is available here: + https://github.com/bitcoin/bitcoin/pull/29050 +* A proposed implementation for rust-bitcoin is available here: + https://github.com/rust-bitcoin/rust-bitcoin/pull/2275 + +NOTE: These implementations are slightly outdated as they were made for an +earlier version of this specification. Updates are in progress. + +Both of the above implementations perform effective caching to avoid potential +denial-of-service attack vectors. + + +# Deployment + +This BIP can be deployed using a BIP 9 VersionBits deployment. The specific +strategy and bit assignment are left unspecified and can later be amended to the +BIP depending on community preference. + + +# Acknowledgement + +Credit for this proposal mostly goes to Jeremy Rubin for his work on BIP-119's +`OP_CHECKTEMPLATEVERIFY` and to Russell O'Connor for the original idea of +generalizing `OP_CHECKTEMPLATEVERIFY` into `OP_TXHASH`. + +Additional thanks to Andrew Poelstra, Greg Sanders, Rearden Code, Rusty Russell +and others for their feedback on the specification. + + +# Copyright + +This document is licensed under the 3-clause BSD license. + diff --git a/bip-0346/ref-impl/Cargo.lock b/bip-0346/ref-impl/Cargo.lock new file mode 100644 index 0000000000..fd4f7d6b37 --- /dev/null +++ b/bip-0346/ref-impl/Cargo.lock @@ -0,0 +1,206 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "txhash-ref" +version = "0.0.0" +dependencies = [ + "bitcoin", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/bip-0346/ref-impl/Cargo.toml b/bip-0346/ref-impl/Cargo.toml new file mode 100644 index 0000000000..79c8cd2902 --- /dev/null +++ b/bip-0346/ref-impl/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "txhash-ref" +version = "0.0.0" +edition = "2021" + +[dependencies] +bitcoin = { version = "=0.32.8", features = [ "serde" ] } +serde_json = "1" diff --git a/bip-0346/ref-impl/src/main.rs b/bip-0346/ref-impl/src/main.rs new file mode 100644 index 0000000000..b519358803 --- /dev/null +++ b/bip-0346/ref-impl/src/main.rs @@ -0,0 +1,693 @@ + +use bitcoin::{Transaction, TxOut}; +use bitcoin::consensus::encode::Encodable; +use bitcoin::hashes::{sha256, Hash, HashEngine}; + +pub const TXFS_VERSION: u8 = 1 << 0; +pub const TXFS_LOCKTIME: u8 = 1 << 1; +pub const TXFS_CURRENT_INPUT_IDX: u8 = 1 << 2; +pub const TXFS_CURRENT_INPUT_CONTROL_BLOCK: u8 = 1 << 3; +pub const TXFS_CURRENT_INPUT_SPENTSCRIPT: u8 = 1 << 4; +pub const TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS: u8 = 1 << 5; +pub const TXFS_CURRENT_INPUT_TAPROOT_ANNEX: u8 = 1 << 6; +pub const TXFS_CONTROL: u8 = 1 << 7; + +pub const TXFS_INPUTS_PREVOUTS: u8 = 1 << 0; +pub const TXFS_INPUTS_SEQUENCES: u8 = 1 << 1; +pub const TXFS_INPUTS_SCRIPTSIGS: u8 = 1 << 2; +pub const TXFS_INPUTS_PREV_SCRIPTPUBKEYS: u8 = 1 << 3; +pub const TXFS_INPUTS_PREV_VALUES: u8 = 1 << 4; +pub const TXFS_INPUTS_TAPROOT_ANNEXES: u8 = 1 << 5; +pub const TXFS_OUTPUTS_SCRIPTPUBKEYS: u8 = 1 << 6; +pub const TXFS_OUTPUTS_VALUES: u8 = 1 << 7; + +pub const TXFS_INPUTS_ALL: u8 = TXFS_INPUTS_PREVOUTS + | TXFS_INPUTS_SEQUENCES + | TXFS_INPUTS_SCRIPTSIGS + | TXFS_INPUTS_PREV_SCRIPTPUBKEYS + | TXFS_INPUTS_PREV_VALUES + | TXFS_INPUTS_TAPROOT_ANNEXES; +pub const TXFS_OUTPUTS_ALL: u8 = TXFS_OUTPUTS_SCRIPTPUBKEYS | TXFS_OUTPUTS_VALUES; + +pub const TXFS_INOUT_NUMBER: u8 = 1 << 7; +pub const TXFS_INOUT_SELECTION_NONE: u8 = 0x00; +pub const TXFS_INOUT_SELECTION_CURRENT: u8 = 0x40; +pub const TXFS_INOUT_SELECTION_ALL: u8 = 0x3f; +pub const TXFS_INOUT_SELECTION_MODE: u8 = 1 << 6; +pub const TXFS_INOUT_LEADING_SIZE: u8 = 1 << 5; +pub const TXFS_INOUT_INDIVIDUAL_MODE: u8 = 1 << 5; +pub const TXFS_INOUT_SELECTION_MASK: u8 = 0xff ^ (1 << 7) ^ (1 << 6) ^ (1 << 5); + + +pub const TXFS_SPECIAL_TEMPLATE: [u8; 4] = [ + TXFS_VERSION | TXFS_LOCKTIME | TXFS_CURRENT_INPUT_IDX, + TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS | TXFS_OUTPUTS_ALL, + TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL, + TXFS_INOUT_NUMBER | TXFS_INOUT_SELECTION_ALL, +]; + +const SHA256_EMPTY: sha256::Hash = sha256::Hash::const_hash(&[]); + +/// Interpret the bits of the input byte as a signed 7-bit integer and return the +/// value as an i8. +fn read_i7(input: u8) -> i8 { + let masked = input & 0x7f; + if (masked & 0x40) == 0 { + masked as i8 + } else { + 0i8 - ((!(masked-1)) & 0x7f) as i8 + } +} + +/// Interpret the bits of the input bytes as a signed 15-bit integer and return the +/// value as an i16. +fn read_i15(input: u16) -> i16 { + let masked = input & 0x7fff; + if (masked & 0x4000) == 0 { + masked as i16 + } else { + 0i16 - ((!(masked-1)) & 0x7fff) as i16 + } +} + +fn convert_short_txfs(txfs: u8) -> Result<[u8; 4], &'static str> { + let mut base = TXFS_VERSION | TXFS_LOCKTIME | TXFS_CONTROL | TXFS_CURRENT_INPUT_TAPROOT_ANNEX; + let mut inout_fields = TXFS_OUTPUTS_ALL | TXFS_INPUTS_SEQUENCES | TXFS_INPUTS_SCRIPTSIGS; + + let input_selection = match txfs & 0b00000011 { + 0b00000000 => TXFS_INOUT_SELECTION_NONE, + 0b00000001 => TXFS_INOUT_SELECTION_CURRENT, + 0b00000011 => TXFS_INOUT_SELECTION_ALL, + _ => return Err("0b10 is not a valid input selection"), + }; + let output_selection = match txfs & 0b00001100 { + 0b00000000 => TXFS_INOUT_SELECTION_NONE, + 0b00000100 => TXFS_INOUT_SELECTION_CURRENT, + 0b00001100 => TXFS_INOUT_SELECTION_ALL, + _ => return Err("0b10 is not a valid output selection"), + }; + + if txfs & 0b00010000 != 0 { + inout_fields = inout_fields | TXFS_INPUTS_PREVOUTS; + } + + if txfs & 0b00100000 != 0 { + inout_fields = inout_fields | TXFS_INPUTS_PREV_SCRIPTPUBKEYS | TXFS_INPUTS_PREV_VALUES; + } + + if txfs & 0b01000000 != 0 { + base = base | TXFS_CURRENT_INPUT_CONTROL_BLOCK | TXFS_CURRENT_INPUT_SPENTSCRIPT + | TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS; + } + + if txfs & 0b10000000 != 0 { + base = base | TXFS_CURRENT_INPUT_IDX; + } + + Ok([base, inout_fields, input_selection, output_selection]) +} + +/// Parse an input or output selection from the TxFieldSelector bytes. +/// +/// Returns the selected indices and a flag whether to commit the number of items. +fn parse_inout_selection( + first_byte: u8, + bytes: &mut impl Iterator, + nb_items: usize, + current_input_idx: u32, +) -> Result<(Vec, bool), &'static str> { + let commit_number = (first_byte & TXFS_INOUT_NUMBER) != 0; + let selection = first_byte & (0xff ^ TXFS_INOUT_NUMBER); + + let selected = if selection == TXFS_INOUT_SELECTION_NONE { + vec![] + } else if selection == TXFS_INOUT_SELECTION_ALL { + (0..nb_items).collect() + } else if selection == TXFS_INOUT_SELECTION_CURRENT { + if current_input_idx as usize >= nb_items { + // NB can only happen for outputs + return Err("current input index exceeds number of outputs and current output selected"); + } + vec![current_input_idx as usize] + } else if (selection & TXFS_INOUT_SELECTION_MODE) == 0 { + // leading mode + let count = if (selection & TXFS_INOUT_LEADING_SIZE) == 0 { + (selection & TXFS_INOUT_SELECTION_MASK) as usize + } else { + let next_byte = bytes.next().ok_or("second leading selection byte missing")?; + (((selection & TXFS_INOUT_SELECTION_MASK) as usize) << 8) + next_byte as usize + }; + assert_ne!(count, 0, "this should be interpreted as NONE above"); + if count > nb_items { + return Err("selected number of leading in/outputs out of bounds"); + } + (0..count).collect() + } else { + // individual mode + let absolute = (selection & TXFS_INOUT_INDIVIDUAL_MODE) == 0; + + let count = (selection & TXFS_INOUT_SELECTION_MASK) as usize; + + let mut selected = Vec::with_capacity(count as usize); + for _ in 0..count { + let first = bytes.next().ok_or("expected an index byte")?; + let single_byte = (first & (1 << 7)) == 0; + let number = if single_byte { + first as usize + } else { + let next_byte = bytes.next().ok_or("expected another index byte")?; + (((first & (1 << 7)) as usize) << 8) + next_byte as usize + }; + + let idx = if absolute { + number + } else { + let rel = if single_byte { + read_i7(number as u8) as isize + } else { + read_i15(number as u16) as isize + }; + + if rel.is_negative() && rel.abs() > current_input_idx as isize { + return Err("relative index out of bounds"); + } + (current_input_idx as isize + rel) as usize + }; + + if idx > nb_items { + return Err("selected index out of bounds"); + } + if let Some(last) = selected.last() { + if idx <= *last { + return Err("selected indices not in increasing order") + } + } + selected.push(idx); + } + selected + }; + Ok((selected, commit_number)) +} + +/// +/// +/// Assumes that TxFieldSelector is valid. +pub fn calculate_txhash( + txfs: &[u8], + tx: &Transaction, + prevouts: &[TxOut], + current_input_idx: u32, + current_input_last_codeseparator_pos: Option, +) -> Result { + assert_eq!(tx.input.len(), prevouts.len()); + + let txfs = if txfs.is_empty() { + TXFS_SPECIAL_TEMPLATE.to_vec() + } else if txfs.len() == 1 { + convert_short_txfs(txfs[0])?.to_vec() + } else { + txfs.to_vec() + }; + let txfs = &txfs; + + let mut engine = sha256::Hash::engine(); + + if (txfs[0] & TXFS_CONTROL) != 0 { + engine.input(txfs); + } + + let mut bytes = txfs.iter().copied().peekable(); + let global = bytes.next().unwrap(); + + if (global & TXFS_VERSION) != 0 { + tx.version.consensus_encode(&mut engine).unwrap(); + } + + if (global & TXFS_LOCKTIME) != 0 { + tx.lock_time.consensus_encode(&mut engine).unwrap(); + } + + if (global & TXFS_CURRENT_INPUT_IDX) != 0 { + (current_input_idx as u32).consensus_encode(&mut engine).unwrap(); + } + + let current_prevout = &prevouts[current_input_idx as usize]; + let current_input = &tx.input[current_input_idx as usize]; + + if (global & TXFS_CURRENT_INPUT_CONTROL_BLOCK) != 0 { + assert!(current_prevout.script_pubkey.is_p2tr(), "only active in taproot context"); + if let Some(cb) = current_input.witness.taproot_control_block() { + engine.input(&sha256::Hash::hash(&cb)[..]); + } else { + // keyspend + engine.input(&SHA256_EMPTY[..]); + } + } + + if (global & TXFS_CURRENT_INPUT_SPENTSCRIPT) != 0 { + assert!(current_prevout.script_pubkey.is_p2tr(), "only active in taproot context"); + if let Some(script) = current_input.witness.taproot_leaf_script() { + let mut eng = sha256::Hash::engine(); + script.version.to_consensus().consensus_encode(&mut eng).unwrap(); + script.script.consensus_encode(&mut eng).unwrap(); + engine.input(&sha256::Hash::from_engine(eng)[..]); + } else { + engine.input(&SHA256_EMPTY[..]); + } + } + + if (global & TXFS_CURRENT_INPUT_LAST_CODESEPARATOR_POS) != 0 { + let pos = current_input_last_codeseparator_pos.unwrap_or(u32::MAX); + (pos as u32).consensus_encode(&mut engine).unwrap(); + } + + if (global & TXFS_CURRENT_INPUT_TAPROOT_ANNEX) != 0 { + if let Some(annex) = current_input.witness.taproot_annex() { + engine.input(&sha256::Hash::hash(annex)[..]); + } else { + engine.input(&SHA256_EMPTY[..]); + } + } + + let inout_fields = bytes.next().unwrap_or(0x00); + + // Inputs + let (input_selection, commit_number_inputs) = if let Some(first_byte) = bytes.next() { + parse_inout_selection(first_byte, &mut bytes, tx.input.len(), current_input_idx)? + } else { + (vec![], false) + }; + + if commit_number_inputs { + (tx.input.len() as u32).consensus_encode(&mut engine).unwrap(); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREVOUTS) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + tx.input[*i].previous_output.consensus_encode(&mut engine).unwrap(); + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_SEQUENCES) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + tx.input[*i].sequence.consensus_encode(&mut engine).unwrap(); + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_SCRIPTSIGS) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + engine.input(&sha256::Hash::hash(&tx.input[*i].script_sig.as_bytes())[..]); + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREV_SCRIPTPUBKEYS) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + engine.input(&sha256::Hash::hash(&prevouts[*i].script_pubkey.as_bytes())[..]); + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_PREV_VALUES) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + prevouts[*i].value.consensus_encode(&mut engine).unwrap(); + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + if !input_selection.is_empty() && (inout_fields & TXFS_INPUTS_TAPROOT_ANNEXES) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &input_selection { + if prevouts[*i].script_pubkey.is_p2tr() { + if let Some(annex) = tx.input[*i].witness.taproot_annex() { + engine.input(&sha256::Hash::hash(annex)[..]); + } else { + engine.input(&SHA256_EMPTY[..]); + } + } else { + engine.input(&SHA256_EMPTY[..]); + } + } + sha256::Hash::from_engine(engine) + }; + engine.input(&hash[..]); + } + + // Outputs + let (output_selection, commit_number_outputs) = if let Some(first_byte) = bytes.next() { + parse_inout_selection(first_byte, &mut bytes, tx.output.len(), current_input_idx)? + } else { + (vec![], false) + }; + + if commit_number_outputs { + (tx.output.len() as u32).consensus_encode(&mut engine).unwrap(); + } + + if !output_selection.is_empty() && (inout_fields & TXFS_OUTPUTS_SCRIPTPUBKEYS) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &output_selection { + engine.input(&sha256::Hash::hash(&tx.output[*i].script_pubkey.as_bytes())[..]); + } + sha256::Hash::from_engine(engine) + }; + hash.consensus_encode(&mut engine).unwrap(); + } + + if !output_selection.is_empty() && (inout_fields & TXFS_OUTPUTS_VALUES) != 0 { + let hash = { + let mut engine = sha256::Hash::engine(); + for i in &output_selection { + tx.output[*i].value.consensus_encode(&mut engine).unwrap(); + } + sha256::Hash::from_engine(engine) + }; + hash.consensus_encode(&mut engine).unwrap(); + } + + if bytes.next().is_some() { + return Err("unexpected additional txfs bytes"); + } + Ok(sha256::Hash::from_engine(engine)) +} + +mod test_vectors { + use super::*; + use std::any::Any; + use std::ops::{self, RangeBounds}; + use bitcoin::hex::DisplayHex; + use bitcoin::{Amount, ScriptBuf, Sequence, Witness}; + use bitcoin::blockdata::transaction::{self, TxIn}; + use bitcoin::opcodes::all::*; + + fn test_vector_tx() -> (Transaction, Vec) { + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::from_consensus(42), + input: vec![ + TxIn { + previous_output: "3333333333333333333333333333333333333333333333333333333333333333:3".parse().unwrap(), + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(2), + witness: { + let mut buf = Witness::new(); + buf.push(vec![0x12]); + buf + }, + }, + TxIn { + previous_output: "4444444444444444444444444444444444444444444444444444444444444444:4".parse().unwrap(), + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(3), + witness: { + let mut buf = Witness::new(); + buf.push(vec![0x13]); + buf.push(vec![0x14]); + buf.push(vec![0x50, 0x42]); // annex + buf + }, + }, + TxIn { + previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1".parse().unwrap(), + script_sig: vec![0x23].into(), + sequence: Sequence::from_consensus(1), + witness: Witness::new(), + }, + TxIn { + previous_output: "2222222222222222222222222222222222222222222222222222222222222222:2".parse().unwrap(), + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(3), + witness: { // p2wsh annex-like stack element + let mut buf = Witness::new(); + buf.push(vec![0x13]); + buf.push(vec![0x14]); + buf.push(vec![0x50, 0x42]); // annex + buf + }, + }, + ], + output: vec![ + TxOut { + script_pubkey: vec![OP_PUSHNUM_6.to_u8()].into(), + value: Amount::from_sat(350), + }, + TxOut { + script_pubkey: vec![OP_PUSHNUM_7.to_u8()].into(), + value: Amount::from_sat(351), + }, + TxOut { + script_pubkey: vec![OP_PUSHNUM_8.to_u8()].into(), + value: Amount::from_sat(353), + }, + ], + }; + let prevs = vec![ + TxOut { + script_pubkey: vec![ // p2tr + 0x51, 0x20, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ].into(), + value: Amount::from_sat(361), + }, + TxOut { + script_pubkey: vec![ // p2tr + 0x51, 0x20, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ].into(), + value: Amount::from_sat(362), + }, + TxOut { + script_pubkey: vec![OP_PUSHNUM_16.to_u8()].into(), + value: Amount::from_sat(360), + }, + TxOut { + script_pubkey: vec![ // p2wsh + 0x00, 0x20, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ].into(), + value: Amount::from_sat(361), + }, + ]; + (tx, prevs) + } + + #[derive(Debug)] + struct TestCase { + tx: Transaction, + prevs: Vec, + vectors: Vec + } + + #[derive(Debug)] + struct TestVector { + txfs: Vec, + input: usize, + codeseparator: Option, + txhash: sha256::Hash, + } + + fn generate_vectors() -> Vec { + let all = 0xff; + let allio = TXFS_INPUTS_ALL | TXFS_OUTPUTS_ALL; + let selnone = TXFS_INOUT_SELECTION_NONE; // 0x00 + let selcur = TXFS_INOUT_SELECTION_CURRENT; + let selall = TXFS_INOUT_SELECTION_ALL; + let number = TXFS_INOUT_NUMBER; + let leading = 0; + let individual = TXFS_INOUT_SELECTION_MODE; + let absolute = 0; + let relative = TXFS_INOUT_INDIVIDUAL_MODE; + + fn r + 'static>(t: T) -> Option> { + Some(Box::new(t)) + } + + // txfs and range of inputs to run it on + let selectors: &[(&[u8], Option>)] = &[ + // global + (&[1 << 0, 0], None), + (&[1 << 1, 0], None), + (&[1 << 2, 0], None), + (&[1 << 3, 0], None), + (&[1 << 4, 0], None), + (&[1 << 5, 0], None), + (&[1 << 6, 0], None), + (&[1 << 7, 0], None), + // outputs + (&[all, 0, 0, number | selnone], None), + (&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, selcur], None), + (&[all, TXFS_OUTPUTS_VALUES, 0, selcur], None), + (&[all, TXFS_OUTPUTS_ALL, 0, selcur], None), + (&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, selall], None), + (&[all, TXFS_OUTPUTS_VALUES, 0, selall], None), + (&[all, TXFS_OUTPUTS_ALL, 0, selall], None), + (&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, number | selcur], None), + (&[all, TXFS_OUTPUTS_VALUES, 0, number | selcur], None), + (&[all, TXFS_OUTPUTS_ALL, 0, number | selcur], None), + (&[all, TXFS_OUTPUTS_SCRIPTPUBKEYS, 0, number | selall], None), + (&[all, TXFS_OUTPUTS_VALUES, 0, number | selall], None), + (&[all, TXFS_OUTPUTS_ALL, 0, number | selall], None), + // inputs + (&[all, 0, number | selnone], None), + (&[all, TXFS_INPUTS_PREVOUTS, selcur], None), + (&[all, TXFS_INPUTS_SEQUENCES, selcur], None), + (&[all, TXFS_INPUTS_SCRIPTSIGS, selcur], None), + (&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, selcur], None), + (&[all, TXFS_INPUTS_PREV_VALUES, selcur], None), + (&[all, TXFS_INPUTS_TAPROOT_ANNEXES, selcur], None), + (&[all, TXFS_INPUTS_ALL, selcur], None), + (&[all, TXFS_INPUTS_PREVOUTS, selall], None), + (&[all, TXFS_INPUTS_SEQUENCES, selall], None), + (&[all, TXFS_INPUTS_SCRIPTSIGS, selall], None), + (&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, selall], None), + (&[all, TXFS_INPUTS_PREV_VALUES, selall], None), + (&[all, TXFS_INPUTS_TAPROOT_ANNEXES, selall], None), + (&[all, TXFS_INPUTS_ALL, selall], None), + (&[all, TXFS_INPUTS_PREVOUTS, number | selcur], None), + (&[all, TXFS_INPUTS_SEQUENCES, number | selcur], None), + (&[all, TXFS_INPUTS_SCRIPTSIGS, number | selcur], None), + (&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, number | selcur], None), + (&[all, TXFS_INPUTS_PREV_VALUES, number | selcur], None), + (&[all, TXFS_INPUTS_TAPROOT_ANNEXES, number | selcur], None), + (&[all, TXFS_INPUTS_ALL, number | selcur], None), + (&[all, TXFS_INPUTS_PREVOUTS, number | selall], None), + (&[all, TXFS_INPUTS_SEQUENCES, number | selall], None), + (&[all, TXFS_INPUTS_SCRIPTSIGS, number | selall], None), + (&[all, TXFS_INPUTS_PREV_SCRIPTPUBKEYS, number | selall], None), + (&[all, TXFS_INPUTS_PREV_VALUES, number | selall], None), + (&[all, TXFS_INPUTS_TAPROOT_ANNEXES, number | selall], None), + (&[all, TXFS_INPUTS_ALL, number | selall], None), + // both + (&[all, allio, selall, selall], None), + (&[all, allio, selcur, selcur], None), + (&[all, 0, number | selnone, number | selnone], None), + (&[all, allio, number | selall, number | selall], None), + (&[all, allio, number | selcur, number | selcur], None), + (&[all, allio, selcur, selall], None), + (&[all, allio, selall, selcur], None), + // leading + (&[all, allio, leading | 0x01, number | leading | 0x02], None), + (&[all, allio, number | selcur, leading | 0x02], None), + // individual absolute + (&[all, allio, individual | absolute | 0x01, 0x01, + individual | absolute | 0x02, 0x00, 0x02], None), + (&[all, allio, number | individual | absolute | 0x01, 0x01, + number | individual | absolute | 0x02, 0x00, 0x02], None), + // individual relative + (&[all, allio, individual | relative | 0x01, (-1i8 as u8) >> 1, + individual | relative | 0x02, (-1i8 as u8) >> 1, 0], r(1..2)), + (&[all, allio, number | individual | relative | 0x01, (-1i8 as u8) >> 1, + number | individual | relative | 0x02, (-1i8 as u8) >> 1, 0], r(1..2)), + //TODO(stevenroose) test index size, but for that we need > 32 in/outputs + // special case template + (&[], None), + // shorthand txfs, sighash examples + (&[0b11111111], None), + (&[0b11110111], None), + (&[0b11110011], None), + (&[0b11111101], None), + (&[0b11110101], None), + (&[0b11110001], None), + (&[0b11101101], None), + (&[0b11100101], None), + (&[0b11100001], None), + (&[0b11001101], None), + (&[0b11000101], None), + (&[0b11000001], None), + ]; + + let cases = vec![ + test_vector_tx(), + ]; + + fn check_range(r: &Box, idx: usize) -> bool { + if let Some(ref range) = r.downcast_ref::() { + return range.contains(&idx); + } + if let Some(ref range) = r.downcast_ref::>() { + return range.contains(&idx); + } + if let Some(ref range) = r.downcast_ref::>() { + return range.contains(&idx); + } + if let Some(ref range) = r.downcast_ref::>() { + return range.contains(&idx); + } + unreachable!("invalid range type used: {:?}", r.type_id()); + } + + cases.into_iter().enumerate().map(|(cidx, (tx, prevs))| { + let mut vectors = Vec::new(); + for (_sidx, (txfs, idx_range)) in selectors.iter().enumerate() { + for i in 0..tx.input.len() { + let default = r(..2); // only 2 fist inputs are taproot + let range = idx_range.as_ref().unwrap_or(default.as_ref().unwrap()); + if !check_range(range, i) { + continue; + } + // println!("{} >> #{} ({}) >> {}", cidx, _sidx, txfs.as_hex(), i); + + match calculate_txhash(txfs, &tx, &prevs, i as u32, None) { + Ok(txhash) => vectors.push(TestVector { + txfs: txfs.to_vec(), + input: i, + codeseparator: None, + txhash: txhash, + }), + Err(e) => panic!("Error in vector #{} for selector {}: {}", + cidx, txfs.as_hex(), e, + ), + } + } + } + TestCase { tx, prevs, vectors } + }).collect() + } + + pub fn write_vector_file(path: impl AsRef) { + use bitcoin::consensus::encode::serialize_hex; + + let ret = generate_vectors().into_iter().enumerate().map(|(i_tx, c)| serde_json::json!({ + "tx": serialize_hex(&c.tx), + "prevs": c.prevs.iter().map(|p| serialize_hex(p)).collect::>(), + "vectors": c.vectors.into_iter().enumerate().map(|(i_v, v)| serde_json::json!({ + "id": format!("{}:{} ({} #{})", i_tx, i_v, v.txfs.as_hex(), v.input), + "txfs": v.txfs.as_hex().to_string(), + "input": v.input, + "codeseparator": v.codeseparator, + "txhash": v.txhash, + })).collect::>(), + })).collect::>(); + + let mut file = std::fs::File::create(path).unwrap(); + serde_json::to_writer_pretty(&mut file, &ret).unwrap(); + } +} + +fn main() { + test_vectors::write_vector_file("./txhash_vectors.json"); +} diff --git a/bip-0346/ref-impl/txhash_vectors.json b/bip-0346/ref-impl/txhash_vectors.json new file mode 100644 index 0000000000..232bc18542 --- /dev/null +++ b/bip-0346/ref-impl/txhash_vectors.json @@ -0,0 +1,1063 @@ +[ + { + "prevs": [ + "69010000000000002251200100000000000000000000000000000000000000000000000000000000000000", + "6a010000000000002251200200000000000000000000000000000000000000000000000000000000000000", + "68010000000000000160", + "69010000000000002200200100000000000000000000000000000000000000000000000000000000000000" + ], + "tx": "02000000000104333333333333333333333333333333333333333333333333333333333333333303000000000200000044444444444444444444444444444444444444444444444444444444444444440400000000030000001111111111111111111111111111111111111111111111111111111111111111010000000123010000002222222222222222222222222222222222222222222222222222222222222222020000000003000000035e0100000000000001565f0100000000000001576101000000000000015801011203011301140250420003011301140250422a000000", + "vectors": [ + { + "codeseparator": null, + "id": "0:0 (0100 #0)", + "input": 0, + "txfs": "0100", + "txhash": "26b25d457597a7b0463f9620f666dd10aa2c4373a505967c7c8d70922a2d6ece" + }, + { + "codeseparator": null, + "id": "0:1 (0100 #1)", + "input": 1, + "txfs": "0100", + "txhash": "26b25d457597a7b0463f9620f666dd10aa2c4373a505967c7c8d70922a2d6ece" + }, + { + "codeseparator": null, + "id": "0:2 (0200 #0)", + "input": 0, + "txfs": "0200", + "txhash": "e8a4b2ee7ede79a3afb332b5b6cc3d952a65fd8cffb897f5d18016577c33d7cc" + }, + { + "codeseparator": null, + "id": "0:3 (0200 #1)", + "input": 1, + "txfs": "0200", + "txhash": "e8a4b2ee7ede79a3afb332b5b6cc3d952a65fd8cffb897f5d18016577c33d7cc" + }, + { + "codeseparator": null, + "id": "0:4 (0400 #0)", + "input": 0, + "txfs": "0400", + "txhash": "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119" + }, + { + "codeseparator": null, + "id": "0:5 (0400 #1)", + "input": 1, + "txfs": "0400", + "txhash": "67abdd721024f0ff4e0b3f4c2fc13bc5bad42d0b7851d456d88d203d15aaa450" + }, + { + "codeseparator": null, + "id": "0:6 (0800 #0)", + "input": 0, + "txfs": "0800", + "txhash": "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456" + }, + { + "codeseparator": null, + "id": "0:7 (0800 #1)", + "input": 1, + "txfs": "0800", + "txhash": "d703d3da6a87bd8e0b453f3b6c41edcc9bf331b2b88ef26eb39dc7abee4e00a3" + }, + { + "codeseparator": null, + "id": "0:8 (1000 #0)", + "input": 0, + "txfs": "1000", + "txhash": "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456" + }, + { + "codeseparator": null, + "id": "0:9 (1000 #1)", + "input": 1, + "txfs": "1000", + "txhash": "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456" + }, + { + "codeseparator": null, + "id": "0:10 (2000 #0)", + "input": 0, + "txfs": "2000", + "txhash": "ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e" + }, + { + "codeseparator": null, + "id": "0:11 (2000 #1)", + "input": 1, + "txfs": "2000", + "txhash": "ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e" + }, + { + "codeseparator": null, + "id": "0:12 (4000 #0)", + "input": 0, + "txfs": "4000", + "txhash": "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456" + }, + { + "codeseparator": null, + "id": "0:13 (4000 #1)", + "input": 1, + "txfs": "4000", + "txhash": "227747766d19539b54f018e7ccfde16bd7c38ebbf5649357ecf67bdfb9755b5c" + }, + { + "codeseparator": null, + "id": "0:14 (8000 #0)", + "input": 0, + "txfs": "8000", + "txhash": "8509b81230019d2ad970d970f791dfbdc8caf54f5c594fcd327cef9feed206c1" + }, + { + "codeseparator": null, + "id": "0:15 (8000 #1)", + "input": 1, + "txfs": "8000", + "txhash": "8509b81230019d2ad970d970f791dfbdc8caf54f5c594fcd327cef9feed206c1" + }, + { + "codeseparator": null, + "id": "0:16 (ff000080 #0)", + "input": 0, + "txfs": "ff000080", + "txhash": "6286469e7f9ac95ff97d41ab1cf8a1d8a3872e45f5bac80a1acf8fd9d4397c18" + }, + { + "codeseparator": null, + "id": "0:17 (ff000080 #1)", + "input": 1, + "txfs": "ff000080", + "txhash": "74256c5211c8209c1be86fa0d7b92993a03d5e19647b5d1f83fdc65ebee51b86" + }, + { + "codeseparator": null, + "id": "0:18 (ff400040 #0)", + "input": 0, + "txfs": "ff400040", + "txhash": "66c3a2d99e0eccce1e769d61b879f6003252912982a9efc138b24365d6c68879" + }, + { + "codeseparator": null, + "id": "0:19 (ff400040 #1)", + "input": 1, + "txfs": "ff400040", + "txhash": "f1dfe060222dfa92e1f91d5afd5790ac9de5b1ff5db777282c85e0a968c25d0e" + }, + { + "codeseparator": null, + "id": "0:20 (ff800040 #0)", + "input": 0, + "txfs": "ff800040", + "txhash": "52bc63b1b02c3d375d13236938b25d5249df08fb4d891686a70e5702ea12c0e4" + }, + { + "codeseparator": null, + "id": "0:21 (ff800040 #1)", + "input": 1, + "txfs": "ff800040", + "txhash": "40351ff507344dd2493e80df868843bac4cff20c456e2ca28cfa39b2a0cf0557" + }, + { + "codeseparator": null, + "id": "0:22 (ffc00040 #0)", + "input": 0, + "txfs": "ffc00040", + "txhash": "75ae3a008d82c09aa7afd34abd46f03afe940758d33763b188d1fa2bd068cd80" + }, + { + "codeseparator": null, + "id": "0:23 (ffc00040 #1)", + "input": 1, + "txfs": "ffc00040", + "txhash": "6c3adf154dec8e01f323c0ac20ee5731bfbc5af4259a9a87c8ad0c77dd9c4417" + }, + { + "codeseparator": null, + "id": "0:24 (ff40003f #0)", + "input": 0, + "txfs": "ff40003f", + "txhash": "8dea16353d8dbb04764bc5a21fa36236aa3f736761cbb9be809a0a870fa6a366" + }, + { + "codeseparator": null, + "id": "0:25 (ff40003f #1)", + "input": 1, + "txfs": "ff40003f", + "txhash": "f3d00fee00e8841e38f465e211ede292fde56e1223c0f211786bc1d357593545" + }, + { + "codeseparator": null, + "id": "0:26 (ff80003f #0)", + "input": 0, + "txfs": "ff80003f", + "txhash": "d0b05b4c01125da54d473da6d5f21bef264cfa236e9dde70ff140027cef94931" + }, + { + "codeseparator": null, + "id": "0:27 (ff80003f #1)", + "input": 1, + "txfs": "ff80003f", + "txhash": "5eb3c700160c0b8b185babc8d2f2be4370f666c660ddb2113a187aacbfbf2b59" + }, + { + "codeseparator": null, + "id": "0:28 (ffc0003f #0)", + "input": 0, + "txfs": "ffc0003f", + "txhash": "a28ff0cd1109352b4dcbf74a0d2a6c73732235b532bb8284ad388b8e49838ea5" + }, + { + "codeseparator": null, + "id": "0:29 (ffc0003f #1)", + "input": 1, + "txfs": "ffc0003f", + "txhash": "ec672278f9ab6f4a21e9406721c27173e7887b75ab82cdee81988692324b3e47" + }, + { + "codeseparator": null, + "id": "0:30 (ff4000c0 #0)", + "input": 0, + "txfs": "ff4000c0", + "txhash": "af6e1adf1ff16c7bae08e4127b30dab4fedee683277c8b65e27897e9013d63cf" + }, + { + "codeseparator": null, + "id": "0:31 (ff4000c0 #1)", + "input": 1, + "txfs": "ff4000c0", + "txhash": "a0f1b5db81c1bba78772f6306107809c29c2f01d41e5d74e39fcf3463e666a91" + }, + { + "codeseparator": null, + "id": "0:32 (ff8000c0 #0)", + "input": 0, + "txfs": "ff8000c0", + "txhash": "8ce6884e8b3a11d81f69b9642981770cf2f7ed59cd7748e66f443e10cb16094d" + }, + { + "codeseparator": null, + "id": "0:33 (ff8000c0 #1)", + "input": 1, + "txfs": "ff8000c0", + "txhash": "d62d70495e26ba054e60328d35595ea3caf74d8faec2a34e08fb0da8ac6e4514" + }, + { + "codeseparator": null, + "id": "0:34 (ffc000c0 #0)", + "input": 0, + "txfs": "ffc000c0", + "txhash": "29c5f8ace77d67bc684be308d8bba076ddbccb7db86890f1affde88243111d66" + }, + { + "codeseparator": null, + "id": "0:35 (ffc000c0 #1)", + "input": 1, + "txfs": "ffc000c0", + "txhash": "9d7f7313cf89e95b4984f6864fe6103e26fe02d8e930221d1bdf47b8222b5d8d" + }, + { + "codeseparator": null, + "id": "0:36 (ff4000bf #0)", + "input": 0, + "txfs": "ff4000bf", + "txhash": "a111bf0e5c46831243ec5d8095ffdb959b316c00e00296dfce12b5dc817f1d29" + }, + { + "codeseparator": null, + "id": "0:37 (ff4000bf #1)", + "input": 1, + "txfs": "ff4000bf", + "txhash": "1ac5b588e154bbf4cf1a462eb83718bcd72c1b544867300004950afa15a416a0" + }, + { + "codeseparator": null, + "id": "0:38 (ff8000bf #0)", + "input": 0, + "txfs": "ff8000bf", + "txhash": "2ecd12402dd1443629037d5864d5464d9c8812bd44449bfc25b16b0c76bfff79" + }, + { + "codeseparator": null, + "id": "0:39 (ff8000bf #1)", + "input": 1, + "txfs": "ff8000bf", + "txhash": "c5a5ac672a574311029e25b4d381f844745aad80e23040119aa9df76570d9ae2" + }, + { + "codeseparator": null, + "id": "0:40 (ffc000bf #0)", + "input": 0, + "txfs": "ffc000bf", + "txhash": "6b7d5990b3435e42a4212c88967eb0f9c8b3c3738ecc2fc79d4b48bb386f178c" + }, + { + "codeseparator": null, + "id": "0:41 (ffc000bf #1)", + "input": 1, + "txfs": "ffc000bf", + "txhash": "ba92bbf9b3151efaaa0582c5db076066fdc842037daf5c8d51ee425130cd2181" + }, + { + "codeseparator": null, + "id": "0:42 (ff0080 #0)", + "input": 0, + "txfs": "ff0080", + "txhash": "3a23df77318fd630a1b004bd49ee23e2f6ffb588a3e38d50577a307401f67127" + }, + { + "codeseparator": null, + "id": "0:43 (ff0080 #1)", + "input": 1, + "txfs": "ff0080", + "txhash": "c8c9cb44b4405c2efb77e6fd2a1258d4479042d6dc1fb51bf8aa0448f6514635" + }, + { + "codeseparator": null, + "id": "0:44 (ff0140 #0)", + "input": 0, + "txfs": "ff0140", + "txhash": "e2fcdd4957e7891edac19f58776806218069192b0e94d1d77260a555342d06ac" + }, + { + "codeseparator": null, + "id": "0:45 (ff0140 #1)", + "input": 1, + "txfs": "ff0140", + "txhash": "3f87e78b8be3e4b28b29c255bd5ed7c10ab69b02963b02b9394aee1b5f788434" + }, + { + "codeseparator": null, + "id": "0:46 (ff0240 #0)", + "input": 0, + "txfs": "ff0240", + "txhash": "2589b0924c45f17484e681c5df41fa136833c44b985ef3fbf17974ca46d7ecd4" + }, + { + "codeseparator": null, + "id": "0:47 (ff0240 #1)", + "input": 1, + "txfs": "ff0240", + "txhash": "871bc1ff067094336c1860b01a38673cedce057568f036202e7a3e2410847e44" + }, + { + "codeseparator": null, + "id": "0:48 (ff0440 #0)", + "input": 0, + "txfs": "ff0440", + "txhash": "91509f1fd4fb675a936c5cf9c79528ea6441750f41ca8851125c1d78f1880a6c" + }, + { + "codeseparator": null, + "id": "0:49 (ff0440 #1)", + "input": 1, + "txfs": "ff0440", + "txhash": "6e18b0171ce7aee2307a4e9ecc1e8d625d07116063e52604a85bc6d1064cfc32" + }, + { + "codeseparator": null, + "id": "0:50 (ff0840 #0)", + "input": 0, + "txfs": "ff0840", + "txhash": "d9f97f4e62518332b2bd5d0bad604973d5dfef0945a95685d258ce9743d079ec" + }, + { + "codeseparator": null, + "id": "0:51 (ff0840 #1)", + "input": 1, + "txfs": "ff0840", + "txhash": "0575703690b0c557080ff3bef27cfe517abacbe420aa9ddaf53cfe3c733ec077" + }, + { + "codeseparator": null, + "id": "0:52 (ff1040 #0)", + "input": 0, + "txfs": "ff1040", + "txhash": "eb53fa90d5e2617ec342007a1bf7ac62dbe66c5147ba60a294a7db1704620f75" + }, + { + "codeseparator": null, + "id": "0:53 (ff1040 #1)", + "input": 1, + "txfs": "ff1040", + "txhash": "6dc2f01a3b22b761df91d9a6ba9b58d85f26d881585bd252fcf75eed4ab74e94" + }, + { + "codeseparator": null, + "id": "0:54 (ff2040 #0)", + "input": 0, + "txfs": "ff2040", + "txhash": "1a7a2897721b14ebabac2dd83964323864d6f54610a21c255b7a61be4042f269" + }, + { + "codeseparator": null, + "id": "0:55 (ff2040 #1)", + "input": 1, + "txfs": "ff2040", + "txhash": "953b14f68d47206c29b780c13234a8e2017804367c4b67012626d19e2f21ceff" + }, + { + "codeseparator": null, + "id": "0:56 (ff3f40 #0)", + "input": 0, + "txfs": "ff3f40", + "txhash": "e94869fa522e4acd229958b360dd6b9d87f9e59fcead016875b0f97f10c18595" + }, + { + "codeseparator": null, + "id": "0:57 (ff3f40 #1)", + "input": 1, + "txfs": "ff3f40", + "txhash": "3262c8c9f7b2538e123f11e7b31aa7c7c5f6c7bf031ac89ead194991a9df74b1" + }, + { + "codeseparator": null, + "id": "0:58 (ff013f #0)", + "input": 0, + "txfs": "ff013f", + "txhash": "3ab80fffdf1301f056762b0e2e09c11699a479adbaa1bea38a95a6ec49e94c0e" + }, + { + "codeseparator": null, + "id": "0:59 (ff013f #1)", + "input": 1, + "txfs": "ff013f", + "txhash": "2ccab90e40aaab0304458362ba31ce910c41ed446ad14b15a15613a84be0a147" + }, + { + "codeseparator": null, + "id": "0:60 (ff023f #0)", + "input": 0, + "txfs": "ff023f", + "txhash": "29ebfe25e2d5e33a416808b36e2d23538c7e94c77ada313fd78a3137471fb618" + }, + { + "codeseparator": null, + "id": "0:61 (ff023f #1)", + "input": 1, + "txfs": "ff023f", + "txhash": "eddeb813dabe380be2f59cf04b4e0528a2033933ffea03bb49b45a262ca2d566" + }, + { + "codeseparator": null, + "id": "0:62 (ff043f #0)", + "input": 0, + "txfs": "ff043f", + "txhash": "ebd4f5e2636391b007d0c1e68e0d088d6e5a638a3e40c00e322b0a9363b3e477" + }, + { + "codeseparator": null, + "id": "0:63 (ff043f #1)", + "input": 1, + "txfs": "ff043f", + "txhash": "9f1cd389104b23b29d534b16d06ba3f0839d58d443f798ada9155bf2f4ebe4f6" + }, + { + "codeseparator": null, + "id": "0:64 (ff083f #0)", + "input": 0, + "txfs": "ff083f", + "txhash": "4b596fbefb903426720ba9a3906306371253d2de874537e722a47de29c925647" + }, + { + "codeseparator": null, + "id": "0:65 (ff083f #1)", + "input": 1, + "txfs": "ff083f", + "txhash": "ef50137ead871d0dd9c1b4c6cb0c57ea00f7650d111897d0bcd0441622e9375d" + }, + { + "codeseparator": null, + "id": "0:66 (ff103f #0)", + "input": 0, + "txfs": "ff103f", + "txhash": "874afe3e5a88a2cc3afc3239ea78e6d0cd91c83ae384880830f0daf1096838d1" + }, + { + "codeseparator": null, + "id": "0:67 (ff103f #1)", + "input": 1, + "txfs": "ff103f", + "txhash": "7002a2f133407c98c08f17caac86fdeac6e5f2ae86c059567028a9e2f2718929" + }, + { + "codeseparator": null, + "id": "0:68 (ff203f #0)", + "input": 0, + "txfs": "ff203f", + "txhash": "589f39f55732242d97de0fdb8aa8a3736b197d4f319885c32651cd565021da6b" + }, + { + "codeseparator": null, + "id": "0:69 (ff203f #1)", + "input": 1, + "txfs": "ff203f", + "txhash": "46fc739044047fc00053ec7532ceee2ea53fe75029a2d65c90d5da2738937b8e" + }, + { + "codeseparator": null, + "id": "0:70 (ff3f3f #0)", + "input": 0, + "txfs": "ff3f3f", + "txhash": "ece064b4f1551cfbcff6792e32842bb08335fb2cb5ee430f7707a00d15f89c41" + }, + { + "codeseparator": null, + "id": "0:71 (ff3f3f #1)", + "input": 1, + "txfs": "ff3f3f", + "txhash": "e41fd96f9d8f48c6ac04ce5039d850535b668c3b8b83f3913e2a131edfe5b4df" + }, + { + "codeseparator": null, + "id": "0:72 (ff01c0 #0)", + "input": 0, + "txfs": "ff01c0", + "txhash": "35aa24440a7e833ed0f6503688a7a7009ebc1999cebd58c10724a2d321d41174" + }, + { + "codeseparator": null, + "id": "0:73 (ff01c0 #1)", + "input": 1, + "txfs": "ff01c0", + "txhash": "d3bbb1e58851725a94a5538189b5dad23c9c7cec53549f959e6f498dce737828" + }, + { + "codeseparator": null, + "id": "0:74 (ff02c0 #0)", + "input": 0, + "txfs": "ff02c0", + "txhash": "79ee870774e05726bbf47780154ff33e9ded3c67bae7ec02a12663106adbcc68" + }, + { + "codeseparator": null, + "id": "0:75 (ff02c0 #1)", + "input": 1, + "txfs": "ff02c0", + "txhash": "01bf86cb8dd28adf6807c505b4c538a2490c213be8b1224b55c7e4b5ab523d97" + }, + { + "codeseparator": null, + "id": "0:76 (ff04c0 #0)", + "input": 0, + "txfs": "ff04c0", + "txhash": "8dbd343cd6dd7f92c0c9e0c278d6189f8ae08400e7ffbe0e932fb4675bc0d4f1" + }, + { + "codeseparator": null, + "id": "0:77 (ff04c0 #1)", + "input": 1, + "txfs": "ff04c0", + "txhash": "8b566825b9c36b5e0b6916827ad7573cfe74f8a903c1ff06a06329aaa28b9c1d" + }, + { + "codeseparator": null, + "id": "0:78 (ff08c0 #0)", + "input": 0, + "txfs": "ff08c0", + "txhash": "98d344d199bb540a91c6a9d70e3daa3ead46c30a377470ffbe1199d12a606aec" + }, + { + "codeseparator": null, + "id": "0:79 (ff08c0 #1)", + "input": 1, + "txfs": "ff08c0", + "txhash": "b68c12f03396664d494b2af4363efc89667f76d9bf962e52c2a52fcb0ed877b9" + }, + { + "codeseparator": null, + "id": "0:80 (ff10c0 #0)", + "input": 0, + "txfs": "ff10c0", + "txhash": "2fb64ddf05090b11ec14848963af6aac97ea2394623e66d8f9237eb712b5bbe8" + }, + { + "codeseparator": null, + "id": "0:81 (ff10c0 #1)", + "input": 1, + "txfs": "ff10c0", + "txhash": "8eaa22ece3e6e61340a013c607ca487270b14143fdb666d021879f5c84fbf661" + }, + { + "codeseparator": null, + "id": "0:82 (ff20c0 #0)", + "input": 0, + "txfs": "ff20c0", + "txhash": "70bf9abee31014fc3e454049535e115948ea16022323d58eda379801763b00c5" + }, + { + "codeseparator": null, + "id": "0:83 (ff20c0 #1)", + "input": 1, + "txfs": "ff20c0", + "txhash": "5abbe67209f513d56380f45578dc5479ab76e3ed9121f4d100dca09793da2584" + }, + { + "codeseparator": null, + "id": "0:84 (ff3fc0 #0)", + "input": 0, + "txfs": "ff3fc0", + "txhash": "791a302cbb5e2a711ef54911a62b9968223bfda2d6c1f91f72ad8680883b4568" + }, + { + "codeseparator": null, + "id": "0:85 (ff3fc0 #1)", + "input": 1, + "txfs": "ff3fc0", + "txhash": "5449e0e4fd52db193c70b0804fafd53093753b936cbf6582c33e375ca0c55ed2" + }, + { + "codeseparator": null, + "id": "0:86 (ff01bf #0)", + "input": 0, + "txfs": "ff01bf", + "txhash": "0c86d8789f7a98e7aed70940dd78a1ea6b42d3a4adf16fe343d6ddac78c5fdc3" + }, + { + "codeseparator": null, + "id": "0:87 (ff01bf #1)", + "input": 1, + "txfs": "ff01bf", + "txhash": "770c254d1a603e09243bd0d193ec89aaaee20958abe3c2249c57565cfb3ebc90" + }, + { + "codeseparator": null, + "id": "0:88 (ff02bf #0)", + "input": 0, + "txfs": "ff02bf", + "txhash": "b2f3145c3caaddb1344fd170e62eadd93b34adb5a8262c8e1c2d542f8ce0e045" + }, + { + "codeseparator": null, + "id": "0:89 (ff02bf #1)", + "input": 1, + "txfs": "ff02bf", + "txhash": "fbf7a5bec0037e3ed35f0fe243688f6e0fe3583adce9ca221af02bed1cced8cb" + }, + { + "codeseparator": null, + "id": "0:90 (ff04bf #0)", + "input": 0, + "txfs": "ff04bf", + "txhash": "d08f2c883d8bc078ee42aa2647307d455a53caeeda3a2d1ce1bcaaaf223b2dc8" + }, + { + "codeseparator": null, + "id": "0:91 (ff04bf #1)", + "input": 1, + "txfs": "ff04bf", + "txhash": "0db8760a71982cf0c354be8b286e1a202695aac3c18a952ec0af6c79247a50bd" + }, + { + "codeseparator": null, + "id": "0:92 (ff08bf #0)", + "input": 0, + "txfs": "ff08bf", + "txhash": "acb8c88af64ecd6c65931c7549e6e913c7eb8a6305f5abdea1c21ada1c57c1fb" + }, + { + "codeseparator": null, + "id": "0:93 (ff08bf #1)", + "input": 1, + "txfs": "ff08bf", + "txhash": "528f4d8c24cc929b21b2bf2306c12225b4a58119a57b46905a4748117a1f671e" + }, + { + "codeseparator": null, + "id": "0:94 (ff10bf #0)", + "input": 0, + "txfs": "ff10bf", + "txhash": "dfc30a7df4cd7f105274ce5913b56e1fdb15f842d16259cac5e2142b8e328b72" + }, + { + "codeseparator": null, + "id": "0:95 (ff10bf #1)", + "input": 1, + "txfs": "ff10bf", + "txhash": "fc7eb663ecafee91eaeb327be8ce4141eba15be928411b13b1145e825a25c7a5" + }, + { + "codeseparator": null, + "id": "0:96 (ff20bf #0)", + "input": 0, + "txfs": "ff20bf", + "txhash": "68ddc75edee9e075116b3a92da91934b873bc5319b7d843964a1bdbbcc98a3fc" + }, + { + "codeseparator": null, + "id": "0:97 (ff20bf #1)", + "input": 1, + "txfs": "ff20bf", + "txhash": "70b258975fd720f72300eb935e9ad1d6cb627201535d205ea2547f6379290f2c" + }, + { + "codeseparator": null, + "id": "0:98 (ff3fbf #0)", + "input": 0, + "txfs": "ff3fbf", + "txhash": "af99fb1bb1d843814f205517718b70ff2729c3cc0169e97863d4de1b5f97d102" + }, + { + "codeseparator": null, + "id": "0:99 (ff3fbf #1)", + "input": 1, + "txfs": "ff3fbf", + "txhash": "d51e4053eae324623ac8b45e6c756f54dc4f674c9024b1739976e481b58e4c29" + }, + { + "codeseparator": null, + "id": "0:100 (ffff3f3f #0)", + "input": 0, + "txfs": "ffff3f3f", + "txhash": "2b8b2247712581368099f351dc3c3fea0ee5a46158c2ab8ea023dfd02483656e" + }, + { + "codeseparator": null, + "id": "0:101 (ffff3f3f #1)", + "input": 1, + "txfs": "ffff3f3f", + "txhash": "efc150fc72f4d9ade4c88c6bf507c85abcac3c09e6ffb6e57e2b372460a9b58b" + }, + { + "codeseparator": null, + "id": "0:102 (ffff4040 #0)", + "input": 0, + "txfs": "ffff4040", + "txhash": "bbc351d3a61ff0531c48883202c347ba6e2e889d0dae526cc302a8978f54194f" + }, + { + "codeseparator": null, + "id": "0:103 (ffff4040 #1)", + "input": 1, + "txfs": "ffff4040", + "txhash": "56789e76148add29a6e11a71f9eee1d587967f475a7b7adead3a38aad9753070" + }, + { + "codeseparator": null, + "id": "0:104 (ff008080 #0)", + "input": 0, + "txfs": "ff008080", + "txhash": "fc87d178387418de163b5cf92d7ec20236bc4e64d8013926e8202d7ffd35eaff" + }, + { + "codeseparator": null, + "id": "0:105 (ff008080 #1)", + "input": 1, + "txfs": "ff008080", + "txhash": "ef7074fa8f70e1c36b9c392afdca434320e9fd58a8e35a5b7722fb832f11cb15" + }, + { + "codeseparator": null, + "id": "0:106 (ffffbfbf #0)", + "input": 0, + "txfs": "ffffbfbf", + "txhash": "d872d9eda08df40dc5a6761f90d886720c24b392307df90c00d1647902967ca7" + }, + { + "codeseparator": null, + "id": "0:107 (ffffbfbf #1)", + "input": 1, + "txfs": "ffffbfbf", + "txhash": "b3d3393fd149ec4bb0ecf50119f36ba0621259b6d6288d9a2e8b8332bc75bcd7" + }, + { + "codeseparator": null, + "id": "0:108 (ffffc0c0 #0)", + "input": 0, + "txfs": "ffffc0c0", + "txhash": "addc2bf0570b09cf5dae3b8535158261e3e7c91db0fb313133343763cd3958dd" + }, + { + "codeseparator": null, + "id": "0:109 (ffffc0c0 #1)", + "input": 1, + "txfs": "ffffc0c0", + "txhash": "66e8cf6e41e1f2963d4dd1a78d3e19318b27f34abc5011ef6359ecf5151ab812" + }, + { + "codeseparator": null, + "id": "0:110 (ffff403f #0)", + "input": 0, + "txfs": "ffff403f", + "txhash": "9413abd29279ea6ad5a0675d1007ccd3d6d61ef6633eb9415961282f271d6c31" + }, + { + "codeseparator": null, + "id": "0:111 (ffff403f #1)", + "input": 1, + "txfs": "ffff403f", + "txhash": "8cd9e9133c585c31e207db07732fb5f6a3c392118c90b4d20e9e7dd470dc38ed" + }, + { + "codeseparator": null, + "id": "0:112 (ffff3f40 #0)", + "input": 0, + "txfs": "ffff3f40", + "txhash": "404f72fe8f7365a27bc5b1c496dd11a689a2f7d0f3b7b3c9755a8f7bf11d7b89" + }, + { + "codeseparator": null, + "id": "0:113 (ffff3f40 #1)", + "input": 1, + "txfs": "ffff3f40", + "txhash": "59628e1df03ce68564e9035f376ac9c76f7a11d5b1b0b71c5a593da5b4cf5c6d" + }, + { + "codeseparator": null, + "id": "0:114 (ffff0182 #0)", + "input": 0, + "txfs": "ffff0182", + "txhash": "ab8c241a251a98288b1758710961c0924e1c27b0df974cd669b2e154101e36b5" + }, + { + "codeseparator": null, + "id": "0:115 (ffff0182 #1)", + "input": 1, + "txfs": "ffff0182", + "txhash": "eb561e62a71274921a7e0025e66c51299d1904cfba98debb4a23cee2f49f9209" + }, + { + "codeseparator": null, + "id": "0:116 (ffffc002 #0)", + "input": 0, + "txfs": "ffffc002", + "txhash": "d5c3e2f80a574f8f80c016c2ddd4bee2298f4e91a49d07fbf2ff96ee1ebdbbc2" + }, + { + "codeseparator": null, + "id": "0:117 (ffffc002 #1)", + "input": 1, + "txfs": "ffffc002", + "txhash": "99efebafd498376dfced0515ae0d1949cb7673bff30bd73a693011edf1541db6" + }, + { + "codeseparator": null, + "id": "0:118 (ffff4101420002 #0)", + "input": 0, + "txfs": "ffff4101420002", + "txhash": "72f86b1e8e4721e3aa8c4ddfc744e93e2f4628fe63116e1c592449608077d0ea" + }, + { + "codeseparator": null, + "id": "0:119 (ffff4101420002 #1)", + "input": 1, + "txfs": "ffff4101420002", + "txhash": "6dfab74a32a90fd024f5faeab5935486de4c07b76ac788f3a80fb4e426e63dfc" + }, + { + "codeseparator": null, + "id": "0:120 (ffffc101c20002 #0)", + "input": 0, + "txfs": "ffffc101c20002", + "txhash": "8bcbd63009fea26f96e1736f2f2b517c7274feef626b3d9524e2dce22b81ec97" + }, + { + "codeseparator": null, + "id": "0:121 (ffffc101c20002 #1)", + "input": 1, + "txfs": "ffffc101c20002", + "txhash": "d801364c0d3d9468c17101f1906e89e84d0e325d904ce54a1305885296228055" + }, + { + "codeseparator": null, + "id": "0:122 (ffff617f627f00 #1)", + "input": 1, + "txfs": "ffff617f627f00", + "txhash": "a6eab06ad0e6053b0221d50463c6fa5c87dd4926bdff88dfbaae60147fa91beb" + }, + { + "codeseparator": null, + "id": "0:123 (ffffe17fe27f00 #1)", + "input": 1, + "txfs": "ffffe17fe27f00", + "txhash": "9f5dee0abdcbdfc6016e7186ad639b50728771eebbd7464e3ba0b5d8f3ef68ea" + }, + { + "codeseparator": null, + "id": "0:124 ( #0)", + "input": 0, + "txfs": "", + "txhash": "a1601773a4ddd7a2a1117fffaa670bd67fb3b85f91e2793e25de8c4f848fe0d9" + }, + { + "codeseparator": null, + "id": "0:125 ( #1)", + "input": 1, + "txfs": "", + "txhash": "6ae6fe854e08cbf0a25636b575d8bbe63c120ba496796dd30b4924b79f909fbd" + }, + { + "codeseparator": null, + "id": "0:126 (ff #0)", + "input": 0, + "txfs": "ff", + "txhash": "45da8ae2f210c2d6985d5c3e1067817838273495da515941e18fa554dc5e740d" + }, + { + "codeseparator": null, + "id": "0:127 (ff #1)", + "input": 1, + "txfs": "ff", + "txhash": "335bfdcf79f6ae2ccb7d4972e26106f043813de634ba9a752b4808192f5e8c38" + }, + { + "codeseparator": null, + "id": "0:128 (f7 #0)", + "input": 0, + "txfs": "f7", + "txhash": "bba33c04d64d580f27ef181c3482ed7243212176a721613f522415234b194df6" + }, + { + "codeseparator": null, + "id": "0:129 (f7 #1)", + "input": 1, + "txfs": "f7", + "txhash": "13b100680de28fd5d7ecd40389c49add2298ea464152c06b859150e7018ea9e6" + }, + { + "codeseparator": null, + "id": "0:130 (f3 #0)", + "input": 0, + "txfs": "f3", + "txhash": "27163ce496cf96f4dd5a0dc275ed3aeb4f13fac742011ae657c64536b7523896" + }, + { + "codeseparator": null, + "id": "0:131 (f3 #1)", + "input": 1, + "txfs": "f3", + "txhash": "4f45f4c2a2390634e6591cffba66bec810fa17c88734fedc6ce974f765533771" + }, + { + "codeseparator": null, + "id": "0:132 (fd #0)", + "input": 0, + "txfs": "fd", + "txhash": "66b2523a0e8ccc2388b401043dcd08bcd9848ce07ba90309a3194a88cceefc58" + }, + { + "codeseparator": null, + "id": "0:133 (fd #1)", + "input": 1, + "txfs": "fd", + "txhash": "bf51c6a7993a1abc7f62d92ceb1abaf03ce10df620cab77a9c68bc8b2d993768" + }, + { + "codeseparator": null, + "id": "0:134 (f5 #0)", + "input": 0, + "txfs": "f5", + "txhash": "4ac1bd781328f1a24dc514d686b1c0ea78ca28bf4b37e60472f98e9b1c07568c" + }, + { + "codeseparator": null, + "id": "0:135 (f5 #1)", + "input": 1, + "txfs": "f5", + "txhash": "be93ef8b84789a7e522f73bb3c0c00afe91075e3a02dc82fb766f31a5e2810db" + }, + { + "codeseparator": null, + "id": "0:136 (f1 #0)", + "input": 0, + "txfs": "f1", + "txhash": "0d09ed34a6bf5fbb8d447549c88a9aba198498f8351ddc32c89450cb8e2be34a" + }, + { + "codeseparator": null, + "id": "0:137 (f1 #1)", + "input": 1, + "txfs": "f1", + "txhash": "38d07d3216b3a2096ac879292cc4d2eca1eee87c3a8ed0d4e53b11adda28d7b6" + }, + { + "codeseparator": null, + "id": "0:138 (ed #0)", + "input": 0, + "txfs": "ed", + "txhash": "4978bd282206c9ad2f781e0ca252e9a37e4a366ca619c09c0a1d37028c8f7f28" + }, + { + "codeseparator": null, + "id": "0:139 (ed #1)", + "input": 1, + "txfs": "ed", + "txhash": "009af4edad039ac16da71104b4fdd715a8068a56bd5b27e08a7a5c1eedbfb4e4" + }, + { + "codeseparator": null, + "id": "0:140 (e5 #0)", + "input": 0, + "txfs": "e5", + "txhash": "83dc16b7f5dc131be5b21057a9a411e24a773579e5777953598ccfeee3bebcaa" + }, + { + "codeseparator": null, + "id": "0:141 (e5 #1)", + "input": 1, + "txfs": "e5", + "txhash": "a5a38488a0ac5600cb790ebc61200e92e4bd2421f0e9cb412e53634efdb4f014" + }, + { + "codeseparator": null, + "id": "0:142 (e1 #0)", + "input": 0, + "txfs": "e1", + "txhash": "bcc69369370c5083a378d0c9d6501f30bf61bc9c2e53ca0703d4da81b629ea37" + }, + { + "codeseparator": null, + "id": "0:143 (e1 #1)", + "input": 1, + "txfs": "e1", + "txhash": "8746f7789df6c561577f84965b6db3a12eb8f66f34af3006713fef6db9620455" + }, + { + "codeseparator": null, + "id": "0:144 (cd #0)", + "input": 0, + "txfs": "cd", + "txhash": "4b75f5635bbceb991d5c3f9c0c2a3d367a9b6b12cb150b0db632e2a8d81e337c" + }, + { + "codeseparator": null, + "id": "0:145 (cd #1)", + "input": 1, + "txfs": "cd", + "txhash": "e48d0a29d86078a17a9283f6f1c314499eab7343855f4b9f7140c68d4b346b9f" + }, + { + "codeseparator": null, + "id": "0:146 (c5 #0)", + "input": 0, + "txfs": "c5", + "txhash": "6f774daf5b1b163f35b4ca367df2798aa8e66c2ff30b77ea9f2e98a5db2b0ec6" + }, + { + "codeseparator": null, + "id": "0:147 (c5 #1)", + "input": 1, + "txfs": "c5", + "txhash": "b21e18937bbfadfb37ef10ebf9dacff13801503e475f2524e9c3bb620c0c1567" + }, + { + "codeseparator": null, + "id": "0:148 (c1 #0)", + "input": 0, + "txfs": "c1", + "txhash": "4f3d9c36d74b027ce9487907e36a59076f6a6760c7c5902b4dccac1199379a05" + }, + { + "codeseparator": null, + "id": "0:149 (c1 #1)", + "input": 1, + "txfs": "c1", + "txhash": "f6abfe1167fecd2809b28fb8d3b0fc98829c49a8f80f47492a0a910ff5799aa5" + } + ] + } +] \ No newline at end of file diff --git a/bip-0347.mediawiki b/bip-0347.mediawiki index 12a1a08e1f..319f75b8d6 100644 --- a/bip-0347.mediawiki +++ b/bip-0347.mediawiki @@ -4,11 +4,13 @@ Title: OP_CAT in Tapscript Authors: Ethan Heilman Armin Sabouri - Status: Draft + Status: Complete Type: Specification Assigned: 2023-12-11 License: BSD-3-Clause Discussion: 2023-10-21: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-October/022049.html [bitcoin-dev] Proposed BIP for OP_CAT + Version: 1.0.0 + Requires: 340, 341, 342 ==Abstract== @@ -42,6 +44,8 @@ OP_CAT aims to expand the toolbox of the tapscript developer with a simple, modu * Non-equivocation contracts T. Ruffing, A. Kate, D. Schröder, "Liar, Liar, Coins on Fire: Penalizing Equivocation by Loss of Bitcoins", 2015, https://web.archive.org/web/20221023121048/https://publications.cispa.saarland/565/1/penalizing.pdf in tapscript provide a mechanism to punish equivocation/double spending in Bitcoin payment channels. OP_CAT enables this by enforcing rules on the spending transaction's nonce. The capability is a useful building block for payment channels and other Bitcoin protocols. * Vaults M. Moser, I. Eyal, and E. G. Sirer, Bitcoin Covenants, https://web.archive.org/web/20220203124718/https://fc16.ifca.ai/bitcoin/papers/MES16.pdf which are a specialized covenant that allows a user to block a malicious party who has compromised the user's secret key from stealing the funds in that output. As shown in A. Poelstra, "CAT and Schnorr Tricks II", 2021, https://www.wpsoftware.net/andrew/blog/cat-and-schnorr-tricks-ii.html OP_CAT is sufficient to build vaults in Bitcoin. * Replicating CheckSigFromStack A. Poelstra, "CAT and Schnorr Tricks I", 2021, https://www.wpsoftware.net/andrew/blog/cat-and-schnorr-tricks-i.html which would allow the creation of simple covenants and other advanced contracts without having to presign spending transactions, possibly reducing complexity and the amount of data that needs to be stored. Originally shown to work with Schnorr signatures, this result has been extended to ECDSA signatures R. Linus, "Covenants with CAT and ECDSA", 2023, https://gist.github.com/RobinLinus/9a69f5552be94d13170ec79bf34d5e85#file-covenants_cat_ecdsa-md. +* OP_CAT would allow tapscript to perform arbitrary computation on stack elements larger than 32-bits such as signatures and public keys. While Collider Script E. Heilman, V. I. Kolobov, A. M. Levy, A. Poelstra, "ColliderScript: Covenants in Bitcoin via 160-bit hash collisions", 2024, https://eprint.iacr.org/2024/1802 showed that even without OP_CAT tapscript can perform arbitrary computation on stack elements larger than 32-bits, this approach is extremely computationally expensive. In practice, today's tapscript can only perform arbitrary computation on 32-bit stack elements. OP_CAT was not designed with this usecase in mind and OP_CAT an inefficient way to perform this task. +* BitVMR. Linus, L. Aumayr, A. Zamyatin, A. Pelosi, Z. Avarikioti, M. Maffei "BitVM2: Bridging Bitcoin to Second Layers", 2025, https://bitvm.org/bitvm_bridge.pdf improvements. OP_CAT would enable BitVM2 to eliminating the trusted setup from its proof system allow BitVM2 to reduce the size of transactions it uses. OP_CAT was available in early versions of Bitcoin. In 2010, a single commit disabled OP_CAT, along with another 15 opcodes. @@ -102,10 +106,75 @@ break; An alternative implementation of OP_CAT can be found in Elements Roose S., Elements Project, "Re-enable several disabled opcodes", 2019, https://github.com/ElementsProject/elements/commit/13e1103abe3e328c5a4e2039b51a546f8be6c60a#diff-a0337ffd7259e8c7c9a7786d6dbd420c80abfa1afdb34ebae3261109d9ae3c19R740-R759. + +==Test Vectors== + +The following test vectors use Bitcoin-core JSON script-test format. + +
+[
+    [
+        "78a11a1260c1101260",
+        "78a11a1260",
+        "c1101260",
+        "#SCRIPT# CAT EQUAL",
+        "#CONTROLBLOCK#",
+        0.00000001
+    ],
+    "",
+    "0x51 0x20 #TAPROOTOUTPUT#",
+    "P2SH,WITNESS,TAPROOT,OP_CAT",
+    "OK",
+    "TAPSCRIPT CATs 78a11a1260 and c1101260 together and checks it is EQUAL to stack element 78a11a1260c1101260"
+],
+[
+    [
+        "51",
+        "bbbb",
+        "01",
+        "#SCRIPT# IF CAT ELSE DROP ENDIF",
+        "#CONTROLBLOCK#",
+        0.00000001
+    ],
+    "",
+    "0x51 0x20 #TAPROOTOUTPUT#",
+    "P2SH,WITNESS,TAPROOT,OP_CAT",
+    "OK",
+    "TAPSCRIPT Tests CAT inside of an IF ELSE conditional (true IF)"
+],
+[
+    [
+        "",
+        "09ca7009ca7009ca7009ca7009ca70",
+        "#SCRIPT# CAT",
+        "#CONTROLBLOCK#",
+        0.00000001
+    ],
+    "",
+    "0x51 0x20 #TAPROOTOUTPUT#",
+    "P2SH,WITNESS,TAPROOT,OP_CAT",
+    "OK",
+    "TAPSCRIPT (['', 09ca7009ca7009ca7009ca7009ca70], CAT) Tests CAT succeeds when one of the two values to concatenate is of size zero"
+],
+
+ +A full test suite with additional vectors can be found at the Bitcoin [https://github.com/bitcoin/bitcoin/pull/29247 OP_CAT PR]. + ==References== +==Changelog== + +* __1.0.0__ (2026-03-01) - Marked as complete. +* __0.3.1__ (2026-01-23) - Made compliant with BIP 003, use cases added. +* __0.3.0__ (2024-05-06) - Merged to BIP repo +* __0.2.0__ (2024-04-24) - Assigned BIP number +* __0.1.0__ (2023-12-11) - Initial draft posted + + +May 6, 2024 + ==Acknowledgements== We wish to acknowledge Dan Gould for encouraging and helping review this effort. We also want to thank Madars Virza, Jeremy Rubin, Andrew Poelstra, Bob Summerwill, diff --git a/bip-0352.mediawiki b/bip-0352.mediawiki index 36944adc9e..21bca34fc5 100644 --- a/bip-0352.mediawiki +++ b/bip-0352.mediawiki @@ -4,6 +4,7 @@ Title: Silent Payments Authors: josibake Ruben Somsen + Sebastian Falbesoner Status: Complete Type: Specification Assigned: 2023-03-09 @@ -300,6 +301,8 @@ After the inputs have been selected, the sender can create one or more outputs f * Let ''input_hash = hashBIP0352/Inputs(outpointL || A)'', where ''outpointL'' is the smallest ''outpoint'' lexicographically used in the transaction and ''A = a·G'' ** If ''input_hash'' is not a valid scalar, i.e., if ''input_hash = 0'' or ''input_hash'' is larger or equal to the secp256k1 group order, fail * Group receiver silent payment addresses by ''Bscan'' (e.g. each group consists of one ''Bscan'' and one or more ''Bm'') +* If any of the groups exceed the limit of ''Kmax'' (=2323) silent payment addresses, fail.'''Why is the size of groups (i.e. silent payment addresses sharing the same scan public key) limited by ''Kmax''?''' An adversary could construct a block filled with a single transaction consisting of N=23255 outputs (that's the theoretical maximum under current consensus rules, w.r.t. the block weight limit) that all target the same entity, consisting of one large group. Without a limit on the group size, scanning such a block with the algorithm described in this document would have a complexity of ''O(N2)'' for that entity, taking several minutes on modern systems. By capping the group size at ''Kmax'', we reduce the inner loop iterations to ''Kmax'', thereby decreasing the worst-case block scanning complexity to ''O(N·Kmax)''. This cuts down the scanning cost to the order of tens of seconds. The chosen value of ''Kmax'' = 2323 represents the maximum number of P2TR outputs that can fit into a 100kvB transaction, meaning a transaction that adheres to the current standardness rules is guaranteed to be within the limit. This ensures flexibility and also mitigates potential fingerprinting issues. + * For each group: ** Let ''ecdh_shared_secret = input_hash·a·Bscan'' ** Let ''k = 0'' @@ -339,6 +342,7 @@ If each of the checks in ''[[#scanning-silent-payment-eligible-transactions|Scan * Check for outputs: ** Let ''outputs_to_check'' be the taproot output keys from all taproot outputs in the transaction (spent and unspent). ** Starting with ''k = 0'': +*** If ''k == Kmax'' (=2323), stop scanning. *** Let ''tk = hashBIP0352/SharedSecret(serP(ecdh_shared_secret) || ser32(k))'' **** If ''tk'' is not a valid scalar, i.e., if ''tk = 0'' or ''tk'' is larger or equal to the secp256k1 group order, fail *** Compute ''Pk = Bspend + tk·G'' @@ -374,7 +378,10 @@ Silent payments introduces a new address format and protocol for sending and as == Test Vectors == -A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JSON format]] are provided, along with a [[bip-0352/reference.py|python reference implementation]]. Each test vector consists of a sending test case and corresponding receiving test case. This is to allow sending and receiving to be implemented separately. To ensure determinism while testing, sort the array of ''Bm'' by amount (see the [[bip-0352/reference.py|reference implementation]]). Test cases use the following schema: +A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JSON format]] is provided, along with a [[bip-0352/reference.py|python reference implementation]]. It uses a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab/ secp256k1lab] library at version 1.0.0 +(commit [https://github.com/secp256k1lab/secp256k1lab/commit/44dc4bd893b8f03e621585e3bf255253e0e0fbfb 44dc4bd893b8f03e621585e3bf255253e0e0fbfb]). + +Each test vector consists of a sending test case and corresponding receiving test case. This is to allow sending and receiving to be implemented separately. To ensure determinism while testing, sort the array of ''Bm'' by amount (see the [[bip-0352/reference.py|reference implementation]]). Test cases use the following schema: ''' test_case ''' @@ -389,7 +396,15 @@ A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JS { "given": { "vin": [], - "recipients": [] + "recipients": [ + { + "address": , + "scan_pub_key": , + "spend_pub_key": , + "count": + }, + ... + ] }, "expected": { "outputs": [], @@ -410,7 +425,7 @@ A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JS }, "expected": { "addresses": [], - "outputs": [ + "outputs": [ { "priv_key_tweak": , "pub_key": , @@ -418,7 +433,7 @@ A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JS }, ... ], - "n_outputs": + "n_outputs": } } @@ -488,6 +503,8 @@ The MAJOR version is incremented if changes to the BIP are introduc The MINOR version is incremented whenever the inputs or the output of an algorithm changes in a backward-compatible way or new backward-compatible functionality is added. The PATCH version is incremented for other changes that are noteworthy (bug fixes, test vectors, important clarifications, etc.). +* '''1.1.0''' (2026-03-02): +** Introduce per-group recipient limit ''Kmax'' to mitigate quadratic scanning behavior for adversarial transactions. * '''1.0.2''' (2025-07-25): ** Clarify how to handle the improbable corner case where the output of SHA256 is equal to 0 or greater than or equal to the secp256k1 curve order. * '''1.0.1''' (2024-06-22): diff --git a/bip-0352/bitcoin_utils.py b/bip-0352/bitcoin_utils.py index 9e279e455b..7e9bd9b265 100644 --- a/bip-0352/bitcoin_utils.py +++ b/bip-0352/bitcoin_utils.py @@ -2,7 +2,7 @@ import struct from io import BytesIO from ripemd160 import ripemd160 -from secp256k1 import ECKey +from secp256k1lab.secp256k1 import Scalar from typing import Union @@ -93,7 +93,7 @@ def __init__(self, outpoint=None, scriptSig=b"", txinwitness=None, prevout=b"", else: self.txinwitness = txinwitness if private_key is None: - self.private_key = ECKey() + self.private_key = Scalar() else: self.private_key = private_key self.scriptSig = scriptSig diff --git a/bip-0352/reference.py b/bip-0352/reference.py index c236c0dbd9..714050e466 100755 --- a/bip-0352/reference.py +++ b/bip-0352/reference.py @@ -2,15 +2,19 @@ # For running the test vectors, run this script: # ./reference.py send_and_receive_test_vectors.json -import hashlib import json +from pathlib import Path +import sys from typing import List, Tuple, Dict, cast -from sys import argv, exit -from functools import reduce -# local files +# import the vendored copy of secp256k1lab +sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) +from secp256k1lab.bip340 import schnorr_sign, schnorr_verify +from secp256k1lab.secp256k1 import G, GE, Scalar +from secp256k1lab.util import tagged_hash, hash_sha256 + + from bech32m import convertbits, bech32_encode, decode, Encoding -from secp256k1 import ECKey, ECPubKey, TaggedHash, NUMS_H from bitcoin_utils import ( deser_txid, from_hex, @@ -26,7 +30,11 @@ ) -def get_pubkey_from_input(vin: VinInfo) -> ECPubKey: +K_max = 2323 # per-group recipient limit +NUMS_H = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 + + +def get_pubkey_from_input(vin: VinInfo) -> GE: if is_p2pkh(vin.prevout): # skip the first 3 op_codes and grab the 20 byte hash # from the scriptPubKey @@ -44,20 +52,22 @@ def get_pubkey_from_input(vin: VinInfo) -> ECPubKey: pubkey_bytes = vin.scriptSig[i - 33:i] pubkey_hash = hash160(pubkey_bytes) if pubkey_hash == spk_hash: - pubkey = ECPubKey().set(pubkey_bytes) - if (pubkey.valid) & (pubkey.compressed): - return pubkey + try: + return GE.from_bytes_compressed(pubkey_bytes) + except ValueError: + pass if is_p2sh(vin.prevout): redeem_script = vin.scriptSig[1:] if is_p2wpkh(redeem_script): - pubkey = ECPubKey().set(vin.txinwitness.scriptWitness.stack[-1]) - if (pubkey.valid) & (pubkey.compressed): - return pubkey + try: + return GE.from_bytes_compressed(vin.txinwitness.scriptWitness.stack[-1]) + except (ValueError, AssertionError): + pass if is_p2wpkh(vin.prevout): - txin = vin.txinwitness - pubkey = ECPubKey().set(txin.scriptWitness.stack[-1]) - if (pubkey.valid) & (pubkey.compressed): - return pubkey + try: + return GE.from_bytes_compressed(vin.txinwitness.scriptWitness.stack[-1]) + except (ValueError, AssertionError): + pass if is_p2tr(vin.prevout): witnessStack = vin.txinwitness.scriptWitness.stack if (len(witnessStack) >= 1): @@ -72,71 +82,69 @@ def get_pubkey_from_input(vin: VinInfo) -> ECPubKey: internal_key = control_block[1:33] if (internal_key == NUMS_H.to_bytes(32, 'big')): # Skip if NUMS_H - return ECPubKey() - - pubkey = ECPubKey().set(vin.prevout[2:]) - if (pubkey.valid) & (pubkey.compressed): - return pubkey + return GE() + try: + return GE.from_bytes_xonly(vin.prevout[2:]) + except ValueError: + pass - return ECPubKey() + return GE() -def get_input_hash(outpoints: List[COutPoint], sum_input_pubkeys: ECPubKey) -> bytes: +def get_input_hash(outpoints: List[COutPoint], sum_input_pubkeys: GE) -> bytes: lowest_outpoint = sorted(outpoints, key=lambda outpoint: outpoint.serialize())[0] - return TaggedHash("BIP0352/Inputs", lowest_outpoint.serialize() + cast(bytes, sum_input_pubkeys.get_bytes(False))) + return tagged_hash("BIP0352/Inputs", lowest_outpoint.serialize() + sum_input_pubkeys.to_bytes_compressed()) -def encode_silent_payment_address(B_scan: ECPubKey, B_m: ECPubKey, hrp: str = "tsp", version: int = 0) -> str: - data = convertbits(cast(bytes, B_scan.get_bytes(False)) + cast(bytes, B_m.get_bytes(False)), 8, 5) +def encode_silent_payment_address(B_scan: GE, B_m: GE, hrp: str = "tsp", version: int = 0) -> str: + data = convertbits(B_scan.to_bytes_compressed() + B_m.to_bytes_compressed(), 8, 5) return bech32_encode(hrp, [version] + cast(List[int], data), Encoding.BECH32M) -def generate_label(b_scan: ECKey, m: int) -> bytes: - return TaggedHash("BIP0352/Label", b_scan.get_bytes() + ser_uint32(m)) +def generate_label(b_scan: Scalar, m: int) -> Scalar: + return Scalar.from_bytes_checked(tagged_hash("BIP0352/Label", b_scan.to_bytes() + ser_uint32(m))) -def create_labeled_silent_payment_address(b_scan: ECKey, B_spend: ECPubKey, m: int, hrp: str = "tsp", version: int = 0) -> str: - G = ECKey().set(1).get_pubkey() - B_scan = b_scan.get_pubkey() +def create_labeled_silent_payment_address(b_scan: Scalar, B_spend: GE, m: int, hrp: str = "tsp", version: int = 0) -> str: + B_scan = b_scan * G B_m = B_spend + generate_label(b_scan, m) * G labeled_address = encode_silent_payment_address(B_scan, B_m, hrp, version) return labeled_address -def decode_silent_payment_address(address: str, hrp: str = "tsp") -> Tuple[ECPubKey, ECPubKey]: +def decode_silent_payment_address(address: str, hrp: str = "tsp") -> Tuple[GE, GE]: _, data = decode(hrp, address) if data is None: - return ECPubKey(), ECPubKey() - B_scan = ECPubKey().set(data[:33]) - B_spend = ECPubKey().set(data[33:]) + return GE(), GE() + B_scan = GE.from_bytes_compressed(data[:33]) + B_spend = GE.from_bytes_compressed(data[33:]) return B_scan, B_spend -def create_outputs(input_priv_keys: List[Tuple[ECKey, bool]], outpoints: List[COutPoint], recipients: List[str], expected: Dict[str, any] = None, hrp="tsp") -> List[str]: - G = ECKey().set(1).get_pubkey() +def create_outputs(input_priv_keys: List[Tuple[Scalar, bool]], outpoints: List[COutPoint], recipients: List[str], expected: Dict[str, any] = None, hrp="tsp") -> List[str]: negated_keys = [] for key, is_xonly in input_priv_keys: - k = ECKey().set(key.get_bytes()) - if is_xonly and k.get_pubkey().get_y() % 2 != 0: - k.negate() + k = Scalar.from_bytes_checked(key.to_bytes()) + if is_xonly and not (k * G).has_even_y(): + k = -k negated_keys.append(k) - a_sum = sum(negated_keys) - if not a_sum.valid: + a_sum = Scalar.sum(*negated_keys) + if a_sum == 0: # Input privkeys sum is zero -> fail return [] - assert ECKey().set(bytes.fromhex(expected.get("input_private_key_sum"))) == a_sum, "a_sum did not match expected input_private_key_sum" - input_hash = get_input_hash(outpoints, a_sum * G) - silent_payment_groups: Dict[ECPubKey, List[ECPubKey]] = {} + assert Scalar.from_bytes_checked(bytes.fromhex(expected.get("input_private_key_sum"))) == a_sum, "a_sum did not match expected input_private_key_sum" + input_hash_scalar = Scalar.from_bytes_checked(get_input_hash(outpoints, a_sum * G)) + silent_payment_groups: Dict[GE, List[GE]] = {} for recipient in recipients: B_scan, B_m = decode_silent_payment_address(recipient["address"], hrp=hrp) # Verify decoded intermediate keys for recipient - expected_B_scan = ECPubKey().set(bytes.fromhex(recipient["scan_pub_key"])) - expected_B_m = ECPubKey().set(bytes.fromhex(recipient["spend_pub_key"])) + expected_B_scan = GE.from_bytes_compressed(bytes.fromhex(recipient["scan_pub_key"])) + expected_B_m = GE.from_bytes_compressed(bytes.fromhex(recipient["spend_pub_key"])) assert expected_B_scan == B_scan, "B_scan did not match expected recipient.scan_pub_key" assert expected_B_m == B_m, "B_m did not match expected recipient.spend_pub_key" if B_scan in silent_payment_groups: @@ -144,68 +152,73 @@ def create_outputs(input_priv_keys: List[Tuple[ECKey, bool]], outpoints: List[CO else: silent_payment_groups[B_scan] = [B_m] + # Fail if per-group recipient limit (K_max) is exceeded + if any([len(group) > K_max for group in silent_payment_groups.values()]): + return [] + outputs = [] for B_scan, B_m_values in silent_payment_groups.items(): - ecdh_shared_secret = input_hash * a_sum * B_scan + ecdh_shared_secret = input_hash_scalar * a_sum * B_scan expected_shared_secrets = expected.get("shared_secrets", {}) # Find the recipient address that corresponds to this B_scan and get its index for recipient_idx, recipient in enumerate(recipients): - recipient_B_scan = ECPubKey().set(bytes.fromhex(recipient["scan_pub_key"])) + recipient_B_scan = GE.from_bytes_compressed(bytes.fromhex(recipient["scan_pub_key"])) if recipient_B_scan == B_scan: expected_shared_secret_hex = expected_shared_secrets[recipient_idx] - assert ecdh_shared_secret.get_bytes(False).hex() == expected_shared_secret_hex, f"ecdh_shared_secret did not match expected, recipient {recipient_idx} ({recipient['address']}): expected={expected_shared_secret_hex}" + assert ecdh_shared_secret.to_bytes_compressed().hex() == expected_shared_secret_hex, f"ecdh_shared_secret did not match expected, recipient {recipient_idx} ({recipient['address']}): expected={expected_shared_secret_hex}" break k = 0 for B_m in B_m_values: - t_k = TaggedHash("BIP0352/SharedSecret", ecdh_shared_secret.get_bytes(False) + ser_uint32(k)) + t_k = Scalar.from_bytes_checked(tagged_hash("BIP0352/SharedSecret", ecdh_shared_secret.to_bytes_compressed() + ser_uint32(k))) P_km = B_m + t_k * G - outputs.append(P_km.get_bytes().hex()) + outputs.append(P_km.to_bytes_xonly().hex()) k += 1 return list(set(outputs)) -def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: bytes, outputs_to_check: List[ECPubKey], labels: Dict[str, str] = None, expected: Dict[str, any] = None) -> List[Dict[str, str]]: - G = ECKey().set(1).get_pubkey() - input_hash_key = ECKey().set(input_hash) - computed_tweak_point = input_hash_key * A_sum - assert computed_tweak_point.get_bytes(False).hex() == expected.get("tweak"), "tweak did not match expected" - ecdh_shared_secret = input_hash * b_scan * A_sum - assert ecdh_shared_secret.get_bytes(False).hex() == expected.get("shared_secret"), "ecdh_shared_secret did not match expected shared_secret" +def scanning(b_scan: Scalar, B_spend: GE, A_sum: GE, input_hash: bytes, outputs_to_check: List[bytes], labels: Dict[str, str] = None, expected: Dict[str, any] = None) -> List[Dict[str, str]]: + input_hash_scalar = Scalar.from_bytes_checked(input_hash) + computed_tweak_point = input_hash_scalar * A_sum + assert computed_tweak_point.to_bytes_compressed().hex() == expected.get("tweak"), "tweak did not match expected" + ecdh_shared_secret = input_hash_scalar * b_scan * A_sum + assert ecdh_shared_secret.to_bytes_compressed().hex() == expected.get("shared_secret"), "ecdh_shared_secret did not match expected shared_secret" k = 0 wallet = [] while True: - t_k = TaggedHash("BIP0352/SharedSecret", ecdh_shared_secret.get_bytes(False) + ser_uint32(k)) + if k == K_max: # Don't look further than the per-group recipient limit (K_max) + break + t_k = Scalar.from_bytes_checked(tagged_hash("BIP0352/SharedSecret", ecdh_shared_secret.to_bytes_compressed() + ser_uint32(k))) P_k = B_spend + t_k * G for output in outputs_to_check: - if P_k == output: - wallet.append({"pub_key": P_k.get_bytes().hex(), "priv_key_tweak": t_k.hex()}) + output_ge = GE.from_bytes_xonly(output) + if P_k.to_bytes_xonly() == output: + wallet.append({"pub_key": P_k.to_bytes_xonly().hex(), "priv_key_tweak": t_k.to_bytes().hex()}) outputs_to_check.remove(output) k += 1 break elif labels: - m_G_sub = output - P_k - if m_G_sub.get_bytes(False).hex() in labels: + m_G_sub = output_ge - P_k + if m_G_sub.to_bytes_compressed().hex() in labels: P_km = P_k + m_G_sub wallet.append({ - "pub_key": P_km.get_bytes().hex(), - "priv_key_tweak": (ECKey().set(t_k).add( - bytes.fromhex(labels[m_G_sub.get_bytes(False).hex()]) - )).get_bytes().hex(), + "pub_key": P_km.to_bytes_xonly().hex(), + "priv_key_tweak": (t_k + Scalar.from_bytes_checked( + bytes.fromhex(labels[m_G_sub.to_bytes_compressed().hex()]) + )).to_bytes().hex(), }) outputs_to_check.remove(output) k += 1 break else: - output.negate() - m_G_sub = output - P_k - if m_G_sub.get_bytes(False).hex() in labels: + m_G_sub = -output_ge - P_k + if m_G_sub.to_bytes_compressed().hex() in labels: P_km = P_k + m_G_sub wallet.append({ - "pub_key": P_km.get_bytes().hex(), - "priv_key_tweak": (ECKey().set(t_k).add( - bytes.fromhex(labels[m_G_sub.get_bytes(False).hex()]) - )).get_bytes().hex(), + "pub_key": P_km.to_bytes_xonly().hex(), + "priv_key_tweak": (t_k + Scalar.from_bytes_checked( + bytes.fromhex(labels[m_G_sub.to_bytes_compressed().hex()]) + )).to_bytes().hex(), }) outputs_to_check.remove(output) k += 1 @@ -216,15 +229,13 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte if __name__ == "__main__": - if len(argv) != 2 or argv[1] in ('-h', '--help'): + if len(sys.argv) != 2 or sys.argv[1] in ('-h', '--help'): print("Usage: ./reference.py send_and_receive_test_vectors.json") - exit(0) + sys.exit(0) - with open(argv[1], "r") as f: + with open(sys.argv[1], "r") as f: test_data = json.loads(f.read()) - # G , needed for generating the labels "database" - G = ECKey().set(1).get_pubkey() for case in test_data: print(case["comment"]) # Test sending @@ -238,7 +249,7 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte scriptSig=bytes.fromhex(input["scriptSig"]), txinwitness=CTxInWitness().deserialize(from_hex(input["txinwitness"])), prevout=bytes.fromhex(input["prevout"]["scriptPubKey"]["hex"]), - private_key=ECKey().set(bytes.fromhex(input["private_key"])), + private_key=Scalar.from_bytes_checked(bytes.fromhex(input["private_key"])), ) for input in given["vin"] ] @@ -247,19 +258,23 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte input_pub_keys = [] for vin in vins: pubkey = get_pubkey_from_input(vin) - if not pubkey.valid: + if pubkey.infinity: continue input_priv_keys.append(( vin.private_key, is_p2tr(vin.prevout), )) input_pub_keys.append(pubkey) - assert [pk.get_bytes(False).hex() for pk in input_pub_keys] == expected.get("input_pub_keys"), "input_pub_keys did not match expected" + assert [pk.to_bytes_compressed().hex() for pk in input_pub_keys] == expected.get("input_pub_keys"), "input_pub_keys did not match expected" sending_outputs = [] if (len(input_pub_keys) > 0): outpoints = [vin.outpoint for vin in vins] - sending_outputs = create_outputs(input_priv_keys, outpoints, given["recipients"], expected=expected, hrp="sp") + recipients = [] # expand given recipient entries to full list + for recipient_entry in given["recipients"]: + count = recipient_entry.get("count", 1) + recipients.extend([recipient_entry] * count) + sending_outputs = create_outputs(input_priv_keys, outpoints, recipients, expected=expected, hrp="sp") # Note: order doesn't matter for creating/finding the outputs. However, different orderings of the recipient addresses # will produce different generated outputs if sending to multiple silent payment addresses belonging to the @@ -270,13 +285,13 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte assert(sending_outputs == expected["outputs"][0] == []), "Sending test failed" # Test receiving - msg = hashlib.sha256(b"message").digest() - aux = hashlib.sha256(b"random auxiliary data").digest() + msg = hash_sha256(b"message") + aux = hash_sha256(b"random auxiliary data") for receiving_test in case["receiving"]: given = receiving_test["given"] expected = receiving_test["expected"] outputs_to_check = [ - ECPubKey().set(bytes.fromhex(p)) for p in given["outputs"] + bytes.fromhex(p) for p in given["outputs"] ] vins = [ VinInfo( @@ -289,12 +304,10 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte ] # Check that the given inputs for the receiving test match what was generated during the sending test receiving_addresses = [] - b_scan = ECKey().set(bytes.fromhex(given["key_material"]["scan_priv_key"])) - b_spend = ECKey().set( - bytes.fromhex(given["key_material"]["spend_priv_key"]) - ) - B_scan = b_scan.get_pubkey() - B_spend = b_spend.get_pubkey() + b_scan = Scalar.from_bytes_checked(bytes.fromhex(given["key_material"]["scan_priv_key"])) + b_spend = Scalar.from_bytes_checked(bytes.fromhex(given["key_material"]["spend_priv_key"])) + B_scan = b_scan * G + B_spend = b_spend * G receiving_addresses.append( encode_silent_payment_address(B_scan, B_spend, hrp="sp") ) @@ -311,21 +324,21 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte input_pub_keys = [] for vin in vins: pubkey = get_pubkey_from_input(vin) - if not pubkey.valid: + if pubkey.infinity: continue input_pub_keys.append(pubkey) add_to_wallet = [] if (len(input_pub_keys) > 0): - A_sum = reduce(lambda x, y: x + y, input_pub_keys) - if A_sum.get_bytes() is None: + A_sum = GE.sum(*input_pub_keys) + if A_sum.infinity: # Input pubkeys sum is point at infinity -> skip tx assert expected["outputs"] == [] continue - assert A_sum.get_bytes(False).hex() == expected.get("input_pub_key_sum"), "A_sum did not match expected input_pub_key_sum" + assert A_sum.to_bytes_compressed().hex() == expected.get("input_pub_key_sum"), "A_sum did not match expected input_pub_key_sum" input_hash = get_input_hash([vin.outpoint for vin in vins], A_sum) pre_computed_labels = { - (generate_label(b_scan, label) * G).get_bytes(False).hex(): generate_label(b_scan, label).hex() + (generate_label(b_scan, label) * G).to_bytes_compressed().hex(): generate_label(b_scan, label).to_bytes().hex() for label in given["labels"] } add_to_wallet = scanning( @@ -340,13 +353,13 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte # Check that the private key is correct for the found output public key for output in add_to_wallet: - pub_key = ECPubKey().set(bytes.fromhex(output["pub_key"])) - full_private_key = b_spend.add(bytes.fromhex(output["priv_key_tweak"])) - if full_private_key.get_pubkey().get_y() % 2 != 0: - full_private_key.negate() + pub_key = GE.from_bytes_xonly(bytes.fromhex(output["pub_key"])) + full_private_key = b_spend + Scalar.from_bytes_checked(bytes.fromhex(output["priv_key_tweak"])) + if not (full_private_key * G).has_even_y(): + full_private_key = -full_private_key - sig = full_private_key.sign_schnorr(msg, aux) - assert pub_key.verify_schnorr(sig, msg), f"Invalid signature for {pub_key}" + sig = schnorr_sign(msg, full_private_key.to_bytes(), aux) + assert schnorr_verify(msg, pub_key.to_bytes_xonly(), sig), f"Invalid signature for {pub_key}" output["signature"] = sig.hex() # Note: order doesn't matter for creating/finding the outputs. However, different orderings of the recipient addresses @@ -354,9 +367,14 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte # same sender but with different labels. Because of this, expected["outputs"] contains all possible valid output sets, # based on all possible permutations of recipient address orderings. Must match exactly one of the possible found output # sets in expected["outputs"] - generated_set = {frozenset(d.items()) for d in add_to_wallet} - expected_set = {frozenset(d.items()) for d in expected["outputs"]} - assert generated_set == expected_set, "Receive test failed" + if "outputs" in expected: # detailed check against expected outputs + generated_set = {frozenset(d.items()) for d in add_to_wallet} + expected_set = {frozenset(d.items()) for d in expected["outputs"]} + assert generated_set == expected_set, "Receive test failed" + elif "n_outputs" in expected: # only check the number of found outputs + assert len(add_to_wallet) == expected["n_outputs"], "Receive test failed" + else: + assert False, "either 'outputs' or 'n_outputs' must be specified in 'expected' field of receiving test vector" print("All tests passed") diff --git a/bip-0352/secp256k1.py b/bip-0352/secp256k1.py deleted file mode 100644 index 0ccbc4e6a4..0000000000 --- a/bip-0352/secp256k1.py +++ /dev/null @@ -1,696 +0,0 @@ -# Copyright (c) 2019 Pieter Wuille -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test-only secp256k1 elliptic curve implementation - -WARNING: This code is slow, uses bad randomness, does not properly protect -keys, and is trivially vulnerable to side channel attacks. Do not use for -anything but tests.""" -import random -import hashlib -import hmac - -def TaggedHash(tag, data): - ss = hashlib.sha256(tag.encode('utf-8')).digest() - ss += ss - ss += data - return hashlib.sha256(ss).digest() - -def modinv(a, n): - """Compute the modular inverse of a modulo n - - See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers. - """ - t1, t2 = 0, 1 - r1, r2 = n, a - while r2 != 0: - q = r1 // r2 - t1, t2 = t2, t1 - q * t2 - r1, r2 = r2, r1 - q * r2 - if r1 > 1: - return None - if t1 < 0: - t1 += n - return t1 - -def jacobi_symbol(n, k): - """Compute the Jacobi symbol of n modulo k - - See http://en.wikipedia.org/wiki/Jacobi_symbol - - For our application k is always prime, so this is the same as the Legendre symbol.""" - assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k" - n %= k - t = 0 - while n != 0: - while n & 1 == 0: - n >>= 1 - r = k & 7 - t ^= (r == 3 or r == 5) - n, k = k, n - t ^= (n & k & 3 == 3) - n = n % k - if k == 1: - return -1 if t else 1 - return 0 - -def modsqrt(a, p): - """Compute the square root of a modulo p when p % 4 = 3. - - The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm - - Limiting this function to only work for p % 4 = 3 means we don't need to - iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd - is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4) - - secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4. - """ - if p % 4 != 3: - raise NotImplementedError("modsqrt only implemented for p % 4 = 3") - sqrt = pow(a, (p + 1)//4, p) - if pow(sqrt, 2, p) == a % p: - return sqrt - return None - -def int_or_bytes(s): - "Convert 32-bytes to int while accepting also int and returning it as is." - if isinstance(s, bytes): - assert(len(s) == 32) - s = int.from_bytes(s, 'big') - elif not isinstance(s, int): - raise TypeError - return s - -class EllipticCurve: - def __init__(self, p, a, b): - """Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p).""" - self.p = p - self.a = a % p - self.b = b % p - - def affine(self, p1): - """Convert a Jacobian point tuple p1 to affine form, or None if at infinity. - - An affine point is represented as the Jacobian (x, y, 1)""" - x1, y1, z1 = p1 - if z1 == 0: - return None - inv = modinv(z1, self.p) - inv_2 = (inv**2) % self.p - inv_3 = (inv_2 * inv) % self.p - return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1) - - def has_even_y(self, p1): - """Whether the point p1 has an even Y coordinate when expressed in affine coordinates.""" - return not (p1[2] == 0 or self.affine(p1)[1] & 1) - - def negate(self, p1): - """Negate a Jacobian point tuple p1.""" - x1, y1, z1 = p1 - return (x1, (self.p - y1) % self.p, z1) - - def on_curve(self, p1): - """Determine whether a Jacobian tuple p is on the curve (and not infinity)""" - x1, y1, z1 = p1 - z2 = pow(z1, 2, self.p) - z4 = pow(z2, 2, self.p) - return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0 - - def is_x_coord(self, x): - """Test whether x is a valid X coordinate on the curve.""" - x_3 = pow(x, 3, self.p) - return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1 - - def lift_x(self, x): - """Given an X coordinate on the curve, return a corresponding affine point.""" - x_3 = pow(x, 3, self.p) - v = x_3 + self.a * x + self.b - y = modsqrt(v, self.p) - if y is None: - return None - return (x, y, 1) - - def double(self, p1): - """Double a Jacobian tuple p1 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling""" - x1, y1, z1 = p1 - if z1 == 0: - return (0, 1, 0) - y1_2 = (y1**2) % self.p - y1_4 = (y1_2**2) % self.p - x1_2 = (x1**2) % self.p - s = (4*x1*y1_2) % self.p - m = 3*x1_2 - if self.a: - m += self.a * pow(z1, 4, self.p) - m = m % self.p - x2 = (m**2 - 2*s) % self.p - y2 = (m*(s - x2) - 8*y1_4) % self.p - z2 = (2*y1*z1) % self.p - return (x2, y2, z2) - - def add_mixed(self, p1, p2): - """Add a Jacobian tuple p1 and an affine tuple p2 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)""" - x1, y1, z1 = p1 - x2, y2, z2 = p2 - assert(z2 == 1) - # Adding to the point at infinity is a no-op - if z1 == 0: - return p2 - z1_2 = (z1**2) % self.p - z1_3 = (z1_2 * z1) % self.p - u2 = (x2 * z1_2) % self.p - s2 = (y2 * z1_3) % self.p - if x1 == u2: - if (y1 != s2): - # p1 and p2 are inverses. Return the point at infinity. - return (0, 1, 0) - # p1 == p2. The formulas below fail when the two points are equal. - return self.double(p1) - h = u2 - x1 - r = s2 - y1 - h_2 = (h**2) % self.p - h_3 = (h_2 * h) % self.p - u1_h_2 = (x1 * h_2) % self.p - x3 = (r**2 - h_3 - 2*u1_h_2) % self.p - y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p - z3 = (h*z1) % self.p - return (x3, y3, z3) - - def add(self, p1, p2): - """Add two Jacobian tuples p1 and p2 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition""" - x1, y1, z1 = p1 - x2, y2, z2 = p2 - # Adding the point at infinity is a no-op - if z1 == 0: - return p2 - if z2 == 0: - return p1 - # Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1 - if z1 == 1: - return self.add_mixed(p2, p1) - if z2 == 1: - return self.add_mixed(p1, p2) - z1_2 = (z1**2) % self.p - z1_3 = (z1_2 * z1) % self.p - z2_2 = (z2**2) % self.p - z2_3 = (z2_2 * z2) % self.p - u1 = (x1 * z2_2) % self.p - u2 = (x2 * z1_2) % self.p - s1 = (y1 * z2_3) % self.p - s2 = (y2 * z1_3) % self.p - if u1 == u2: - if (s1 != s2): - # p1 and p2 are inverses. Return the point at infinity. - return (0, 1, 0) - # p1 == p2. The formulas below fail when the two points are equal. - return self.double(p1) - h = u2 - u1 - r = s2 - s1 - h_2 = (h**2) % self.p - h_3 = (h_2 * h) % self.p - u1_h_2 = (u1 * h_2) % self.p - x3 = (r**2 - h_3 - 2*u1_h_2) % self.p - y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p - z3 = (h*z1*z2) % self.p - return (x3, y3, z3) - - def mul(self, ps): - """Compute a (multi) point multiplication - - ps is a list of (Jacobian tuple, scalar) pairs. - """ - r = (0, 1, 0) - for i in range(255, -1, -1): - r = self.double(r) - for (p, n) in ps: - if ((n >> i) & 1): - r = self.add(r, p) - return r - -SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977 -SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7) -SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1) -SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2 -NUMS_H = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 - -class ECPubKey(): - """A secp256k1 public key""" - - def __init__(self): - """Construct an uninitialized public key""" - self.valid = False - - def __repr__(self): - return self.get_bytes().hex() - - def __eq__(self, other): - assert isinstance(other, ECPubKey) - return self.get_bytes() == other.get_bytes() - - def __hash__(self): - return hash(self.get_bytes()) - - def set(self, data): - """Construct a public key from a serialization in compressed or uncompressed DER format or BIP340 format""" - if (len(data) == 65 and data[0] == 0x04): - p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1) - self.valid = SECP256K1.on_curve(p) - if self.valid: - self.p = p - self.compressed = False - elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)): - x = int.from_bytes(data[1:33], 'big') - if SECP256K1.is_x_coord(x): - p = SECP256K1.lift_x(x) - # if the oddness of the y co-ord isn't correct, find the other - # valid y - if (p[1] & 1) != (data[0] & 1): - p = SECP256K1.negate(p) - self.p = p - self.valid = True - self.compressed = True - else: - self.valid = False - elif (len(data) == 32): - x = int.from_bytes(data[0:32], 'big') - if SECP256K1.is_x_coord(x): - p = SECP256K1.lift_x(x) - # if the oddness of the y co-ord isn't correct, find the other - # valid y - if p[1]%2 != 0: - p = SECP256K1.negate(p) - self.p = p - self.valid = True - self.compressed = True - else: - self.valid = False - else: - self.valid = False - return self - - @property - def is_compressed(self): - return self.compressed - - @property - def is_valid(self): - return self.valid - - def get_y(self): - return SECP256K1.affine(self.p)[1] - - def get_x(self): - return SECP256K1.affine(self.p)[0] - - def get_bytes(self, bip340=True): - assert(self.valid) - p = SECP256K1.affine(self.p) - if p is None: - return None - if bip340: - return bytes(p[0].to_bytes(32, 'big')) - elif self.compressed: - return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big') - else: - return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big') - - def verify_ecdsa(self, sig, msg, low_s=True): - """Verify a strictly DER-encoded ECDSA signature against this pubkey. - - See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the - ECDSA verifier algorithm""" - assert(self.valid) - - # Extract r and s from the DER formatted signature. Return false for - # any DER encoding errors. - if (sig[1] + 2 != len(sig)): - return False - if (len(sig) < 4): - return False - if (sig[0] != 0x30): - return False - if (sig[2] != 0x02): - return False - rlen = sig[3] - if (len(sig) < 6 + rlen): - return False - if rlen < 1 or rlen > 33: - return False - if sig[4] >= 0x80: - return False - if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)): - return False - r = int.from_bytes(sig[4:4+rlen], 'big') - if (sig[4+rlen] != 0x02): - return False - slen = sig[5+rlen] - if slen < 1 or slen > 33: - return False - if (len(sig) != 6 + rlen + slen): - return False - if sig[6+rlen] >= 0x80: - return False - if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)): - return False - s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big') - - # Verify that r and s are within the group order - if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER: - return False - if low_s and s >= SECP256K1_ORDER_HALF: - return False - z = int.from_bytes(msg, 'big') - - # Run verifier algorithm on r, s - w = modinv(s, SECP256K1_ORDER) - u1 = z*w % SECP256K1_ORDER - u2 = r*w % SECP256K1_ORDER - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)])) - if R is None or R[0] != r: - return False - return True - - def verify_schnorr(self, sig, msg): - assert(len(msg) == 32) - assert(len(sig) == 64) - assert(self.valid) - r = int.from_bytes(sig[0:32], 'big') - if r >= SECP256K1_FIELD_SIZE: - return False - s = int.from_bytes(sig[32:64], 'big') - if s >= SECP256K1_ORDER: - return False - e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + self.get_bytes() + msg), 'big') % SECP256K1_ORDER - R = SECP256K1.mul([(SECP256K1_G, s), (self.p, SECP256K1_ORDER - e)]) - if not SECP256K1.has_even_y(R): - return False - if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]: - return False - return True - - def __add__(self, other): - """Adds two ECPubKey points.""" - assert isinstance(other, ECPubKey) - assert self.valid - assert other.valid - ret = ECPubKey() - ret.p = SECP256K1.add(other.p, self.p) - ret.valid = True - ret.compressed = self.compressed - return ret - - def __radd__(self, other): - """Allows this ECPubKey to be added to 0 for sum()""" - if other == 0: - return self - else: - return self + other - - def __mul__(self, other): - """Multiplies ECPubKey point with a scalar(int/32bytes/ECKey).""" - if isinstance(other, ECKey): - assert self.valid - assert other.secret is not None - multiplier = other.secret - else: - # int_or_bytes checks that other is `int` or `bytes` - multiplier = int_or_bytes(other) - - assert multiplier < SECP256K1_ORDER - multiplier = multiplier % SECP256K1_ORDER - ret = ECPubKey() - ret.p = SECP256K1.mul([(self.p, multiplier)]) - ret.valid = True - ret.compressed = self.compressed - return ret - - def __rmul__(self, other): - """Multiplies a scalar(int/32bytes/ECKey) with an ECPubKey point""" - return self * other - - def __sub__(self, other): - """Subtract one point from another""" - assert isinstance(other, ECPubKey) - assert self.valid - assert other.valid - ret = ECPubKey() - ret.p = SECP256K1.add(self.p, SECP256K1.negate(other.p)) - ret.valid = True - ret.compressed = self.compressed - return ret - - def tweak_add(self, tweak): - assert(self.valid) - t = int_or_bytes(tweak) - if t >= SECP256K1_ORDER: - return None - tweaked = SECP256K1.affine(SECP256K1.mul([(self.p, 1), (SECP256K1_G, t)])) - if tweaked is None: - return None - ret = ECPubKey() - ret.p = tweaked - ret.valid = True - ret.compressed = self.compressed - return ret - - def mul(self, data): - """Multiplies ECPubKey point with scalar data.""" - assert self.valid - other = ECKey() - other.set(data, True) - return self * other - - def negate(self): - self.p = SECP256K1.affine(SECP256K1.negate(self.p)) - -def rfc6979_nonce(key): - """Compute signing nonce using RFC6979.""" - v = bytes([1] * 32) - k = bytes([0] * 32) - k = hmac.new(k, v + b"\x00" + key, 'sha256').digest() - v = hmac.new(k, v, 'sha256').digest() - k = hmac.new(k, v + b"\x01" + key, 'sha256').digest() - v = hmac.new(k, v, 'sha256').digest() - return hmac.new(k, v, 'sha256').digest() - -class ECKey(): - """A secp256k1 private key""" - - def __init__(self): - self.valid = False - - def __repr__(self): - return str(self.secret) - - def __eq__(self, other): - assert isinstance(other, ECKey) - return self.secret == other.secret - - def __hash__(self): - return hash(self.secret) - - def set(self, secret, compressed=True): - """Construct a private key object from either 32-bytes or an int secret and a compressed flag.""" - secret = int_or_bytes(secret) - - self.valid = (secret > 0 and secret < SECP256K1_ORDER) - if self.valid: - self.secret = secret - self.compressed = compressed - return self - - def generate(self, compressed=True): - """Generate a random private key (compressed or uncompressed).""" - self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed) - return self - - def get_bytes(self): - """Retrieve the 32-byte representation of this key.""" - assert(self.valid) - return self.secret.to_bytes(32, 'big') - - def as_int(self): - return self.secret - - def from_int(self, secret, compressed=True): - self.valid = (secret > 0 and secret < SECP256K1_ORDER) - if self.valid: - self.secret = secret - self.compressed = compressed - - def __add__(self, other): - """Add key secrets. Returns compressed key.""" - assert isinstance(other, ECKey) - assert other.secret > 0 and other.secret < SECP256K1_ORDER - assert self.valid is True - ret_data = ((self.secret + other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') - ret = ECKey() - ret.set(ret_data, True) - return ret - - def __radd__(self, other): - """Allows this ECKey to be added to 0 for sum()""" - if other == 0: - return self - else: - return self + other - - def __sub__(self, other): - """Subtract key secrets. Returns compressed key.""" - assert isinstance(other, ECKey) - assert other.secret > 0 and other.secret < SECP256K1_ORDER - assert self.valid is True - ret_data = ((self.secret - other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') - ret = ECKey() - ret.set(ret_data, True) - return ret - - def __mul__(self, other): - """Multiply a private key by another private key or multiply a public key by a private key. Returns compressed key.""" - if isinstance(other, ECKey): - assert other.secret > 0 and other.secret < SECP256K1_ORDER - assert self.valid is True - ret_data = ((self.secret * other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') - ret = ECKey() - ret.set(ret_data, True) - return ret - elif isinstance(other, ECPubKey): - return other * self - else: - # ECKey().set() checks that other is an `int` or `bytes` - assert self.valid - second = ECKey().set(other, self.compressed) - return self * second - - def __rmul__(self, other): - return self * other - - def add(self, data): - """Add key to scalar data. Returns compressed key.""" - other = ECKey() - other.set(data, True) - return self + other - - def mul(self, data): - """Multiply key secret with scalar data. Returns compressed key.""" - other = ECKey() - other.set(data, True) - return self * other - - def negate(self): - """Negate a private key.""" - assert self.valid - self.secret = SECP256K1_ORDER - self.secret - - @property - def is_valid(self): - return self.valid - - @property - def is_compressed(self): - return self.compressed - - def get_pubkey(self): - """Compute an ECPubKey object for this secret key.""" - assert(self.valid) - ret = ECPubKey() - p = SECP256K1.mul([(SECP256K1_G, self.secret)]) - ret.p = p - ret.valid = True - ret.compressed = self.compressed - return ret - - def sign_ecdsa(self, msg, low_s=True, rfc6979=False): - """Construct a DER-encoded ECDSA signature with this key. - - See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the - ECDSA signer algorithm.""" - assert(self.valid) - z = int.from_bytes(msg, 'big') - # Note: no RFC6979 by default, but a simple random nonce (some tests rely on distinct transactions for the same operation) - if rfc6979: - k = int.from_bytes(rfc6979_nonce(self.secret.to_bytes(32, 'big') + msg), 'big') - else: - k = random.randrange(1, SECP256K1_ORDER) - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)])) - r = R[0] % SECP256K1_ORDER - s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER - if low_s and s > SECP256K1_ORDER_HALF: - s = SECP256K1_ORDER - s - # Represent in DER format. The byte representations of r and s have - # length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33 - # bytes). - rb = r.to_bytes((r.bit_length() + 8) // 8, 'big') - sb = s.to_bytes((s.bit_length() + 8) // 8, 'big') - return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb - - def sign_schnorr(self, msg, aux=None): - """Create a Schnorr signature (see BIP340).""" - if aux is None: - aux = bytes(32) - - assert self.valid - assert len(msg) == 32 - assert len(aux) == 32 - - t = (self.secret ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big') - kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + self.get_pubkey().get_bytes() + msg), 'big') % SECP256K1_ORDER - assert kp != 0 - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) - k = kp if SECP256K1.has_even_y(R) else SECP256K1_ORDER - kp - e = int.from_bytes(TaggedHash("BIP0340/challenge", R[0].to_bytes(32, 'big') + self.get_pubkey().get_bytes() + msg), 'big') % SECP256K1_ORDER - return R[0].to_bytes(32, 'big') + ((k + e * self.secret) % SECP256K1_ORDER).to_bytes(32, 'big') - - def tweak_add(self, tweak): - """Return a tweaked version of this private key.""" - assert(self.valid) - t = int_or_bytes(tweak) - if t >= SECP256K1_ORDER: - return None - tweaked = (self.secret + t) % SECP256K1_ORDER - if tweaked == 0: - return None - ret = ECKey() - ret.set(tweaked.to_bytes(32, 'big'), self.compressed) - return ret - -def generate_key_pair(secret=None, compressed=True): - """Convenience function to generate a private-public key pair.""" - d = ECKey() - if secret: - d.set(secret, compressed) - else: - d.generate(compressed) - - P = d.get_pubkey() - return d, P - -def generate_bip340_key_pair(): - """Convenience function to generate a BIP0340 private-public key pair.""" - d = ECKey() - d.generate() - P = d.get_pubkey() - if P.get_y()%2 != 0: - d.negate() - P.negate() - return d, P - -def generate_schnorr_nonce(): - """Generate a random valid BIP340 nonce. - - See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki. - This implementation ensures the y-coordinate of the nonce point is even.""" - kp = random.randrange(1, SECP256K1_ORDER) - assert kp != 0 - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) - k = kp if R[1] % 2 == 0 else SECP256K1_ORDER - kp - k_key = ECKey() - k_key.set(k.to_bytes(32, 'big'), True) - return k_key diff --git a/bip-0352/secp256k1lab/.github/workflows/main.yml b/bip-0352/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..4950b96550 --- /dev/null +++ b/bip-0352/secp256k1lab/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Tests +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx ruff check . + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx mypy . diff --git a/bip-0352/secp256k1lab/.python-version b/bip-0352/secp256k1lab/.python-version new file mode 100644 index 0000000000..bd28b9c5c2 --- /dev/null +++ b/bip-0352/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/bip-0352/secp256k1lab/CHANGELOG.md b/bip-0352/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..15779717c4 --- /dev/null +++ b/bip-0352/secp256k1lab/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-03-31 + +Initial release. diff --git a/bip-0352/secp256k1lab/COPYING b/bip-0352/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0352/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bip-0352/secp256k1lab/README.md b/bip-0352/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/bip-0352/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + +![Dependencies: None](https://img.shields.io/badge/dependencies-none-success) + +An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education. + +Features: +* Low-level secp256k1 field and group arithmetic. +* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). +* ECDH key exchange. + +WARNING: The code in this library is slow and trivially vulnerable to side channel attacks. diff --git a/bip-0352/secp256k1lab/pyproject.toml b/bip-0352/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..a0bdd19f42 --- /dev/null +++ b/bip-0352/secp256k1lab/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "secp256k1lab" +version = "1.0.0" +description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education" +readme = "README.md" +authors = [ + { name = "Pieter Wuille", email = "pieter@wuille.net" }, + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +maintainers = [ + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +requires-python = ">=3.9" +license = "MIT" +license-files = ["COPYING"] +keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", +] +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0352/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0352/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0352/secp256k1lab/src/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0352/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0352/secp256k1lab/src/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/keys.py b/bip-0352/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0352/secp256k1lab/src/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/py.typed b/bip-0352/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0374/secp256k1.py b/bip-0352/secp256k1lab/src/secp256k1lab/secp256k1.py old mode 100755 new mode 100644 similarity index 55% rename from bip-0374/secp256k1.py rename to bip-0352/secp256k1lab/src/secp256k1lab/secp256k1.py index b83d028f92..6e262bf51e --- a/bip-0374/secp256k1.py +++ b/bip-0352/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright (c) 2022-2023 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -17,31 +15,29 @@ * G: the secp256k1 generator point """ -import unittest -from hashlib import sha256 - -class FE: - """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977). +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. They are represented internally in numerator / denominator form, in order to delay inversions. """ # The size of the field (also its modulus and characteristic). - SIZE = 2**256 - 2**32 - 977 + SIZE: int def __init__(self, a=0, b=1): """Initialize a field element a/b; both a and b can be ints or field elements.""" - if isinstance(a, FE): + if isinstance(a, type(self)): num = a._num den = a._den else: - num = a % FE.SIZE + num = a % self.SIZE den = 1 - if isinstance(b, FE): - den = (den * b._num) % FE.SIZE - num = (num * b._den) % FE.SIZE + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE else: - den = (den * b) % FE.SIZE + den = (den * b) % self.SIZE assert den != 0 if num == 0: den = 1 @@ -50,71 +46,74 @@ def __init__(self, a=0, b=1): def __add__(self, a): """Compute the sum of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._den + self._den * a._num, self._den * a._den) - return FE(self._num + self._den * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented def __radd__(self, a): """Compute the sum of an integer and a field element.""" - return FE(a) + self + return type(self)(a) + self + + @classmethod + # REVIEW This should be + # def sum(cls, *es: Iterable[Self]) -> Self: + # but Self needs the typing_extension package on Python <= 3.12. + def sum(cls, *es): + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) def __sub__(self, a): """Compute the difference of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._den - self._den * a._num, self._den * a._den) - return FE(self._num - self._den * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented def __rsub__(self, a): """Compute the difference of an integer and a field element.""" - return FE(a) - self + return type(self)(a) - self def __mul__(self, a): """Compute the product of two field elements (second may be int).""" - if isinstance(a, FE): - return FE(self._num * a._num, self._den * a._den) - return FE(self._num * a, self._den) + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented def __rmul__(self, a): """Compute the product of an integer with a field element.""" - return FE(a) * self + return type(self)(a) * self def __truediv__(self, a): """Compute the ratio of two field elements (second may be int).""" - return FE(self, a) + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented def __pow__(self, a): """Raise a field element to an integer power.""" - return FE(pow(self._num, a, FE.SIZE), pow(self._den, a, FE.SIZE)) + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) def __neg__(self): """Negate a field element.""" - return FE(-self._num, self._den) + return type(self)(-self._num, self._den) def __int__(self): - """Convert a field element to an integer in range 0..p-1. The result is cached.""" + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" if self._den != 1: - self._num = (self._num * pow(self._den, -1, FE.SIZE)) % FE.SIZE + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE self._den = 1 return self._num def sqrt(self): - """Compute the square root of a field element if it exists (None otherwise). - - Due to the fact that our modulus is of the form (p % 4) == 3, the Tonelli-Shanks - algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply - raising the argument to the power (p + 1) / 4. - - To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, - and thus only half of the non-zero field elements are squares. An element a is - a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're - looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent - to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to - x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.""" - v = int(self) - s = pow(v, (FE.SIZE + 1) // 4, FE.SIZE) - if s**2 % FE.SIZE == v: - return FE(s) - return None + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError def is_square(self): """Determine if this field element has a square root.""" @@ -122,26 +121,42 @@ def is_square(self): return self.sqrt() is not None def is_even(self): - """Determine whether this field element, represented as integer in 0..p-1, is even.""" + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" return int(self) & 1 == 0 def __eq__(self, a): """Check whether two field elements are equal (second may be an int).""" - if isinstance(a, FE): - return (self._num * a._den - self._den * a._num) % FE.SIZE == 0 - return (self._num - self._den * a) % FE.SIZE == 0 + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + return (self._num - self._den * a) % self.SIZE == 0 def to_bytes(self): """Convert a field element to a 32-byte array (BE byte order).""" return int(self).to_bytes(32, 'big') - @staticmethod - def from_bytes(b): + @classmethod + def from_int_checked(cls, v): + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v): + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b): """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" v = int.from_bytes(b, 'big') - if v >= FE.SIZE: - return None - return FE(v) + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b): + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) def __str__(self): """Convert this field element to a 64 character hex string.""" @@ -149,12 +164,40 @@ def __str__(self): def __repr__(self): """Get a string representation of this field element.""" - return f"FE(0x{int(self):x})" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self): + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 class GE: """Objects of this class represent secp256k1 group elements (curve points or infinity) + GE objects are immutable. + Normal points on the curve have fields: * x: the x coordinate (a field element) * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) @@ -164,26 +207,47 @@ class GE: * infinity: True """ + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + # Order of the group (number of points on the curve, plus 1 for infinity) - ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + ORDER = Scalar.SIZE # Number of valid distinct x coordinates on the curve. ORDER_HALF = ORDER // 2 + @property + def infinity(self): + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self): + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self): + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + def __init__(self, x=None, y=None): """Initialize a group element with specified x and y coordinates, or infinity.""" if x is None: # Initialize as infinity. assert y is None - self.infinity = True + self._infinity = True else: # Initialize as point on the curve (and check that it is). fx = FE(x) fy = FE(y) assert fy**2 == fx**3 + 7 - self.infinity = False - self.x = fx - self.y = fy + self._infinity = False + self._x = fx + self._y = fy def __add__(self, a): """Add two group elements together.""" @@ -209,13 +273,20 @@ def __add__(self, a): return GE(x, y) @staticmethod - def mul(*aps): + def sum(*ps): + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps): """Compute a (batch) scalar group element multiplication. - GE.mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, but more efficient.""" # Reduce all the scalars modulo order first (so we can deal with negatives etc). - naps = [(a % GE.ORDER, p) for a, p in aps] + naps = [(int(a), p) for a, p in aps] # Start with point at infinity. r = GE() # Iterate over all bit positions, from high to low. @@ -231,8 +302,8 @@ def mul(*aps): def __rmul__(self, a): """Multiply an integer with a group element.""" if self == G: - return FAST_G.mul(a) - return GE.mul((a, self)) + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) def __neg__(self): """Compute the negation of a group element.""" @@ -244,11 +315,26 @@ def __sub__(self, a): """Subtract a group element from another.""" return self + (-a) + def __eq__(self, a): + """Check if two group elements are equal.""" + return (self - a).infinity + + def has_even_y(self): + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + def to_bytes_compressed(self): """Convert a non-infinite group element to 33-byte compressed encoding.""" assert not self.infinity return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + def to_bytes_compressed_with_infinity(self): + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + def to_bytes_uncompressed(self): """Convert a non-infinite group element to 65-byte uncompressed encoding.""" assert not self.infinity @@ -264,44 +350,51 @@ def lift_x(x): """Return group element with specified field element as x coordinate (and even y).""" y = (FE(x)**3 + 7).sqrt() if y is None: - return None + raise ValueError if not y.is_even(): y = -y return GE(x, y) + @staticmethod + def from_bytes_compressed(b): + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_uncompressed(b): + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + @staticmethod def from_bytes(b): """Convert a compressed or uncompressed encoding to a group element.""" assert len(b) in (33, 65) if len(b) == 33: - if b[0] != 2 and b[0] != 3: - return None - x = FE.from_bytes(b[1:]) - if x is None: - return None - r = GE.lift_x(x) - if r is None: - return None - if b[0] == 3: - r = -r - return r + return GE.from_bytes_compressed(b) else: - if b[0] != 4: - return None - x = FE.from_bytes(b[1:33]) - y = FE.from_bytes(b[33:]) - if y**2 != x**3 + 7: - return None - return GE(x, y) + return GE.from_bytes_uncompressed(b) @staticmethod def from_bytes_xonly(b): """Convert a point given in xonly encoding to a group element.""" assert len(b) == 32 - x = FE.from_bytes(b) - if x is None: - return None - return GE.lift_x(x) + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r @staticmethod def is_valid_x(x): @@ -320,6 +413,13 @@ def __repr__(self): return "GE()" return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + def __hash__(self): + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + # The secp256k1 generator point G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) @@ -344,7 +444,7 @@ def __init__(self, p): def mul(self, a): result = GE() - a = a % GE.ORDER + a = int(a) for bit in range(a.bit_length()): if a & (1 << bit): result += self.table[bit] @@ -352,9 +452,3 @@ def mul(self, a): # Precomputed table with multiples of G for fast multiplication FAST_G = FastGEMul(G) - -class TestFrameworkSecp256k1(unittest.TestCase): - def test_H(self): - H = sha256(G.to_bytes_uncompressed()).digest() - assert GE.lift_x(FE.from_bytes(H)) is not None - self.assertEqual(H.hex(), "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") diff --git a/bip-0352/secp256k1lab/src/secp256k1lab/util.py b/bip-0352/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0352/secp256k1lab/src/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() diff --git a/bip-0352/send_and_receive_test_vectors.json b/bip-0352/send_and_receive_test_vectors.json index 3a5e1d3d94..07a330afb9 100644 --- a/bip-0352/send_and_receive_test_vectors.json +++ b/bip-0352/send_and_receive_test_vectors.json @@ -3185,5 +3185,2411 @@ } } ] + }, + { + "comment": "Maximum per-group recipient limit K_max is exceeded (2324 matches): sending fails, receiver doesn't scan beyond limit", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3cde0e7ad8adf24a4e52fd18efafffb94f73f4c7facf2627a5b588ad1c9db87f", + "vout": 0, + "scriptSig": "", + "txinwitness": "01407ce01bc5ef2e4aae353e7f291fec32623d6a491b5421be6dccc70345f32ebae5baa6654c60c074e72da76fd06b343474370e56d3ec0413156ec6261757125ad4", + "prevout": { + "scriptPubKey": { + "hex": "5120bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + }, + "private_key": "0000000000000000000000000000000000000000000000000000000000001337" + } + ], + "recipients": [ + { + "address": "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kqmk4a3pm24z0yepw6aw27pku56md0hq5ace0l3p3jstkqaajwrsjqc5dde5", + "scan_pub_key": "02e6c47ae6962c5ea1559f48b437c193a1bcb1d72d08d75d743ba3cbfb8e7afbeb", + "spend_pub_key": "0376af621daaa27932176bae57836e535b6bee0a77197fe218ca0bb03bd9387090", + "count": 2324 + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_private_key_sum": "0000000000000000000000000000000000000000000000000000000000001337", + "input_pub_keys": [ + "02bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3cde0e7ad8adf24a4e52fd18efafffb94f73f4c7facf2627a5b588ad1c9db87f", + "vout": 0, + "scriptSig": "", + "txinwitness": "01407ce01bc5ef2e4aae353e7f291fec32623d6a491b5421be6dccc70345f32ebae5baa6654c60c074e72da76fd06b343474370e56d3ec0413156ec6261757125ad4", + "prevout": { + "scriptPubKey": { + "hex": "5120bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + }, + "private_key": "0000000000000000000000000000000000000000000000000000000000001337" + } + ], + "outputs": [ + "5cfb6fd7a7e5d2af0fee276f2104ce2e5c86cde7955b98f2f1fb32811b448f2f", + "da290e0d79ead5cd2e5d150aac80dcab54dbd0e68f241a6c34dc30906eabd13d", + "d4a2b1458f0c8b20d756bde8e545560d7e7a65f6b4158f01ed34a913ba3a4603", + "24752e555b0edcca123f769113692224bf1e6db137cddeb7d69ae52e51144e9e", + "c911f722ef9a3594d6b85c585d4f3ed8dc437f850591dc627a0154723644114b", + "ef91ddaa83a0b748d638dc9be8c9771b049a04815f03967b7ee993bde6c260ab", + "ee4ed334fb38af95f52c6bbadcf2f491e646b9cc8ed31da67635f36c3cfdaba0", + "a17d61b4772e453b32782744902261e051510cbafc04f086f53480d1ceee1a9c", + "44d89f5e903739072e3aff8a3eb2f351520f77ff288ac8be277be3f28834bdd2", + "3eff02a4a04d66d63f6bc93a693f23db703f351c0a1ff1656b907ed348a9a35f", + "628c23a6e21e6a47b80274fc80e4decb98ff79b8040c7f9ea5972e4920b62b6d", + "e72190c0a5ea167c2663b18d378520b9228da61ebbc99c3fa2af39e01fb9501e", + "24183edbc5b80588e0e5b803e924e77fc1cab1afca54cfd285dad9bfc2a34f6d", + "c45bad25a223f0f617e8167f858037907e4454210a745f3b9798c8913646dfc6", + "9f9cec85011d7f718f96ee1a2940df21920a857cd1a545255579cb615f285a42", + "8c60891f9bc2f772efc51d44f7cc885161412835b6fd5aac3b0cf57c3c2f9154", + "85ac46a6ac9569ed67d1bc5342ed3b591af5e01581d1b09bcb2c71deb9b2c2c9", + "8a6bfe47e616c6854fc797a4af53ef51c942b99eae18f316594fc8eb8869ca05", + "ed5e1aa6a8b4e805273367b8ec3ade16ed6494270fc2169ba920b89770efe8eb", + "2448503c55a1a7035c069e1d5461b81774c43bf6061421fdd0157009bfe96a8a", + "d0dcb3d7b551eac8d31fceccb83df415c554860d5541dd08ff5fa0028ce5704f", + "f11a298b54b650613e0b0183552c9c9e42c3812e03cf713d351ef71106b6ee3f", + "49749206f23791f858c453cb60160d48710075bdbfd33907b0046d10f0b0295e", + "3fbfdb52cbf0ea9e6655a211748bf2ac2dc92ff3cfeeef0f0a26be9987016de0", + "d4a29144511981919cadbe8227fc68379e85d3fe399f177c2ae36c304b6ad6b0", + "c586cc196ff56f42396f426a0200afee3a6c41058d81f19caf6536ec0efd5605", + "c666994f027ceab025256af28d8ea6aec452fb8b0e1f69e7e13a4f3c5119f4ae", + "5c9216cc4529c3f9c62caf70b04f8354ce9ec2cabaef745092bea1b160328fda", + "d9aea54323503d78c026a8cd465c19a6df54b32ee866714d190e8260054361da", + "ba71f87ca1d74eee115d8e8a26293fe2c88d9d66cff185a7a079282f02f931ad", + "b8b930666b2df74c33dc8963221080c115246070e3fcd51f39bee71312db4fc4", + "b7f42d2f18a3ac61f570b64ae6864f7dbff774a370ba27358d76797e841d5304", + "d636d179055241a0f8bb7db193912da513a8999684d9829b266985a8083819a9", + "be9622aec125f9605bcee193a99ca27c16bc591122ff8b44c511a85e6f63ec9d", + "af5beb25776f84fadd6079c3ffd85775a35ad74096a58229e1d5076d836ca25d", + "75fb55ec31e304456f663c9a66321e67f9d9bd8f43749494acc5189ed57b0d4a", + "8dfebff33c514486d06eeb4ab8eb8471496556858c4382f73780620101a77f71", + "93bfd4e75706951de4a59c7f70c5d2ce4c617b4bde0b0b23ae046df096116726", + "b6eeca22f43bd62a4fcb3eee719e07d39ea703fa0707970fc197916f2efebbb5", + "0437c3971f73342e5c41178590e1cb1364cb855b2d20b7b6c1c1ed0e7da5b57b", + "d284c831cd9d52793977925ce3111b68f36d1ea456e46716158fe6947fb6d635", + "d95b8d2789fd06cc764d47cc7e0697404220ae8f938ef47dcc9a1bba299fbb9f", + "33599fb1fe313b5a2680b126bfee93cb097d938915d425a63e307e5834479b95", + "05ba86903fac2f02ef2e152072bc6067008ee8ee34ce55ad460801303373eaab", + "0b757802e56728b6d5b28e671a90de573d7251a47fc1bcd9319b617cac8e1463", + "b4a2451e725e4cbcdf47fc736139cfe29c847e2f75cbb7653d80c8522bd48a6c", + "8dd800c3a7cb1ee60f52db3bb69413c4c4247b0b134576552c675970f9903c9b", + "4de21129c6fdf5568040346bbdda272caaadd58218ec6f1f152518068dac4d4d", + "22f6927eae0c3717395df6483d756028839fbf0814f8cf0391159efa65dd2854", + "192d26fe0bc772c705c2657dd5c5033ac41ba408793e6c721f74c53747be16e8", + "9527b8e19570a16ce2d8dd3620dfd915e461bccc82340fe682f7f811b978814d", + "f84039ff95cd44f842d7cf6982a726957a89d90bb151a368e62770720d3efbee", + "b5b36dd84b352cddf715246d0463984b9d7894f50b566a20042c4d485df41946", + "592e1bd733a1095b9e5b770021769090df0c001260803fce9a9b761ba697a5e7", + "dd2fdbd6932d8b56d6ff91db3ed28f282a6ca01588ee5a552d061da215580411", + "82f95ef71d4d0b32e3f0cef89e6e90a8c271cd5536b38c507fc69b354ab51062", + "1800903b2d5f8483a12f0b98a5b35ed39d6cf8f4a52f495395f1816d6e3c139c", + "121a2c2ea70a6986af8bde71b7f75ac9f9e1189eeb9756395e7281702f01936d", + "8578df7e8ec4b068f64dca679310894afec095dbbedbefef5944c3a319809b8b", + "5cebde1dbb352ff62841d7c50c3c45cd0fd1b7b3ebe884a7cf29cef6686695ab", + "362f7a73f2620ea061586b5edbee01f9c026ae6e0d95131c570c44933fc41f10", + "3a19b5976236d44a3d1d6920b348fcef9ec830aac56aa672353c711471ebc36a", + "842c43c5293b03811165f1ff7e04c189ccd355fe08d37808a843a0995541c075", + "0ab1fdbf7117c88aed3f882d5f7dee8964c9246030086b9ee3e2ebed4eb2c731", + "7cf8ca4463d4bd500cc12623ac9d3984cc48e7fbb98bebb4a498c2fd66d54cde", + "631676f3369eb479686256cef33c4fb061c212cc215ab442d8f7837c039ddacc", + "2fff6de5386b65efe6062cc2242b692a1e811a083612ab79fa7f7517f1488767", + "3c7d8b7f2a56e22ade186a70a3039b4d4e4e3c0be6e661770b0a7b2ef18b178b", + "e030882188281a754c00706f621084f96869bf95bb5d005ccc7b2649fbfc747e", + "072cb899c3eb621240792a39bf727bf59398329748317d5ca1addfd8ee2f7e92", + "37cfa6b7214e99bbec6323802115b9615b10851ac85cdc26f213b9532fdefcb8", + "68b14b71ad6b5e2d41ef47fe1b9229a0b8297ede0812d1b9cb95aac185d1a5b8", + "652ef549580cf412bb217fb15b2a0a1db8feb3c79969b95364a80dd97d565f09", + "dd3504b7563a65115a050505bc88ee23a50e37c354773cac329355c16c43d839", + "6d7f05d6ee71459d2ea982f7a5cd4a251d7466d4892e6c8b6aaf45eec66472d7", + "14c28adbf9008d0cbcc978006b6c1ec7be51a134bd5587e94ee33844b62c1ffb", + "f9c20067f16feaaf3f8daa8456dd9334725ebdc8b49b4f74f5a02c38ef5f2804", + "e2c537948a01054b7a0d630952c2ddd395a3a265d618e22ee9d99d4597f82322", + "70e3a71945fee29350b5b10a8ee8dcbd19c2f734e22407bb6b50ec3acb1035a5", + "effbced848df72cd467b1ada7353b1ddc0b7f3f08190c0f2fc747f18303ed9f8", + "02d6eedc5abc295bcfbd4c30a8fa3406c8256d8e51f4714800b4aecb795a976b", + "92134c6158993a0390af92565e166418561c9b157989d02304b68c2ee1babe14", + "6065e3e3aff3e2b2f33bf4af9256e9368b8a303335b0587c8cc9e31cb9db42bd", + "732fd2ba9efabc6d79bd7404c1c6f1d5c1f14c9bfe02ee1320621824847935c2", + "209f9c958b5f4813b32205a44d05c83aa1009cb7b9c7343f0ca3717ae069defc", + "1d274fcb65b6d4c846e95030c37851a97d161f926ed88f6c4dec4ed7007fc23b", + "1d3787106cf47e1e9d08761a6c6f427ef6c07d4a571f23d340ae2d31d34b1d8e", + "5697a3f56a0a2a6bd6991ba05559df9b526d5c42e19604c398548a7cb0dec0b5", + "8c0f581f38238215f10f8017e3d234cbda9d94ca017bbd6b076ac8666014b3d3", + "78f061b0fc912fbbeb0d207b92528ad78ded55ec145c8f18133991847fb9cbe9", + "1e3555081a869f3aafa3d74d6b5afa942baace60869ecd4cd33635f84602b715", + "4f0e36ac0c4813db3fdf79d85ef0db89150dc1be2c879ba06299c049288ab8be", + "6b048c38924b7a1f970cdff081124b739202b703db5c17bafa4ad23a5bcb97e9", + "a5f5e9238aec20bd706262f64671ca2d05961f55ca383953d489edf857e4b2a4", + "a047b08e2af7ae5bfb9e54f98051ff7cd5e7dc88a25afacdd9a324526cd33c63", + "66ae2b9513a0e0ad125001bcd5f3a36e18b4ec009db4020ea4afc4951cb39a34", + "621d8269c8f93b6336f8b7480516d2191cfb15a1d9501463771d0fb70c1afa33", + "76b94079066bff52a077ba1d1ba8cdec4d6484d1ce2f7454b2b04aa3d9cf52ed", + "33c6a108f9d4fe054c085991d89d2a5c32b8f108c95b0e688c5763424c283e54", + "52575ed2ad84295a070a99e7785ff0e4dd32a93d3830be7f7c2811804f3df2f3", + "158dfe014c271c148b38459ef16b49cc3b5c6bdfe052795a6f6a6aacbb3e47b4", + "d471729cb231deeb2f1a67f91962ecfcb9e8c66f66b6b79fe1117d34ae86a7c0", + "e086b1a09ab5307d953e25366b1acf7c41047a926aa6a4d122c5a34e3e0d93d4", + "d541d825401a1734d9961fdb4f0e9ce7af220c6929648e0aa2e885cd28696343", + "a555af7a52ac07e681c247f1d6597f8240d28e76036bbf3b2be1f7cfab90dfd6", + "8598fe7a4a6c6fa865c0ca8e8e65c96301c80965e19e48f5d4ca29ac073551c7", + "70c9ac61ab3508a7419ead73503d8fe51b4ecba73a55d4b167cd4db6c9b0cb8a", + "e594eb83b38ad916176abff3f0ec16d7a814a461012c8f9bc8286274af3f1856", + "fa4e0df6df82f21050394fc2caaa8a8713b131d0d1f7d21eb92b56796095bf17", + "891711c535579b555105e5ec18ea0f2f610c6fe5126ff0f6a4257e99a24033c5", + "f192c9b2c49dab7515d405052f825abead30ef9a870ba3bdad98c1d6256be461", + "70c22f6ceb81744c83eb8dae69980f4583c75431457dbb14f5ecd6c9fae3e63d", + "7d563f44bdfcb2a10ee053f4c05b8d46fa90d44822775f97ee8cfbdeda7a4ca1", + "c09b3df69eaa232a9119e2fb805cdccacb658b2fb9c7f3a2106c8399aa928033", + "15b4b3d50fef72de80fef6e3351dae0f203902e4c4db8ce8d8737b67aed9104d", + "c9b7718633714b3ce1b489f13e53fc647be53cba7703f07a35a8815245b23ff3", + "2d344af9852fc2451ca5d2ed7155e074efd2c53ca9cbed7f48c74658967aeca6", + "c8b778b5c1fc378365d8b342915215117c3d365e12f61f3ccefbe4091879e373", + "1deb076e44c029027c1aa10000305c3eded5d401a036c21a4dd3d3455c0c33a1", + "d2d2dfe410cefbae2309e746690818e8d52dcd80ebd192cddaa00204b6833d05", + "d18a0a0bcc678908ff00b236296f0a955a9aa52a802ee4175849d0bcb3d6738a", + "7af51e08357e455e4dda86d60da75899b5ffce4bebc7f96037d1e404b1260ebe", + "65b930580f9738e1a7af2ac56faad5bdc6a417e75cb872b7f62513f4e8e36b33", + "71c8e88c7c478a4d919d0c80d351331102584d032d82aa25510801e18bb5d6b9", + "2166bad16a6e4a1f77a22b0fb13f7f11d618a123c3c077dbf1ed1504ed68cbb4", + "717a3a5903c2b12aea8be15599ea9864259a964c0139125e649fefbec763890e", + "0d042f5767f6cb2f94526b5b6da8ef986e4a5927eb5f734bdeb6c6ec3bb8211a", + "bb67d586774ae9f3653c609dbdd98f216315b54c0ce24ad9e359d0c2c21614a3", + "945b2962ec44c54ea25e3e35e32a2362c130fc3a9495b16871e8a2eadbe457e7", + "7d5324e5e158c82388cba0d497530c6de0c1ba97131688931c03dbfc60ca7541", + "21805a8f8d43aede3e927b80ef04924d388e96f3cc3947a0dbc1bd1b812539f0", + "27ec492fde5fc3d3784359d9a270b735c1820bc4a6f15ac87797d97de047a6b5", + "4fda2f855ab08a8d4dcba1874402e1662315c872e3ad55681fc4d6c5a89665a3", + "b0dc69204deaa17ac4e0f197f8b23211418b5d54f5bac55414445a66c57fd6f2", + "209262521ab45331639a57523f2e3bbf8250052b84fba81c50c990166a6f58b6", + "b02857949ebddecc58aaba80b9d635ae95cf71f4bfc5ea8fbe05fc55d4c73e96", + "5194133163b28182a261ce0473b1f3ab991a53a08d0e813d3bfb533cfaf1255b", + "c56fe98848fe79124ae1ee09d1b214ee87da51c1440134210c67fe15bf6d1bb0", + "851d949ac91f439ec84f07c481598728ca93acf79cb6505bd77ef776559275f1", + "eeccaf0cd29cb002e2140cffa9fe2c08c53ffd73b99167f4b5e6ab5c94481066", + "821f1058ae54732723937c5ae535cdc0a9ce673eec7fc02bd03c240c93aff24c", + "de836fd21ce91d4a6add661f5d259d0ade4e0ff927d03f67ab5d0b24bff37242", + "fdac0470c61d81d1498e4e0c6fc4ca5ccd02f727413e33c84478ebe844feba13", + "469b4f5af2b2377dc09e5824af17c57f1dd8fbe02294059e16641c1307eff125", + "b77348b40d52d531378dead61cf2eb6fafb4c89f85285a98cd4b6032c0e30a16", + "2cf9616aefee83eddb2df0dda1a3331185f0b772c3a8a62ea3ffbb325290f32b", + "2d23a9035ec627187108b9d13b24134d69ad7cb6f77d2c3ab939db17cc6f129c", + "4925a7008584e04fa769661dd9eb766fe2ea3b66622691249142ca155423f368", + "69090019b3c1635da9d9e4661f55863cb3ab561bcedf2632360745e62b06afc5", + "5cecb744c3e65889c22e14a58e42bb69d068c721f1c547c8c704fbc688c6c6e2", + "2f7371ba97f17c64c4c487742f83755ab61aeb1ee537bcd5f16beb4440d0233a", + "9d1e3dcad79b153296a67238771b91a92c430432a9cc9f7b2ad7667fd67b4de3", + "e9a34ca0067dbb6dbba48e736e1d32337cca5e93f2b76229fafef501979d84ae", + "4d5d36724a2baa0d73657a140493a9ba95e1de95cad74c9753eaad785ac18cbd", + "d135fe3d44b6b0875c1662ecfe33556be0f1b39bc8279dc8730d5e9e351f36ae", + "1fb8439b54ead68f72b7ba4fcaa5041f38d313d92826b57dbda299dab06ab716", + "b96e8d59a7c1006f15b0ac10cd35c08a8008e0c2ecba26d87db3f08c0f6fc991", + "73300dd04c53d270420023957c50410147ea4a2814085d3b98ef25d8d44afeed", + "6941323a74081c1ee28846c4ffe3b4023b29eb3a3b64d0d2dea0f47349fc2138", + "f7cebcfc608a982a3e11b3941abf482d8108a7d59c3719279563788173a2cc6c", + "b76c687ff464c50b58e4231379c5105470264de2826574e8d7fc58d6889d0753", + "f0148c800120bd34159684196bdfb34f08d1af93c74232dd9f59e2ca0a66aed4", + "51e0334c027b5e43e7cd557e301bceb0189903a189dadec2a11c1e2d675e3e9c", + "32a3f110862c9340106f2dbba85c8af5d7ed575635d55cf2af7637a59c83103d", + "b35bc38a430ebd8c7584d0c90bdaaa15aa252b3977fcac456646e839746f9c0a", + "87ea74b899c0b929e83bf31a8d146faea3d26140ea99a3d0d3e0be93a4d6eeb4", + "e222ed1dfcf6505fad77b9cf2e3e38d0ea86fe89a969b88e729ebdf2e6c63f0e", + "d5e92da54ae3432603ea96cd770f4da442f7dcbd0a4f2d82774a33675690297e", + "01e372d5dee08beefd03317d0897d5ef1c5126bbfe00c8967ec9aa05523c5e25", + "369df1d577f66ea1c93b58b770783a74606907713b14ad86b316319d94f84ac7", + "ef7e648860a8b3490440f2a8d541a268b4613f4a94c0dbf685df5973b55f0e88", + "f19f4b29d4d7b70abc4624c01a4ef7ab772bd4feae8cb33bb502f30a11dab29f", + "8ccbea64791275ea047a3648b092ed281acfa21f62a4b50ed111b954e534a697", + "d60a6b4846f218a8369db8deb45972760a1df33c7532d0ef1ae679ba0294216e", + "7aa4638be336fb77bc1ac5dfcd3f5ae002aeed8c42bb46475f718e3c278f0097", + "06a82a57d7951a5629a1153240f6b1f09c150f0b9b777d7c64a0a628e24a0bdc", + "d23d2ce73e24b434f2fe5a6eef2b88eaed4e57848cf357055da6914bb234851d", + "0dfbe791dfc86570004538c64195a7702e33ed121490fee5819537676a021932", + "8e2a510bc84d4600f25812994dfd5b4566c281a153e459cc9460c006ababd093", + "cf73a8cdabecf4323a194f3249cb5b1674037f779c747690eb0d9c9f703f7c8c", + "a8999928014db24e80a27cfb572eacf902b50cdac259b5be80229fe73260ce08", + "f2a13b90ffda09f999ca5d7f9a2cd171ac33ee2a9a889c70a2e2d454dee0ddeb", + "a5fa0377e644e743a38f0b427adefab90d218ba8e9e2894fc1f1f71cd815a92d", + "1a27dc6f452362eca3745218915d24726b72f5d6963a9c41a34b9b7a12aeedda", + "74657a8b361a7d235d94878cf2ac2cf3e18dbd76fa1a88440e3be7ac7a8dc54a", + "15059499248e579d66827fb6700d3c31ab3c1e824fd0b0de583668631f31abe1", + "21dab269f60f67bbce69447b7fec674662bbfe2bf1fb998e164d44e92086a89a", + "a266bfdd44a5969f46985337359fe5ebb6083b8a21cf310ebdf4a62aa16dcb89", + "612ebf41822f25ab638867c1232596a83de60c5bc1a62aea81464cd87c50a53e", + "7c7e8506946118e3af1f5c36f94fc5cddb945e6346a009d21934ea0a384778ac", + "82cc3dcfa10188e9cee8d84a60ab86cef7b564208734ebe8d1c6f4fd583e5466", + "7ec2dce8c901de5c5b5da218d4ca6c304c4a8691568631b1d9d625deab025e8a", + "baf8884aec756bba04cafe11f82e84d5721bc66689a59418e24dba0471e5ed9c", + "2033109b98dec88046390619d056fba7ab1e207591b049b5ca4e5ca7905a0736", + "04337d0bc9948530c0f6bd541442a14cb8da9342cbaae84ffd21f49a3a4ff9b5", + "95a390c9ff01592aefaab09d6409dd618263bd596d5134e024d2e04fc080f9d2", + "c314b11e8ab0957c8c11770b6dd3bf9ba8a281c312f0c25d0dbdb0dab2bf6176", + "9372c5731f8e38ca30845e277b499e77dd3f5fa442f5496c4bdde0512de1e922", + "84ff2e4e6c23f0953aec51c9d8642b1d71c653731d7585fdf021e045ebaa0c11", + "c4472b40a9ec2b01de30d79bafcff2cd4853edb77724a5ae02f4387597deaf53", + "de59785aaed498deb953dc239d06847baed05797fe3dd2420a5c6f35a4ddaa1b", + "9622558b5b744223868445c32fd92ff2ba0d9c56f5b34bbf9de7f3e26206a277", + "80887a104ef050b442514ff8292132f050bfda9b640a840c3258f01893e75f42", + "cbfb1b0ad1df2f84e6ddfa190362225f9306eb62a9f5541cc5deb5da781fa213", + "b01734822f1598f9edceacfe3d828203c629313c7ea0f51111f6b10474e070f9", + "a12cab7ec7e781e90a49f837e6efab253d896dab615aba3f6327b264df5b91be", + "ff54a2a3cae5ac505347c5577e010f713411b5401bff7491cebc18b34592683c", + "99529fea4b50b9b8bd91aee8f8a25cf88dd3612ede12610acb24e73a1fda35ae", + "679c55a753495d26c5ff8ee7e9dd2f6329fe76bc3bba1324f27ff5f3ecb7f8c5", + "c805e4ae5d2358af561cce2ab0ae13a83b68a80886fd11ce5e8200b0194a0085", + "5fbec47c66e62ecc3c483c440f0e39711f414dcc8c0ad72b17325bedb8bce577", + "c8e43b97b4aae188ac288e16f856f16e839e67ee149d8bc8f8bb6aa05a186704", + "ff4748fce9a654b3876905aef033f19030938880da461ae9e05bfe84698658ff", + "c6e39da9593ab92827ce0e81d29fd85003d110bb2e85d530769eaeb0b3fcb122", + "0a1cd405a53aeeb8c03e69eda64aca2dc6df3d599c41551e0e71067c2c3299d2", + "41a3b0883541b8e28e321b9cf326c99f07fe0bf8354e080539511dc27d382728", + "47f3ee76ee3a53b8b6c84de48dbfe40d4487c0f1e024be7ad735b4cb01b240e3", + "4605541e0a8b64ebfb694d092456455e366312df0fdb6edb975d5df6738d5f64", + "f140f701e73403c39925a8ec3bf6fc033df1de9c5d013a0b866b71808f77c3b2", + "0a9f71c5ea3656600032d72147b1fb795f1ec515eb0c0e9b142a51b63ccdd236", + "773a797fa4bb49ec882c2c972b71e509bcc094b89083340df1ebdd64a68f6045", + "bd004ac4dc73dd5304e33abe88de5b3889f233d1b27327b14814f2941f47131e", + "5c2b636057904e70179efc5366acf56b238780b1bd50d5733f3cfd06e4a894f8", + "30cd462a2b16211e365728808b2dea5591fdae5f8e7f816300440e255839d9aa", + "388cd1a37c37a2e7fc75da6363e894618c7e011f6db275cf64011312f3d377ed", + "f88beb132412107250f73babe32b820fe6b277b2cf21035bb4c36e7b60a9585a", + "c38eb4323358d7ae5f9aa8d459dee525427a9a490650246a2e8104d1346599dd", + "212947c08a738977407b769756f1f4d192eca956b07413bb3f702402aeff1307", + "c6bb090f1ea6e4350d6e71cba3c096adca356ce16d997f48dd081eaeeac50267", + "2da0a61f2510fe2fbafb40b27ba5aa8d0fac8254d7d69e475fdff6f3777b4d6a", + "785fc19dee1f3e84307dc836550f5ace30b672460267751e6fcd5e33ac46c296", + "370020c80d7e1f3f53566b5f51931a1297fa1a4efdc81ccf7c5ab72e2577ae40", + "259bc6129f4c136073173a83e0d5d5954203beb997ce140d8f69d7b950e0187c", + "11cb148948a9c92a2c962f6f2da0bc9a024ce63c12e62aab3f0693847c23ad0e", + "a09df031dc5d362e71b730b91a0e84677353406309aa06fce96f83eb77671b4c", + "046bd72f9bedc26c0ffa2bb7bb9679b7b1afc7c9cc72a0de2da2fd9e9f08837e", + "9ee8b7df8898711a797fd8816a32c181e641ae77f9d04f69fbccdcf5916cb668", + "98813c8870bd227cb607e97a296195e673829e66f6c18434e852f36719fe823e", + "5ff5c13d156c6c20c0018848eb761cc24a1739b1a2964e279e1841d587ce3441", + "11caa777f57c824055e6cfec2281deeccea4cc6a9a0cd971ec1b9ea7de49cbca", + "3a8e126522f84bb01239c8d1c51bada77fb0e3e76d51ca82245cbed04dcb7739", + "860a27fef1b6b537a6be688753599f84f29a2600f462bddb5180f0575cf10bf7", + "0bfb8295a95da4b08f084135ce5b221f4e5ea4cb7e63d96410e1b0161f0eb5ec", + "684b917015396fe12a0b84845c805f3e07534070e1a0410bd6319f40704fe392", + "5809b9f126cc43329050dec45c6ec7215c588ae85a0647a23816a73e2eee7e8b", + "21ce0014bae97d4599230abcf322b031d49fee0f48e578f173b317478f0098a2", + "79192c7dec85ace4a9cbb6daa82e76105d10e988ffabf1050a0b27e1eddc706b", + "9a4545f77055b0320345d7f147adae0552191e0b77ba53fcd82749b5ef45e334", + "e154afdc575fcfec7acc74e634eb767a87354b7e4c9277d6218174919f21fd7e", + "12c94ad4e5fccd6a8a24229298b1aee2efa0f231fb209aaa4581a5ae970e4636", + "cd089347c67b34ab0d80db569233b6f42792d9fbb08eddb769c5d2c06c03b78f", + "c95de3ac861cb2435096b17b6a6e96880331ccfe730b15fb14771ce6414fbf36", + "b355cc801fd8712315d0247517eb2ff1ffcf90ce66e786f66c0b70b05c3aac13", + "ead6b1e3c8ce5380567394714a8d3e71b739a128872a4310fd52ccc0cd015bca", + "b2aa10099a3d806f3b5fefc4cf5aade2c91e518a93a03eff4b4bfed37aa98bc1", + "ba727d2be80bdb3b47b860b7588a46ac93984db5de47ee999ad946aedc411a3b", + "5de5016468e7f8b981bd5f1b04b270eb7c5ba3665481b9f2215607137c5d6ecb", + "44a38331d4f01c48fd89cb306be7d40910f61e85c01efa64be56007e168eb704", + "876407f1f2289e2212c3f09fbdddd40e4d99331e618fe1fe76e54c0007146cca", + "3c12bc0d8d061735b0b03371c4869f9ebb423eb84ba6a80bdddf863601287190", + "35426258f74c650aac3cacfe5294871a989306eec1f93c85f34648d3e1e64b57", + "24b0f6118578f557278af2deaea6bcff73b7c1fc2c034f0529b804b5a3667f7c", + "a853095e6abb3da36dab40eba98107044c9bd706a59569c27df4956af6a2867a", + "7bc75c278a522c3f2fe32b83c4e5ac1f002b970424ea9534ecb70519e2ffe1f5", + "968a9fabed55c4ffe4df25e7aa61da56e67bb39f407d6c138d8949f0ebd2d654", + "c01b876bfce242254ed98ebe8999d5bce9d6aa8a0afcd643f192c9c58ba05697", + "0f25ddf33af868397a40586b9b29c759b5f48c5d5ecffd65c8025eae40cd455c", + "9b2aa176b8978b1a6ed7c1a7ea238fa59414085797fcc91a2f0bcd4ba2a56b17", + "454807437182e5bd112654fa4dd079fa0ec3d701f61075d1671c6300e77cc3a2", + "dfb43a5c2c31241a80a316403612fc16007a8bb04652eb3316bbcf1a0f8783ef", + "f0c2b72b9e3f3e0dda04e2c94ad826890cb8fa891b519b63a1646fb00690772b", + "8fc8c2fb737c13eee55751ce1dc798fa291d428858e1cd862eabd89507dd0f65", + "a84481a8966b29dcade6a3e58692ad8868a0b729eeeffa1e25cfc95f7b3912db", + "91c8e0ebbee601828719aa29d7664b5d94e3501f3778999c60a975ea74bb00f8", + "86862070933c5b4cbe7d1b29e15c728766079109dada7f51ab02b03796ffea61", + "2aca32ca5da86a14878e221dd132cf11e9f858a1694eb66c29cd245ed49a81c4", + "621a41b83f8e9299fa89f2f5245ccbe34ddf352e1270c6ef094161708f86edcf", + "23984583b0ab9321f11a5c054ca891c1b5940ca0ee0174a701ed91d8f7c52d33", + "69dc06a85efe0e51884bb0e8215d50ea2d2cd59ff9708079e90434bb2412b217", + "96bd0d2f1ff8360f4846e79d20a31b6eab3705f52fd737741bda0568e629acd0", + "7df4dd373b3fac18a0ec775c111b47f96484f3270a6d5d9b1455d4337c031b62", + "65b585500d0e4f021bf4f625c7f48bfb02d32e9e532ec9cbc0c264247f9c9446", + "28e2b11230c728131c2312f86434b934b1e34e985c1601fa17740b7820775fc1", + "f1eec6722e3a8a3626a3b9cb45ce9c80fd08bb7e71c38d7b637aa986c6206273", + "cd5c00f5aa138760efd51fabfba83c0964c47eaa2e54f95b952b437ed66a5a80", + "16557e57e3b29f120e52c5d9c08f07f93340bc79910a29953df8a4fcda6b745e", + "683fca73c707ae662b516a2c0e2793e6a764b990af2f2ce8c3151ea75e9379af", + "b77d4a94dbc5ce109210288aa0cd3edc83f671ae9a9172495e705ea902ba90e7", + "5506fe1e6126cabdbfdeb4cb59bf30bf21edc868046b263d692345649b3fd6c2", + "e94b83fb44f81d60f82a9844edb2559563ffc876bc584f4fc4e11049237ac60e", + "395cc58f6dc01018cd334bf6ac29b8f5ec3385ec325470b408bc2e8d9af11755", + "3463442e45c1c1df952e9760f91b99ea8a0fb604b203c27f03a955fea0381ceb", + "41f4c041586f3e7468fa44de1ea745e3d41b9942dc19cf35b5565978449a78dd", + "31b31a285469f516015a5045dd8434642b2f55f19373b9e0d5530e843deeccf7", + "943b282f0be57bc8c9b5ad4f47aadfc0a439af58522a4495b7cb36e0ad43a6ca", + "f1d7d6d9dc97dd43e3b76bc13c6973e31572fa1792335ce0387ca171236838b5", + "01cc0fadd0cbb9e8aaa0dd49291e39f3f325030943c8041178738bfb662a716a", + "d96ec821e29cbeeb8b9f25f7123c8d5d708159daf8acd12e7a800c72521a4efa", + "b5395de909f1cade846176880210f4a560ccd3180f8b70d2d4bf15325f196ddb", + "e447d8ecd54443d4674c84cc84763a18b9161c76399eadaa82dd74cf31535064", + "8f47c23e998213388a6abc7d3b5754336bd0eacf4eb545b0825abade1ae48feb", + "b2b553a0af737c3912207071469517222d3d3704c7c45bed01f3cae1b50c54d4", + "ebd2711b2330377b5d7fd2f39a720b896a7eef7d7f0ac2a1d0739002a1511a00", + "d62a3142e4c4a2c87e8014012b804805dfa6f7d5da1a270f7c3401479a3b2f51", + "0142c36fee839b588918bdadaf8c737f91d61b920dfc581e05335f6f2cbc3310", + "e729c42028460ee33d01a15c2e369048bb79f8ddfd70a22dccd43b1c58d41933", + "6fd227ca8a63cd5aae1cb590c6d3baccc3518306e22db68095de25f0ccb4b49f", + "80ece14b164adb418170ba9feaa4d352cfc6cb1e31e47b3239163d2cdb54eaf8", + "04205ac29beea2aad32008fe47c9acc02e3ebc9f66c16db685c8f0976f3562aa", + "071d3e6a4659c37fec52601fe99ccb4d2a2480f3a4a7bcd16f12ff21fe84a666", + "57f2fce9dc52bccdec3de4763a02e3700d13953003190d560a8ab306b81c1221", + "076c6ea3b84d7f2535d034e5371378e9345dee62947ffe4de3185d02a2152340", + "0e386ca6f2161a1c64a4072ce7a507e1131edbd90446723d84a41dcddaed4324", + "48042f945a825bc8cea4e8fef1da5c85b252054137adb5e123dbefbcc189db93", + "410d27dddbea610ff9c4e6354b187fe8a2f7c5b19c5563e2a1bca1a4b4b8c6f1", + "453ce5c10b3de7a4fd1b0bd5f96512d05fba41117299d588a4740585fc295590", + "677e34874e3dde27ba4a987778de7b1a64b6c63051f3cfc774ebd4c3fc605b7a", + "c9bfc40e1be4befc1b44e38b471167667f6b12457d5c82662ef3a0a0e9f66e2e", + "4cd169e13f4aca41b158924d9f1b972aa4d167d6d9c116e7094a7a06d96e6387", + "aad5a84ac08dcbcd06b4fd5a0c5d2c1af8eb0d6568f17b3d8cc47d064b72a3de", + "4d495f70907d93c7c1142990927f1a3d59e3218a34db881714250ea6e9aea84b", + "8c43d66f0d23b88c17d19aa01c64ab1d2af7c43704952326129b0dab4453cfcc", + "c12581dc707a3a6da47886aca1c2aabb5babcc0e0fb21808c27c6d5a939841db", + "2c63aef84a0c812b53e37656cc6d5e8f64f8e4f8552e4e502dbb1aa0319e13c1", + "86886a41f2639a791085b6a2be4f2ac4f76d951880eff5cafc722785ab4d64f7", + "73bb646c6363f96e1ecc19f5ddf7df417327b9089c1b70a867f799af03cb3d66", + "ebe518fccbdf53bfed56fd1f8f3b114b5849a8fb6e14ad7bf3f2a2baf29775c2", + "db82684a4b314c554fa6139c4c7ede0245f68b32a885c819efee12ab0c027414", + "5a985ff0be58aa5a46a0f4122cb70c7ebb937f7802a33a32d8262e63b3771796", + "938ffa6e22fccdc71d6ca1544cece54518b9fb75cac5736a0468420cb9f504b3", + "4726ef072888e9d5ad65342790b285c7ff024d60b084c32377eae6faf67db536", + "57292f406904048855148e3dc04cdb08ce05a58f174867de688b252e448096f0", + "ef8e25e005dda6d6cac7947006e1f688c743efacb7633c34ebbed7f87707a3d7", + "b90e5ada245515dc90c2fec028ce8359708a610927c3f9cb92bd93eff2fb59de", + "32ded7b174c9528307b8042692bd00480d31db9683c9476a4f50085aa6810893", + "532a40b49048d2695ab6ce15572377ea0befb57bf6311e5bc89a3b579561837f", + "587782fd3913fdd01f9f475f25e0704ab834d89fac46a0be1d88e54de6b50701", + "b91389a5cf8a1c10e890ee4c740fe9994d48e999b59a2c6320397a389ec7a316", + "6e737036b6c0c9d72a72c8545cc835d4cf7da70c5ea33f6c6003fb844565354f", + "2f239f75caba3909f2b6f059a782c2624a71b591a7e132ef72b4add3c9c2d738", + "95d4dd64bcc89d80efe84c9fb5d4a17ac7115d1b38a0ac0262e7687905eb1c4f", + "4487d612260e1a6c676d2967cedd84cc5adee2ac845ab2900aa193a1b86fade2", + "97edff35d1872b6a95f43606f084de35aef0292cd411541243bf7336a905a938", + "9001c5d627af968d68f12200662585921aab14619935c8be4775c457e2a40867", + "ba959223c1636c7b0d10b43b78c1821e7af6f6aee1f80337c555a470867c5ad8", + "9f8b4937db8172a5e01b31310cb3b74824910f9c52c356c17035d70173794089", + "1750c07b09dfc52f0cb0f09f4560095db135127cab4c511c525f560c42025abd", + "a51ee98093bf11eb0455cf106b3a4e027135e870e3e5fb212c42d62e190f153f", + "35501a82a47804cf7718d6bd2e4bb6fd67d7639984eade0c1232bc2c74588343", + "9c3192c2435a9cf815b717ecbc6c539cac03c7f582dbda889b39485895f3e174", + "ddf04cdfd5366d08b5886ef736dbf547a403f583cdd7c089f9f3c4239e554210", + "2675b30bcda3da6a2cb6837e614ada858c1bddbd42e6c3963eb220c290fbf967", + "dbd9134ad3f09406a76eba91323ab3ad2a4fc0bff4975b2cc480bd4403ac7560", + "2929520c43b14ce24a94bf56870520ddfe4d29bbcba0d0e1c4624292d8719832", + "f71513cd36ec057435881587013b2fea181cb57df1cd9798e9b4bc2b0457e778", + "b83971a9cbba7903239a1d2a5907351c61028b952d4dd207dd49f6fcc9abc13c", + "c3b7e679bd4d0dab1a2e7938b0d5cd8c28200c5da9534bda97ea0f1ea81c4aaf", + "77ccaeb09a35c5a1df5bece681062f6ce9fea08bbc2c2075ae235946e8e1d9d4", + "51001f07d1462cd9312b077be915dfd5225b85c3ba760e2a414275aba5fee655", + "ab0f6f8f5e58803093fff168c38de2710c2aff8b47661eaa3ae2a337c0e71ecf", + "96e9ce518bea74432b7aa4c857b2e2423a4b3f48275fc4b48f87caec87800b87", + "60c0b7a0ccbe7792b9332ad6072636d9790ff736e2d83a12f50e7dfec65a2680", + "38175abae6191a78d957e3c63121036688cb6b8c46d42d026551ad37a6aeccb1", + "48d82a8ce43a65f567584bfe7fb28cf8c84bc725c73413b8768d2e6688ae77f9", + "18621d94f2733bf9289e0353f066c53d95ef8dde449b2e160e28232caf17df74", + "e86c65449e6b871ac6886526107b8d7bfaca468ef270172576bd9f39fd208aad", + "c32bde208f9087259fab6b053db25b1b136b38276942e2420482855a5d7ad17a", + "dfb55a5293f934d40ff7c0a6749aad57806e571df185f592044e69e680e995a2", + "8c2a1c5eea4c8dad313bbc6beae0c4d94828fcb2a4877b19761e75c74fc10164", + "eed8a70bc0d94fdd32629373b50eb9ef830282b42dcb1d8b96fdb9b8a1797df9", + "2c4735a60cfae0273c4f309be579a51c6ebbed0200715a826048b1ee568d4934", + "f7e56418539529401d0780879ad1266aee0f0354da07486ceab82011a069deef", + "aca86fb3d7d9fb64e920ee5ddccfa78227a0a3a3e40bb3b275f114186e488c78", + "bd4b2c02f07305164010353383415b08f570fbf3fb297df74a38d6f6e3ae99ab", + "589908e264b0c91b1358f1a51f020daf473b7e4d5faf493b3625782866b476ff", + "70360b520b95938174c5b0c2400da3b047743e60d837bf567767732d748e991a", + "a54ed87456ed361b714da5c56fae7c4cfd92f8ac4a6e8e901a565241a39569dc", + "9df7afdf237605066f98cf95611f49f5c5f383a080c86dc3753390c5627efca1", + "6281424793e144590ddb68a85972c45272a5ca39227f9d24d0bfe78e2bb82580", + "13657e27b82c768942fab407deeb645ecfd958f506a6e02c95d119620cac86ec", + "cc67906e35cda000740c83f50b24b92bd04c58f2a0c45580e9a134c284580804", + "44e92cefa2c869560d185d18baafa780bf16f148d1a7ea186f05597b18783606", + "8ae261dd18679d8e203e7b4c8dd241a51634a84f83855b559f7ba55728d9c9de", + "0269bb4c3079709fb80f3aa451a895b639e3b7db83360a14f078f26816979dae", + "2faf079e75b3e9e0298fb15ef8c6d53715cba427c8c551e98d44a3c46a67414e", + "745a15e40bf06483cf58188b9c7df99e796754dc4bf3eb0547b11fd8016c5c66", + "70dbd06038f269ead55d560b7f960d24c7cb3a9c8864fe68c0daebf315cfcc9e", + "1dda0994af0142dc5f71646735ed3bbdb4f6eb40955836db16fc7524eff22ed7", + "492af127ff75fa55f94ed919386917778ed9c51e4cd193b75d82524c186a1b5d", + "5031aec77ba13f73b21e5cf92708f44c0edfd1081c89b4630a0ee3a50b91794b", + "859b7197bd1a5d6b8dbca9d6c96321e706708cba23866d44bf3f014deba6a4bf", + "3108f998d14696b3e945569ca9b47346b26f8a3700cdd444f545fd0a03968891", + "b7cde7af47f0df23419ccba57da19a5ff7645fa160e136d43c827f41b460decc", + "18cb7236904f9784d611eb67b53daa31efac1a8a9a7d2ff47dc34255ba276a57", + "1670703aaef6d8c722bdacacc31a80dcbac0326dcee64becba1339bcf630e330", + "6ae67bdc91167c7efd005be6881b58d24abde9f8c3a71e6aecdb8e6738f3d169", + "3efdadf9ad79dd00eeee1d80e40a8297254f249a9dde4b4d94b656d58338fc5f", + "d14ad9887a66067658afeb25a64866bf90debccb49cb8213184b807e9f1284aa", + "7f2bb055f5dd834bedc5ba9d63b3c83322e4325de7866b2af7ff8692bb2b52ec", + "729e0ab27d04bf1c14dc28c42ea56cd7fc1a8207ef207ac34e676577fc32006e", + "3aea585e172df036a10a0a3c26ebf4705972eb3efb5b61602231b8a2b2942b19", + "a3b01753e934a16eea44aed4a756b7582aa1b469ac462ccfe4c92268705591b4", + "d5af668caeda6f6af27a778359ce9196ba091cc04d9f9b3928f8f5203fc3fc88", + "a6319eb545257cd4c849e2cca4c6faabea9490eafa3723434800331f222b86fb", + "a4f001d05640267e07caac4cad7122771451f3f997caee435965cbd19e64a621", + "f852577eacf926a5db6921da93b4a78f00f57e06fc064ac02633976e35d9e00a", + "c48da9db46193fcda552476b8c08fe35a73ff1bc1950a41f3b1ec098d2f75a47", + "7b26f8d832edc5d6c025be9189216766b58ab27f59e7b253d0ece4d28f51e2d8", + "cc5b9121f74988f1bafbe4ebdb7296d2255a4af6f01f6967c7391568c32377ce", + "29fe88933391cedc886475d8aa7b68415a7f3e80e153d8837cacbaec3f47d8c1", + "392cf2ab942625fad754d738f33732d3172f4213a8606368330f72373ec99c64", + "b9ca5c8d1d64334e586ba1f0303d3fe802a03eca9c2aa12d2f57e79836d0b64a", + "c47d0f78a14f36ea4a9d1fdca74596a88f93dd9a0e746d97c91a79762420390e", + "74a1f274dda992663e03a2a10f29c84d41e2e202bf56e658c1a42ebed46db202", + "dfa9dcc132701d8b1f4332e2a6bc7faae3c7cf11ac0909a83c4c7e30fcbe7dcb", + "35d4395190d570f8288d255533eca2fd7bb13e720d6e8f5a7113094f97538c5f", + "70a16244e7480d092b617242786deee67056d07cb15365fe51d0e8ab1e8932a1", + "a26843f48f4f2a218b8f5b219147641ca236a1abd96905d8534350e570b164e2", + "5d9eed350175b4d1217b25e671c2f3c9868c8732b6dfa80e62e4a9bc7010dcf6", + "72db3913aa066e04662cc170f3363578fa47799b64561d3b9e84598b43ee8c26", + "e406db4deb9cc79c8eb7d658c382c2291668d7917d73c652c415f37a3f713b0f", + "a0faece933213756d22239d1a066c1730fe58b3e815df52c2de1fca102b03caf", + "9d6bc10478d9345260c53c40de93ccd7b1f344dc25e6e4cc8199e1ed9c6dc824", + "ff1a79b9d954943147a0b29d67ce6fcae1f9f8d5476c2b8eaf78ac575f95247c", + "ba31c30e4a28d5ad22a364042b7155e5f4ee008929f22eceb3fb7371d0c1c2a0", + "797bb553ef79e09eb1aa5400a889bcaa513583662c3708614e29b202cfc3b80d", + "3df07ae6890b2c332556c247ee91abf341c5dce226e38df09f7010b1c21615f6", + "30de2051725d9f0fc371cfa55aaded73b12e0668b1624775b657ae0f41ed8daf", + "035e448afdafd5069a783abcd6e268a82f43a52d9ea805c1141d329557d2029f", + "1cd59a8ac8c61f8f97cfcd1e43d633f41f0b3b3782e3deb3d0a9d80942b52077", + "b1bd938d8288167f4a985abf342f977cfd1c2f1da70c6037c9bdc4295a705726", + "418297a2f0c0e5c3d6b2c4d80824e1a909a61fce6c18bc937720eecb1ce3682f", + "06031df90b06aa84d58da9de78808f7b2cd7d563f41dbed683e74bcd7884720d", + "28961517d508b8e963f98272dcbe636af5f99e6fbf436d14b9b64273b17857a0", + "bac113e3e3572e90dad38b0c2b32deb78f9027e59b75db03d0fb86c49c2d6d9d", + "26a5e2f817b0596d95c8a82cbbfeec6971da2247ce1cfa52bc5098ebdfd3c084", + "ba7bae49a577e15e14568cec5f779c4700c8ecbd21bc8cfbd3aa56eb0948dd27", + "90bdd504836627c40d4c14d54da1b3dc5cf5b6b1cacfe2e220d1aa44850f6fba", + "02f1553ab9b4f4486fbee6f5735ffe0d9b305feae9a120df2ea311fde4cc6076", + "2a9ec46cca5184106d4df225de39422136c6cc995fb9d124bdd640ea45667a13", + "28a25bb686696fcfcdeed1a996ff4f8956dd4c41587d599eb78a83ab6b1358c8", + "422bb78c5107f8f0a5e1a253347a72894704cc4d8d1f60a49a49f8c5dbaaf298", + "74f3c43fcc95c26354129e18241a1e747c78975c4bfbb0b90a2b67c117dd85b5", + "8c0f5a2aeb81ef4e5805c1070bfb8667ae185b59705c3cab2aac50363327daed", + "87deb9c6b62b237a5f83b7333776baeebb4eaf804ef78b9ca2c62ab92388a697", + "42897cf080c2e405a77fd501360a7a86d0a1cb7d573f5fb07e094bb13a942f03", + "0d3b80df67851ee640b906d761fd95ad7e47d0186cdcddc59a10a22e011816ed", + "dd222577e63a176e688a09834ba7dfbc7d25af9cecda7b130e48a25fec558e2c", + "f6f7bd7315404bee4a9611e69fe5d3387de5e4577ac28267d04d69946f42f18c", + "03cce690c88d34689a3fa66b6f8c9da75747ed568e63fb20715def88e7228b38", + "d9a4eced4646f53c7840f817b4296b5bf16e7b3d5e906953b6302d7b8a848dd9", + "319741447aa9b6c819cab13368125a52f3c35d264249d5b4068d2f379010f4d6", + "0a0f3eab30a538a23ac52e4f251d383d418bccc96d5975446e093cb424e8c247", + "550942f02799dc998561d8695fdafc4554c5a89f12333403cfd7c4c9cb15bbec", + "1b0d7536d6774149b4ed8db577268f176e4c1f76e88c9f956d51577d7349cfa6", + "5d27bef5a988fb678c4e178b4b1063c4151c5d3780c1dff1ec2ca898f8f8cb5a", + "6bb4b71ce2a72c465e83586c39a2c1d2f77a47ecfca14b45af365a4ec29448ca", + "938e0c409a837260a9fc27ad5878f3b8936ca0e6219ecd3f9bd4bef6c402acd2", + "d8aea1404c3d5fca7289e2512982bda873437b43e7b8b2a912f383340f821c8f", + "1f4cd386950035ca3cec81cba5d6a506620863056da90c7dc9a5afe47d0d9dd1", + "dac066c881d458341c7db30fde7d9197e498ee577bb4f0e0e48fb859f3790143", + "09381cdc27896ab6c73c341673a61927099eaf2d300d3e00ea37ea6cecb262b8", + "65f65598a48e7b2e38efa51d454ef1f26be016c6a70181e26bf35921998671ea", + "98d991cab4e708f9281d1f56b1a507026a114ab0733ec2dd572bbeaa22fa4887", + "1c2a4e2bf0dc9a09e511baff42c297b321cb8da45c18ec50e4c39ca2465d7cd5", + "327c7a6d05d522536e61aa1fbe8fa19f781f915e20a66a647ea90abed8879fb4", + "cb46cc08d32b5bc7ae9480f30884c7670b8e8694d66d5438e5f8fe81785dcd1c", + "537fa4c41690903a48b7a632249144f154c5dc4686f51868df0159a0350a4ea5", + "75a5e4322582d7e240de8e8b0ab9ad73b7e283428537338b41b35107dd63ea2c", + "1f87054001c06863b66f55b3a897ac0d76f361908900b30e1d8ffa46742bfd16", + "22197f4735d51aa39ef48c88c4d60818245fca73b98df8b9669dc434c6ac8796", + "a2e14dc37362b89a5b85c57b3b48169adb2e6dd5360b8030de7a0e028a6db3b2", + "676f7706eb7a57397f6c9f69de04cb037d3a1045281b8ad8390244b9afc7a2d1", + "8defc187fb1f0d7bfea657619f9a020f303f0cede255b473d18817b500b94b75", + "be1166434669922a764541789705f9d86af0587241cf3da9e33a7983d048177b", + "c5adb9840c2cba41c6e49f9e97c3b78d8d0a1c4af88929e89af205f75f61cebe", + "a83b4a5b3282ab0a2dfa4b1620dd0e3ecefd89cd43fcee45a1a6e4760453e9a8", + "ae2117b3fadabe7e3d94153da7e68c6d7b371efc20373aa8e1d9326ed9a3b369", + "a0b5970a81570a1b2b4d058b32ba5f207273765be398986490ed5e9109d07863", + "9ea221fe02ecd3c974957ec24796afb677672fd1ca770a13ec9c3a94135ef06c", + "a8316aa74116d533973b74f528dac5bc257a3e9cfdeae39db7d8d32f38085f4e", + "efc5250bdfe42346a1d032ef4c7189cfeaf303af28a12b3be074daeea810b171", + "9aa7194fdc5f39e9ea59a13d04158913df4c87ed385b32a37f216c17abfc69c1", + "a17f4cce7a3aa86d9bd5d51f3c38f235fcc4b086a746030af939997d05e54fe5", + "98b049494b468b0c6cdb3cf382283110668d919005678e4e8a5550a892ce133b", + "1ff264905c3f040c3fcaa107b7df3c38ec0dc7d59ddf9ff8780d6aff0381da57", + "ef6077b9e143f01f8b500e2eb6b6ff02043ec00befeb78ee44db5f00d037bbf9", + "ca1af382997fd751c70fa1fea1cdee6ee1d2a22246463ed7867d56e57619aaa5", + "be8045324dcbd46d31a1b24be314d5f0ed7bd1d1ca67bf71ba5cc43043d5c9e3", + "70455e4b87d4b01585564a77a6a414a53338c05a984de6dcd14d7126306e22af", + "5bb2bfe0be18cf76e3f70ab3c6ef2816dd988074d43389a3c4479703d1a61041", + "3c95b917d6409a12360186d7c8839b33d6fb9a7f66f2e0e3a887601927d14043", + "0259cb83d1129316ee620361341d083c43690ae542e5fec01f887acaf2fb04b9", + "9aa9b67c720b3b2fe3eff8732fdc359ab15ed4fe1092e289fc27b7e55a9c5b50", + "1151155d7be1017544c0c2b7432ea1f67cf3191baee64771e9364f3bd1da3708", + "9c45b3560ce8419825ae84affed5223683fcccfd6e35247d1c03f4b988e8f48d", + "a3242aa0a01b82c6d5faf748c22a616b950ecd30ef0f72685aa5860f9320761c", + "0f07485d78018a506a69021080b41861f801ddad09666090e53fff3bff0cfa92", + "e819c861150099438a5ef2613bbc8e4c854cb8410521e9699fe90500a9127443", + "4e78cd42978462aaedb993b6a49a5ba1315519db360522a26add6a17c8adac8e", + "b21ef6dd5976ca60b6e335e78e479a655409da184e68c10e0b4010ff9d208c64", + "e8bd4c3f3e0a8345ae5b2b5748060b15ef0c7529fc3eff080bec1a9a77584592", + "3c8d2c54c7d5d826498d730a289d36f903720b7e2ce1f699ccfa152999b5a0fe", + "fb6e025eec367e2bf4b44af67ea674d767b77397d0568528e7fe3aa7574ae730", + "9236e92010421a2c1674eb0a8044ac2b215b605c0b94b903f3a2872d0fabd344", + "9ea25f2f9472292aba590e298fc5ff15c4cb22608b2f0f2ce1bf26e148920007", + "d4d18a3b2db8540a7bf72e0cb14b176919f995e70f0df607489d5adeedb0a932", + "e6f6e272b36731484782bcd4844087cf45678a05d8b84819f9d41f5376f0f8ca", + "5088a2b878336dd33c3a7c55c09aa70bfb1d34815f031d4b3e92402c7633b400", + "3d12743c540c798ec2587301a3fb2b4f0f6e1278e4bb15280c247272ffed0651", + "52c17f712792472ca80df50f7b9f96f51a6eb155c872ba9ef460aa6d4e10fe39", + "b97f17e90b9fc70044f8789379639e52f97707eac2863a35e9a19376555f52dc", + "19701f39368b26bd3c71a2f0630b5e89586f7249431843eb6c890045f7789280", + "49ea123cea990f6fb2029e8cbc9ff7ba7e049c32db1f6519306ab980d74d0d38", + "fd574068f0c0df88faf7b54341a310446c0ec94931bc459748e11c58435d6ea4", + "fdd6cc2c790b22471f03a32b906635b4028debfd691a0a57f847e15061a2904c", + "b7a173017905bf55dfe03c1640a4e808dd6a1d6bdaeb27722a7c839766b8465e", + "74b799aae53dafe2131b183068d8cf5fa8f2a3006c9bb9944aa493c7491b94cc", + "cef9c06a87ada326bb03e6a301f53fe5fe6d68df015b4d72f1965581545c7fd8", + "db390feffd9cdd7f8a34acafa77d80889a820c9275095e84b439ff5f05781ceb", + "57e17c1b71a226c7bf02687565e92689e5c966f5c864e64d9a621bb6757f4f2f", + "b8c8dc2624b8aa73874647bb89be2765fda8223fa7055796a59c7f4d1918e8ff", + "08a2553e3004a255c9a4135930bd9f8724423747135c0dfe45747285549e4ea2", + "4d4966d172bd8c367d828eb8a14f853999df1394fda96fdcdf5e41d70725ba21", + "23888052d915017c4b47b5307d25158fd13daa02bef1d4a2c11bfef79a851470", + "52a97f2b532139784f4a6a6250b2e61a3989a2c94534c2434041aefa13af2871", + "73c82b469fce6ce8b6b2828441bd31b66784b0994e57b82d331be9e03cdccca8", + "cdbb2bec43b864ec0d7caf5def1222c3cbe20c79d97c7642350dab42b5d1af3e", + "e047c075c048467400806f61489cc503dfcdffc73d4b9b6bfa470427f95127f2", + "82de0f386c63555542e8a34ece6dd42f2a4671d437e00aca7da175e7d18bbc85", + "fee469874ca3fb72f0871a24f410284c04311446f4c5f04480d7b3ceb4e78efa", + "830d8aa08ae29fa0941efc576cb59975d035139cc27213d0a04a0153e45207d4", + "f444da313f6e7bd9e27396f9a95129c83977e1462f86e7a9b5addb1a7ff30580", + "fa601478a412743bdf29bcf44ecd5fdedc09792c8e469a7399bc17c64e134585", + "cec5ef97aa6a39f3c146c5e3f955bc9028798591d4e38094f38f70e981566d58", + "6bbf46cb35bc96854a8fae0e20935f2ed32ab5a940e7891fa7f268d28ce5d549", + "955d9741503c2e5244f513e2c2430bf814201a2d7dc74e2e5c9c9aeed401ebc5", + "9f2e6590991e25b4fbcacbee5bf7381092cfcd7d4f3e258bf4b41b84102fc179", + "647c657c3c930537bc13047cf886869f5a0699fc52496ab30c997e5fea1b3dbd", + "9802afd3ddb4d5067fd0db7ead8c66ffb3ad1c29391ce56321c4b8f34174eeb5", + "60f782f178f30f3d9321017b2c6591f25a82f957ef1c704c6901fe2fc92ed76f", + "109992589b06b8eeba15fc2f6104140fec043b2031c8df3e9025fb094978ddaf", + "091adb2a123d0cbeae7dd399a39f4ca4a3b68086410e6c7aef0526bb2ad2fb82", + "64143c36d10d8d0659f9431dcf30cec804f4487452d5376639157354bb7ee637", + "9602ec038d85b2cc617dbdbbff0a3293ec62048f70b16a6b38702e26b99074a6", + "1c6b6508f0f0c772137ed1a53cb559fb1aa4b3690f6e1f6fd3ddbe5b74da7676", + "a4df5d4c393b255674bfd64aea4ae82da5175732d39591fa671a233004d2d0bf", + "928a67d956422b29189df8c6e88e345ed2cfc8ac941d06f15a7c24cf30bc5296", + "79abadbacc8e8bcf8930e8295dbf6626e3c2c71b8c4c6883318dd1c72e03ecba", + "ddfc0d445cc013734947279fdf7c6f0d474a622da65db4ab1c0c9cea7f60d0fd", + "61ce62f67c3526d4183f74ef0d457216c7ba9913c9a0243944c972cac6dbf42c", + "9cbe363f9bc69639efc73b9bc02264e29f7d0f8ea3b1f35ba16640a5767c5db2", + "d7a25bfdc1e8bfba013bbcdc8afe41d81511e15cece155e9be7bf1ce81af27c3", + "93f9632756d3b0d8bc72c4718dffaac5ef81630d6bd79364b7bbcc9ffbc302d5", + "2311829f9842404d9cc2243dd149bdb26d210031ea78dfb066e90381a392e641", + "cd08a8a153ded8be5d705390e3bf53cb56d7b8c20e0e08c39adb3689a68b9602", + "8d310a900176ade581ad42108605cc6bf867f21cc115e6abbf2a5e083c2bbae5", + "43c5cb40e94874eb80ec26b7df732cea255453f511992fd61ddbf4de739f3b35", + "a2a9abf1c835b824a9d311105a4c92b3d6ca8eb2761b45da7241f5879d05bcab", + "ec48aab645d64498938781156b6daffadc0ec9f3556b7e995c8dbd63299c3121", + "05f6c84dd8ff3468d8992d9dfcb4e31597f7a378828cc0108320c009ef16ceaf", + "d4d39f9d3724cad98bf1466577b79c4dcdca104ecb489e877fa8f44adb1b2ba2", + "b23a123b0bb6c11060658fd14786dd345ad2a2c699e4fdc7c8de79b1eba41fc1", + "f27d04707f4b1110ffd978f8610b2f094ac0f24c1dc819b59b70e1f64cd86a99", + "42895aa8dc4c0ca50b98c7157fbaafa53fc3d8a5221b660dc86837eed7e6d319", + "ea9f13a6e1cbe9c308bf85b50ddc729abe112c46d74b50ef53d0bd7ece84121d", + "e8d250858b5de70d138213342a6765948419b81b8e8ed65b3763b20ddf009fdf", + "827b1279e3ea50b1b7aeda4a7969cdf18bed45fa018e552190e8341d2b8be2a1", + "3584a32e4fa98cc72c35e0050fc00f46462b7286e9d6ad675c340a8d1e56fed2", + "196435304b5e92922c3453ad9f31a8d1b2821634ee6a29c07543b05eafd7b061", + "a86b7271d28b011a2749b302fcdbbf712f89d9f3efa24b9a169e97e67a57e594", + "d40ed1342bca6c536e736f1fa15aa5f5a01274b4208ac4690a4f8c293dd40b0c", + "513256a2ae5c50af825420724fadd88aa0a77d93f64e1425f768f33bc56bf862", + "83381a3fb9ec95f2fccc410ae48e0581ee4f492881cd0c20c90cf19823ffca7e", + "0d51b0210f527afde630887455f5d231425f8a8aab5d8567e5ccd49441f5effb", + "c584a68f8f6a5d8841d9b51fa9cff1a3aee73b1b4f8c8f378843c79ba5ce5902", + "b5396ef2974c2a1040b8e8ff023a82ed4ddbff2b69e5569249d139ec01dafaef", + "e52c0b2f5da5c4c5f68f4c1b7e3dc81e9572bcb6299d3386d54bf881c43dcec4", + "ba60878ab1ffd5e5b85d843ba049d669ec4d0094330147c3942a62f0394c32e5", + "e412b797b242a5619823b6b33008f45bb0b11bac0ff365b4f8c73c9f3fe6e590", + "50933f5388c97812aff9aa52db014d71556383c63c6b1b7ce294195c8efcb540", + "643ee4638b7d9c626522f9178bce2a53a875453252c46ee3ed4d524ad2710a4e", + "e72b8739dbff60185cfe3da8783fd9af56b7da9974610428a11adaf5c433b2c7", + "48c3cb8b5c7a90728fbc104f8b191a98119f8a2777c00f8c1000244013745527", + "34845f23b0bcf4e5e7fac5f82c059cd08d9bd7cabb11edbd62fdf1b40df95758", + "33bcac68cb4ae201b3d3fd356c62feac888ea3f7e0f59ff0dfa2fef97f1eb558", + "778d833969b28ccda7711e0f6e37345fa598ac1a9fd1c6a497dcfbb0d2a9d590", + "5fca36d19c656d347e71c9ca43df0d701852d9811b758d63b3ae6522dd4c0b68", + "74f0e0b61425240202486eb26a74e53bdf8460203352f1c1adf698be9c01dda5", + "d2b4e304a065b74641f42966a43398a96e85c14cb209ff06d18e6fdd8a30e1bd", + "df6401c07b51c72f6ea2654aa3069fb6bf0d8a3adbf00d926d534400d88733b0", + "3850acc410d21b19110d9f229c63d67e36e2484915693f21569b627b0f3395bb", + "6143ef6a8f8edfb6b40ad2314c0f9595fbed1f55fac3b8abbacd72f691da7780", + "b14ae396d6da8ee43a3eaf5e9a06333ce6515cbf1819bb5283d73a68cd39d2c2", + "2ccb1721e1ed7add382cf8838f7cd82106232c7b0bb7fbbc5f36291936fc8f52", + "cbde7da6d0dbf518da6923c7d82749def1260eb6632f03a7e3eaa62cb3a4eecf", + "9a9df0550f1629c3cc3c539ec83ca7e815ab8c9319ceb91f42b6b9dfc3384ede", + "f48737960c6bfd1b4631cd39048b55cad33bd0fa925a8ae094456466e16b7791", + "2514869fcc6ff50736da7f509dcc55cfe8372f82beb46529d757c0ad98fca10e", + "b21124fa2ab9868f3e7375cebd5cf95a48d0d62ed6fb62ef0e0f08f7337bb212", + "84835fd51e9bd4624f4b6feaf2387bf3652f0b03efd56cde086079f86bcf9cc2", + "868492cf4941bc0fd0ca8c37a30008fabf58e56bb3b41f7bd91e09b43914618d", + "00a8ba1a607be27447cfd6f60447a22ff6e603e347eaad21604bf56cfd3b1434", + "e50a1c9ab7655b55cda75ff82bc1d28ea48cc7fd292e5f7c72e0f48acee712de", + "249df64d683697b530c6013692fba966655c6d4e38ba40fd05a282f60570ee67", + "1e66d4f9bf24724b6ebeeba7f2ccce22cc039cba53b7e7fa76723f1dbb934e34", + "db6767231847109ee8ae86b459f0303e08e2fcf5b4165faf7edc33c66909735a", + "406ee377180093d25cc204105aaa3030473ab112f209ae1dd608ebe115a3f95b", + "0763bf2c13cdd4525a95f123d5a42ddcc9b3aad27c90dfac1f19fe2cc643f636", + "3cb4c58f6597a2e937c8bcc870bbef98fe4a2ef16089c94a0afcd0ecbbca140b", + "1a448036689115f5ad50e0587d114cd77901f23a07070b4d18ae816256d8811e", + "4bc718cc25242feeced0c3f07c5459d30e5ecd91011237dd869a2b1d32c390b9", + "127d4282b45be11fe780f20db6c1737192912449c0ba6b44ac499d9fcf546a48", + "2882d864f8aadec1babadf7ea312ca3774fd0202a4702779415a7296501e5828", + "2c1a8fb6d739f4ebfd01044c2ca11b484de6933d33c6d120c5bf36475ab7b457", + "d4224f53e199e315ffba0bb317dcf7743d764642d93770e8f6eff12506df888a", + "b8d8b26045f477d58aa21e5bd835883ae1a7bf47e416032080750e80ea7b008a", + "fad3f5f3c9d29a2165efdd3e33f0aae9e58365b379f0ac61f41ff8912f7e9281", + "c05aa5280e7bb5dc66265a8862932aeb268162c25ac2be8fca44949f94ec1ee7", + "af2c85d7085cbd5ff2760f744d22e1e5be0bb8db8817deaa14c06ba9e4cdf516", + "00305a938c19feb580e7945aaf55f217d5089185176561c5896d3643551c1aa9", + "8ac01d950862d0598424e3488ede8e9a285dd068c1d401759ecfa3c97f9bdc85", + "33f3b9159541a44f2198e6e7bff59160bb52f81c35c9f80433992edb5ed0a340", + "31244c0e5b1f7c934f92aefaab2e06b0c2b88c98576777d3e5c514a6b8ba97ad", + "72ed3556b3a25c229bb436b6bf809173540368bcc434c74548373c043f4f8602", + "00947670a458adcae6c83f223cc0b2b05cc1debeed514ea4011044ce375c8eab", + "123376bfe0702b06b98915e38ad3c8d3d8d270f2c81db595922c298d980a7991", + "b9ea637bee6f3f3bee40fb76dd375b6419fcce5fbab8523785d04b1c4956570f", + "601b25e3e686fcc9c3470bdbf4fd2af9d008ea1858cef028180e3f62df0fca7b", + "6d5a1265a95ee04afd7df5c9aee4341f31de35b9e87f2fc9e02d2fd7e0d5fa2d", + "77bef4f6fde9b94b5397e63d1690c6bcd553ca53b58c2bfcdf92726590246317", + "27f110882479c6cf64c0bb38d121e389d5ff17db4b216597c75151b78b24b2be", + "3d1d9cc9b494b22ab1df806d7b1e9745d132dd6d72ba4726f55af9210f3df4b7", + "84d60c1d787bc5d15a952183acad447856c0501c2bd5809145f8d8582c97decf", + "b79fa13459e7f875488cf30ccd2f0431cbe8dffc671880c10cd72d7e2be7e8cd", + "061c36fb310210ab07b22f39178b7f451d5142a395ececa1ec783af9c52b730b", + "fa16976c19becae31248c8477ff0e746c4e28ddcea5ee36408c05be86a142471", + "82235891577986898debf0cc0bab31fa765e925d01a27c27ec694103320d82a3", + "39989431d3ac81a61b0311181d499018fb4cddfb74b1afe4a9260b5841135989", + "a846acdc0cf349a8d2b352d3dfa8784893f5038c05277d327341d3ef7de41bce", + "f3cd88ff101e9dcaa9e05998db425b590dc45dadfcd8b8a6bc0ac7e932372055", + "a159443648d0796956f92ec4efc5aa44af0cc2fb7ba97de7e6a8e0dd16638530", + "9e381ca3bd9488a656e965de17a44487f8c0da8896ebf9c0af4c0146ce034951", + "4cfafb85f2faa88a88034c4e04b7a37be20eff7e02d3e21bdb79d5aa7de833df", + "eeae101eeff05cb6a0dee3bd70b7999858b898b8c0cc7fa96a5e9235d1377381", + "786ad41ea86eda551d0bfd3e702622e118b21e619c604772befe546e55d4cedc", + "2c2f151ad2b9d7bb8f52dc4b060d194bcc63848754d0d2983def1eb992f194dc", + "530b5035473e95e2bf117e3ba2d88d6de1e648f8672baa0c9dcb83e80ef47580", + "b34801289f8a80277b7d5fdb602ed3df305d9e915138761eb20cdd42b338509f", + "eff8301ccf9032fb1398c62896f8502a564889242c9cce7cfddf1b9b87dc9189", + "1babe594d5c84b3e67237b6abcf14b9a091368c587958eccedd4ab0367601a6f", + "b2e8b56a474d41acb1a53646d825bec845cb12c7b96dff588d6d1051136dae11", + "7b400ad7fa14af9182259b51e810e16029c947f2509fb129ebe111fdd977af3a", + "842d63a701a80a9799357ac704f2288ab62af3a62d476fd740c0f8f5c35a46b6", + "dd146653c3aabc3747df891c3a6685d24e9cfe4d61e52f5c7fc7df6f079d59ee", + "2564c5ffb78c4c132cf8c6dc06591661f2d7ee12004af6ec078af8aca04b42aa", + "8c01eb1dcc1e13892a782e9826288711f28ca7c716bb9756e49b41c8d9946694", + "f68743f290c5002b746d912702c1084cee82bdd3c323200dc0710381d98a41c1", + "292a6383fd16ec5afa4cffbf7a6fcc8c6553d4cbd2574886f623b00bf12b1e4b", + "4980e074baa3623551a56263d46933f5fca5465615467ea914fcf6e389e5eb6b", + "4cf461bc4b7f8e1104cb225bae1f3be5dc70ea47a3879257d6652b42ee302a6c", + "79f626c481d62c454a9a8e5d008141befb190c0f4e78c0f9796cdfd8b490c2d0", + "8b4ab8a45964bdb5ab237c0e014c840e224ab1ef50cf2afe79f8810613de8c9d", + "6daef9ecc84a1d29833c0edbfd38663b0e5b76f6ae1d5a22cb081984be8a1ba1", + "7cf7620117fda4ba2e7e1392a39c2c0467e3b90ef96ab021f90708e46df97e94", + "44f6020ce6f1831ef60cb293f456a5ed770aa92b1837fb571ecbf00963e77249", + "b53bc8f19e29660d7c2c9622fc080eb3839eee4b601df47eb3ac20aff8ef8343", + "114f8884785cb1ea54087bf3d19418129fbac44f8c496677778714743e678c72", + "640e38abc942b35d60027a7ca1671ef96da9dbda8d3aac61917189e0f1613a1f", + "23bc1dfe86d5be148155bcb7aea39b8cb68a887a07c7159a8832d7fa8af8d449", + "8ad3f2e44ac576c4a9c364fe497b840bae3a5673a29e69f0806208499332a001", + "ba11731e009801147b11770a0822e2f1e6cab3923726ed79efb3ed5675289c05", + "409e700d67c2541afa301f2c09a4a98de150ff30662f25dcce0bec82da44a1bb", + "74aebd9823146c25c615aff0cac7a2a7b43a3678fd4acdba428bc874c2de2351", + "1ab29e97945c3ead550846ae8af77e6f02d77d24fa8d6af4a866c45568055be2", + "729944aa7565a697548f162e0ec9f39a634fec17615a4b1b65c7b68d06da8ec7", + "dbff2d93d089c5fc366cb89c702f99022ccc53ceb39418e2a9921b1f9d895c95", + "170f0177e2fd6e2ad5e96bf1c7d5fe8443f824ca80c51cb19fe985020bac4dc6", + "44321c4c2e53ab3faa16443e0dd56d0bb18ad444224c09a686285528c04333fd", + "2af9827f953129f8eec520d1409be4d9747fe2213e877ccb06a82e9bfae761bb", + "65831a1f6d27ecf91d452140be9219d010695af0fcb2ef3f19764a4f6b3e07f9", + "103f4a32f26764273b7ab1abed25b768e7994a6bcfe4c029d2eb5a30c4e506ed", + "545d4fd5dcc11f7c29a6ba552562cf4d23890761c76ff86d6940dc707368a0ad", + "4ff9f091da95e9e8892bbe017a8fe29a05955c5c62992a78e23d3bfa4b3c7d15", + "8e82f5c6062204966bfff0f299b26b5921358e22e0880a5130004280c418827f", + "1a6df2e84c39918881ec151d620cc74007c0d122020f6c501dcaff8822f46891", + "01ff674c4dc5d40f0a53318e4a210f418dd2e504077f80e640360db4d2470be0", + "fb4b597769166e58a3b28a698f18ad0bd4c1e24d6c0e3cf58cb800b131f4fea5", + "dbef3658df7ce530752ca69e5248cbf2832c1c458f0b85918f2a4f3666d7b301", + "9b651ab03463dbfb3e7dff410e758430fe535d409805506018bb3f9ba98d726f", + "0795034bae75b616aa3d94df004f4b80717c593aae42e0eeea2bda508fd091c6", + "52d8998c0c3ea0a42e6271a43660f24f17e6201dcbe28a4e1374dd20758327b9", + "a04159ad9cdb943b39160af9eba203f3aeff9bbfd933843bf6cc32f40bbc1f54", + "cf250507d4d9f066221a2c5ce190da42e2c8e9afce5a539f09305af5074b759f", + "5fca2bf5cbda4284eb2a9aa26b55f54d43e28d0efb97e429482a11b66cbc7f8f", + "a740d86574c321a0d3ffec5124431c9a2e52882f85520f296bdb230778e74201", + "5543633ba06069b459f10a9174dff488575830c86bd32c1da6724847791ea055", + "8cb684f24f0a95ad5f4376aa8f0d9bdda6e413bf04a3c8821a547fdf46dd1938", + "b167590ad1040def2161bb854d08fc62b8e4ff4a2d60376c1692c87cb2008e7b", + "a00f55c70f539a98a23b51888c892f2b5a9b86328ec99cd56a11f0e5d8d4d49c", + "73dc98bc6b3862a513320e0e4cd61f5021b30170a2d054abbb62e87036f4ea08", + "ac0f7cc6fbc94a64b50565a929ed6aebd4c6c36000dd40b4f826052b60e98103", + "e24d3edf80ad414a6e5ffca904a259c6a28f1058c0488a524908d1ca8a2faa26", + "38d020a513200b0a6d7bad5fe1e87e39efc7f80b940698ba23a78bbe2cde0fda", + "ffb2bdb644990f4751b4795b94784b5e3c00a5d40afa2b110822eb7dd0cf2d63", + "40a6543d5faecc7fd797d8aaf091d5e4ddf5f075e7f4798b36b6ff2669afbfca", + "07ee87b5c221a21104881b0a6255c2354abfeca80be635803a387a3ef9032f3b", + "d3fe1eefcaa0d03434e6759f8b48fc50f4223edec547c94bf072d49427e2f418", + "0ce4d7ed2eb07f798588df1df044d9e124604f0178ca8941e189854daf64f26c", + "4e5657ffdeb3b88cee74452cd24a8d78ea33013f600d0a6e97c56eef47c36819", + "8e5a2d6f8b4d0878a438d7b33ed8bb7c32294968f71ebf45ab51ba45bc3836e3", + "0f81dcd7f649bbfdd0db6fc69ae5f5a7d89b99388e7d20312705ba07d16a4ab8", + "1cc3486005fa6151d957d0cb6e2599df2680055949b81d4dab85562622ded07d", + "78c8021936527768d5cd7e1dcc30ce55ab13ceb6a1209996a829e3cff8e7239d", + "ab1747694e6e07051b6739e3df2916e09d7cd8a135753c9136a8dbba0449e83a", + "25d6f0d435679e7d84147da65006a48a5a0f893ed2e956518bbcd6b98f40d368", + "3554c7682f565a20b336c40129b5f7182239f2b0b1169f39f8421f25b9ad52cd", + "51b263e67a4421f6b858eb3cafe303af0b3339e0b0509eb068fe3092d394f530", + "2bdcbc0df01dbdab2c91c4658bb9b160fe3efbb5dfecdd85ab525dc21033d90a", + "aa0105f9628907cdeebaa743c45a458553942c3e089d64a02bdcaef293581337", + "d4636ae50f908fb11cfa1f338314260f587f26c29c86cd682e5003fccb554efa", + "31ae9547640c8ebad28019e8eb66cf608074d5c1a95aefa065db97da4c1e1044", + "a228c94293d05874a00c3aa46ee128a41d10405c28e5bed3f7e7f548243fafd2", + "a7d5f3e4630be48f88cfe45147b94b9010718e55d6eaae6462a41aac52e866c3", + "56d4a383be9efe8694a572a143649be7bf89b1eb12842438162532827f7bf101", + "79f7177cf2655951fe0fe7c98cd1cca252e7eafc0b81066aab0841f7cd7895f5", + "abc1746768383ad59c647846b13dbdf51d50f9a5f2c50fac9f676531ed6d08a6", + "a1ad8a55e14acf37cd4eb5085405b62a53948065dcd3bbf299477d03b1d8cc6f", + "c731487e9086642d4e92ecbada3dc31b72049583d446d65bda3a09b4370cafdb", + "729c914190c753c6ab1f648ed04889baba5adc43e05102587672a7a0051b279d", + "3fcbf09cd64d4070062dd2eaf9ff595d3e1fefaea6996ab134e9f394e0814f56", + "2efc8bdc74b41138f0075e2603ec227f7265d3eb6e7ca16a7e1696aa83cc3635", + "cf05b470683e4090959a5e6222aabb1026d60907f9df98256ed0dc62103eff79", + "cae8a98b1c421ba34af46610134a71153d525636c02cccad8e0362b7109d7410", + "cbf00526ae885899747bad4c395b9f2c02e62bbf39fcee82565fecb1224bc311", + "b6ddac871381353848dbb438f0b7ef14fca4eb39fbda7b4eb1747ea454b3155a", + "375b66c16668308f1e768c09bc2dbb0def9dd04d2f90c42c49453f558220a338", + "a2387da7be7ec4a0ce7ef03cb1bd701330e5e950be120854efac13d903367248", + "21f4efbd857d838626a5b6eb935c1d8dc3a0a6c8defd23928c8018c4956663c1", + "ac96b176999b37719fe3a67231b811a7a435ee7b5e807fd1430803a29cab70ff", + "9c06f32a7f016a0e702b928f7de9a82297498400654109ec3a17a1c2531a36d0", + "fa932de504e54776e5b8fe75a2aa7067cff79865b766882542608eb31de1f3af", + "d994c1c85ab3611ad12fbbd4ee7e7afb30ff9ce3c67ca619d9786553b0383668", + "2e5789ccd625cf20f42a72de7cdc246ab92a1cd85ee619570549a8effa476ff9", + "ec4145c429fecc6b5109d5a4e1434c4418925fc65f53bda6cd06620dbd2d1b69", + "0978239a7a14bb768e1ca71e1d40e37a102091ad01a75f5f92c5da900761c79d", + "3382546aefe321d644bd3626c7b767bbf940df0cebfd3917c05f69c7973136cc", + "227e8c8045928af96e794585a87d6869c3ced7dab3689c5565cec54e168641a9", + "c0a3f1b8c27d0234fc64fd8d1ff9f6d3ca157b03033e505bd443f1b7bba62f7e", + "2e848b33465df0fa0a2b75d23c4df4f02824f431e78e80c0221ff5aa61348a91", + "14656b6da232af334beffd02ffb86992637993ce4aa530acf8bbc2081c5dd2eb", + "1697567eea595c835310638672cc7ff2067a568198731128280bf4167b2e45ee", + "749a784a544085ed3fe56e3ce9643d0e13aab8f3480f2fdfe7c16e15ef08d0b3", + "0b2b27579b615e086959fb35f7741791615e25c611f8ab7851c83bdfeda7d7c9", + "e444de3a6fc90b1d2626987fe98c956c1e0db2008cdbe6d863ad410e9fe0a8a3", + "89a757ea85e9d24b3e584538cded1e5db3a90bb01066af609d3401d1bb5b205f", + "b00b5a1ccbc1ec142c5ef063daa88aaac1d3e52af12256cd892803aa348c3f30", + "0ea22dee4f62ac35e5b45a3beeff82c8c32231193bcc33ba7575916dacbdebde", + "5c428762fa847e9a97fce0f306c7f0d8c4b4abdf9defcb0df0dafec41590ebb8", + "8eacd0d7d08645ad8366807a5cc9f91a4b22fd52ab4d8c74efaeb9fba398f24d", + "648ed141936c9497702be348e260b9a4aa9ff5dadc6b654381c9e54160a07f4b", + "682cc4ae34101c80a98fe4a674ef0b16d11ce70b56b3a7fc5b191b360deeaecd", + "2b82b8fa930e14bcc38f1b9bf6a99f3987b32c7e74363d20acf6bd05057cfb5e", + "1d2c04136aa4ce34040a9756ba9f16af6a3fad26b7a9669a62c3563d66a4d4a5", + "04ab01586e7e4615656127d95cd558e318457ed1641c2229b75ed676f21ae1d5", + "a7a5c84706284a3c98121e35e89e3361c39ab8b5ce26c3e479cdb4c2f30aaea9", + "d12b3299cd334b9899ce855a088577d8d757dc32125921f2ee561daa94814b7b", + "48714a6b92c6e98e3406ee38539721a3ba60254ace52c741925222955a89a8bb", + "27597f719f1a11522733a0db69462fd7d28f0a64d6cb508af5dbfee291b52f81", + "44c2fc25551c204419d798aafacde9e75b4aed663d47c0cca8c5c3482b7660ac", + "9cfc7a76eafe717ecd56501bc16aa644bac189cab58dbb90bbb0cf3a4e3053ef", + "029223d300c7186ab0c823474f67590a86e7d08a9d1ed39588307def876ad59b", + "533054cb85955768daaedd98ccd79495e4726b2e5e7d345a3cc70beb19538a57", + "60e7f410dd3c32e1c88638ca4e758db9a2a8f7e52e2faee358fdbf5dd3fce27e", + "3ed8f47b67c5cc36b0e6745a29251d0470795cb14f0ad9b30170c0e5c8efcbe6", + "1f889c28d68e869bf711e214dbe739acc35780e8b19ef9576dede8e2e91bdb9d", + "dbf00c8bd10f5ae12f38c625c8af8089cd28ff92f34a1d949dbdc20d7a80d797", + "960ef9daa1b246bb656aed819cbe898dc14ca8e558070c9468f840487c05db92", + "fe1b43ac2709550840c682ecc23d2699e9b3fcf55851999d9f09b02370aad502", + "9f58ad99bdc049a7cc29f7bc482629302d56b32ba3b4a703223945c8a61f4254", + "af61383d241fa4cee95f8065527537a237914d9b1494ac8fd3ed476a604882af", + "503e10b0bd880beb400e91fce69c4fc9f36e3c816ae86b7b8d9729d00d08efc7", + "beb9c475c020f4af8659aa27ef0d1dd143479029a8ef2f605adae61bd693c9ce", + "73b93de4843e323d7db0726e0bc301ff0194b9656bb40bd3baae073b9a4df96b", + "3f3a78847d9964cae0df94acbd73ba24eb042f44e5a473b1dc665a4db518dcbd", + "240d37aeb82815e4a61db5729ca10d069d9554e3f209eecd881d12719a47dab6", + "87f8197177364e5b345abc75771b0418890afaa0c109e2f6da58df071868c7de", + "9d0f1a386ab8b2192f4531281d50df5d354a96afca98b9122198982e0120d384", + "0bd411fccce089fbbd2eb04da0cecc3f030d036a44b78f37352816bd2bec9c1d", + "a5587eb1bb907b021e206cf17beaaeeff2c34c22a9857c0ead14d68f62b1818c", + "ed96c8097a4fa7debe2db8b1aff9baea5ebad839b60b7ac878fc93dd704f9633", + "c244897280ea22de3494642c6a8219500b884ccc77802b953f71cf12de445e28", + "e11b64f780d2e2314fe969a74a86129b8cdaf4d223376843b94f4e64e9be7eec", + "1a7771432e6782bf9a5ef436874bb9aa44a5f46d1a5a7aa785dd1fb2311d9d58", + "215be42fb8c3511dbcbccd1634895857d48d2f26de9f9f167ffa86f141819298", + "5635725b8e48404be37a8c9db3274195cd7e36a651c65d09ee8419c571d1d233", + "3bbe9d510574073e795b4f843f272a1900f31510f9ad36a522525845c90075ee", + "1b19a47569f1803ecc66d1d5163c4c6e7ab9f7e99abe3e8bcf55f8cdbb00ef7a", + "c155578db721fb204f9759119f8ff004d78ba068869c938d9ce0a57e650e085c", + "017d7f31830417c7a0ef240d7837ef9cf047ed0a8414eb085da37fe2cddbdf64", + "78ea7551cf28cb954259a28ec1340ee66d715f02c5eb7fec48145298581464f1", + "b89ee6474841186f5afab538f9ca17b4171c303d5dcc89247b79ce38389bc0da", + "f15ceaf2a19e8eb599bac5959aba3518688d4791321e618b5ff4af1700fc969f", + "6c0be8eac8a473b665f89bc3a310c033c093f5b062527a227921b072aa3123be", + "60950a25415add062c9bd5427cf5e0eb75d6fd7363af9d44656831d04cdcf7fe", + "c8ee6f1b5cafc14571642946e8d8e56191c3a9a020e5322ec30a698258c24a17", + "fd5a24fb185292c845fb8759d924a72f24c18758fe9ac5e0df99d1365e0657a4", + "85db138de6940bcd58f37836e431c6d0dda1722d21f7fe2e519d08ec8d990d7b", + "13a18fb4b02a808771e8a5eb2a64829bf27c7e6fb82b2adf8d44a920fa6a5900", + "cf37460c8ad19a604cd4d1475e8258ddbf72e3163d07bad676a0db39248f298d", + "16aaf7854629a4612442c7c7b67836990ce60a78b95bb0221646cb9d27454e2a", + "f3ff2a33d32daff5a2606ad41d8b9fa469fed175ab2d12cba2432d2cec320bcd", + "3f84dcab7582376089e959b42b47ed237ad52f5e633093bf7fcbff6164b74cbd", + "1776d3f2ca165cc20e0736e7ba69ce320b1e19575245e0e78bf628bf8a3b4c90", + "4b815273385785fe9e57dbe6bbab75423b6302f8daef245c12bc4b8572696893", + "f9daf148d2ece99869e00ed44ffc1a740792569a36d6dc21a94388453c31a08b", + "0a7f0cc9bc54b816a21785f84414bdb99f34a49a3e33e5fa1d105ba026540627", + "05ec349ece9f47e92ec0bada62dc00a7896abe818b213914b95d3cc301afa0cf", + "3f652f63459aba4684390a90687e117bfd79e09b42542733acb734adbb0a01cf", + "d7be18e61d11c4e5757f2ad5fb6b8eb70ab1f621f7785d048baec87e3c81ddb7", + "cb4ef5ac7ff2c34808e9398ff7e313cfb5b73e8b3cf9c0c3ab8059983159213c", + "c1f66f86c3dc26b2212cd7536bd40bde14015a19108d997e24d0f5b1532c5201", + "2e8766d4dd56646f8fca1cc12fe66785bd90f42d27074c71e3380732f5616558", + "feb95b15d26976b6b31bd488430da464d87102d5b35cec8bb578cffc91bc7933", + "bef956f6a785e9937e4e3bad0b1468c428534fc06658738d5e02c1a604c77003", + "fb471f8b762add49e6d91eb46cc018a6990ca7bf1fb15d26aec90f6ba968f6cd", + "d4f35e4a4e006546af1bf09f5e49b5dda60ae5dd526b85ba92d871f4f09df77b", + "795fda42bece1d711437fe504f78e940f309c89830bdb5e9dd46afe3fbb1b150", + "abaf0bc879b70ace0a82af4b9d4f32085ddc7c5d920a673c47e83199959b4de6", + "ae69a784b18524bf95e7b2349cd1c36709eb810b0168e7e140a7a411443a02dd", + "7858fa7acf40bbc2168ebf05e3389ae646d1fd4567b17470506596baa5e00ea9", + "d9f1d5f5ba2a228915342e41510bcdaac533e10025531eb9a1960b93ae667332", + "5a0def567978daf9e17209cf27f261256f11d0f8cc946c888ba3bd15e56c786a", + "589c40f1e0e442e1e227c1d45ddbd799c41cc38d025ae871bbbad98e0b65d2c4", + "d25e897ed507bc4dcbbf349cc2de3d2240ad6bede49c89fb07c9b5dcc138363c", + "b480a7829783c502490abec998b2d0253aeeee68e54bc165a5f1e98cb6a9f4c5", + "ae8c6d400c2d517eb69379c486e1ad35497e692292bbe495f0d01b4d4e48e265", + "27fe2f66e35f873b8094d7e2ced46e55811f5a936f9019e5e2db41b7ae5e819b", + "1d767698a7e6a1d6adfb55c1584880350f2c4e80f40e51776bd33a6a2e47ba8a", + "70114481a06394db7de45673ff702a2ca4047d2715d780884f3f841a493790b3", + "404487dd890566585291dbf1a22cbbb7fd70e4870b36f39ee7c710320aa0c270", + "b908e1a132f73bc705594db6e08b82fb277a87456142f6e6145fa4701d7de09c", + "7f0734a0c77fbb88b5a5a4d49f995bad567f0a676cc9326c3267211d06ffffe7", + "204e168e47177bf41e3066b1ffa5f0779d98fdc7a0ff8ac5399f1c3abd6998cd", + "fae75f8b1c0302a15813cdacc724fb9c9d06d6ac7c6a358b1f1c33a9df3da5c3", + "c18e90a57bd273f5d27751fe81ea9302f5604f24c2a2a9d306e2b0b26ed65cdc", + "dc084d1ec5c777f8cb78fa955903adb7bcc68b0d18cd2de66f9c32acfe9e9948", + "4fbad69a041e89833b72a21907459d2a9985bef82d05f70a8740f4d1abbac471", + "f0d752ca1604efdc9102728c70fc2198ed466b21acdafe6a234f7124cdc3c531", + "96a291d08e64c21d74c94982a2d1a146ca25ef24b4c2ffcdb3a8bcd77288072b", + "d84f7a140461dc63b72d66dc97af315127f8260a6176159f7120ad15e0ffbb42", + "e323b02b16317a133f695b892900cbed463c146310f13847cf52f5e8a1556403", + "56f9986c62059e091f741f5febd25cd26400acfd51302e09e853c49815345372", + "4a91c13e08448237509eefdc09ed41470f72309aeb0f3577bfbdaf2f45b359ca", + "3f1214f03f2bb53d779d05f2a3a647d0bc2550b5540ef159c2917cbab8abe006", + "c00d7a898094e86d4659ecd1ebead956c2d74969a9c7ea05b2ef5daf456cbfde", + "c9f7b7bd863e12133688f089abc3846578a8c50f8fe95d33ffae1472cd61d396", + "9c4cb359614595f16ba7a76a3a5f7a78205c32a23653a935400f147206eb4a2e", + "f81f1b1b6ec3af3a09c79607ff09a025162ddfc14cf85ddaf4ed678599031bcb", + "07518a7b6771f27678761c04c584da6475af520f2358b0886ac0f792e5b07e79", + "3d8edf9ff36d26b12f22af02685c72a8f9106295c7cd06cfe74fd59967501ef9", + "957afb2e2abda010d44ee4eda67a2b70b61c56594a31964f860b26a28cb8454f", + "f136ce60520308496e1802b6de253f4911afe61e910358b2ead474a9cf48ee6c", + "7c90e7fd57edfe1fd3504153c5fc31342a7b354e93038b45140b67fb6fd18e65", + "629c3a127f091ebee42b4d487d029bd4c3fa2ec72dbecc715e6c8c9d2671b736", + "41e8646e02f9bb0537b627ae5f269154b03d916fc490d38abbcfc3ebf6d9dda4", + "72d66600ed0cc1faa6d91c2a0da81bd90d4538cee893aae4bb0cc14eeb4dc280", + "81889ef1c6d2c81067e366ab7185eb2199be8789f114ddd6a3b693ef7821bbec", + "8adb60319e326ca0afa70e7f8ce7209bef89e1ec820a5d70f13d606add534ab6", + "ddcebfd51afd77e78f06d5a5dccbfb4d6dc61977c47ac9b1a2f6402940d9a156", + "b4cc18127051cfd93b1a5eb0358153c03f1d44b841a41cf3c2a7a23a14dec4f4", + "a3d44b5e13a0c0e6eb22da6e8811d4995d495d4d780e840ee85c83f2b1656082", + "eb69269adfc48cad8200b8e31807e9454ec615b8d33b67e7191cead52549007a", + "cc6bed8de65dc9b905e2aa95ad5fd57bb82ee21c49968cf30a6cb4b002be5f6d", + "0ee85d0f8f0f5bfb77f957de73fcb2da0ff2691b2971ea681432a67ae5c7c378", + "d8a2af3e7a30804574e74ed97275804fcc22c19607ea30251f7fcecfc86c8cd4", + "be4f45e912a7cc5a174d6cd1c058412042308381f9882d2a74c43cef9a93e51c", + "b2137011b2206e9737392b735787c1861d5df47690b5682cbd432fcac4c05198", + "f4fcc63e7a123783b643adbf0b97580b789def70d8ce5af5955b35b263f4a4fe", + "1cdf85f46a7dd1937cd43c941f1b555d582f36d3091097231f86602c73c84c7d", + "76cd77e06f946eab3f59cdccd3a3e20395f04d5e61ebc5ae6cfebbf72d96214a", + "f080f232fbf74a104a25ff2df6958e0008662596f81cfb84cd72c2a940c9b5fd", + "70a13b34f284353ecae70355403e6297fbab263f207707860ec22dc8a2da4c94", + "2d1bceb4417eabef500af978c975f18ab1b01b12946ff411832af267652f5d49", + "74847cb2b631547a13df246f10f21053e1d412b4bcc83d2a28285033c75a2839", + "0df6e0d42f19ca95d31d9b762fb58b9ee4ef0a2037cab32e49509c575c6ed19b", + "e9b6acc84e087163ed1ea23f7b7aa0fa4ea37f15473a47af6618d7f30e8f56a7", + "3578384eae5e7a10731d6de0972072f1f568a764ee9a2c2c6152eee141fdd3d7", + "69653aa6632c7fb7c69a8129861e9f3226b726a8804471165642481b598dd099", + "a296c05fca5e44afc5c2cd583cbde4a1791bdb7a8461c6676b97ea63c11fe393", + "077900cf366702fe8add4f0265cc52273b343a14218ea59ab26cd628a38240fa", + "a63d694c0e6728166e578d90061d24146d56ccda4ef281c41ce6076ad51727bc", + "1623e53bff3af6346b27b38cfc0d98f6f2ec80830789f88bbccfbecb4ce1162c", + "ce5c3ddd1852bb75590b6d8db16b4e6264a8993a3aad89f1731753565d5e54fb", + "3dc87d48cddba8c9aea71e8307e9d803925c9e4c4f55d7730d3e651dbfc7ba45", + "1cd04f320cca7fe33c2f43dd9e0098cda82edf20162a67320e68ebaf3fd74fe5", + "8d63274c4a44170e5008d2d1b790d5f468269abfa91a54c329152433a30665b9", + "89ff3946f2111436afd2b41932c475a0d09223e23b6b9e9a9ccf164d3cbe0b11", + "db63caf3eda1222a308deb317a0bc8f1deb95a6b3b0108f71d2d16691244927b", + "3d1ba03634ffaeb5d906032273191fe2595bd655a70dd8c22680fa4929012c6f", + "0d355563eac60a85c52b4b2a54c21ee7610819a9a3b30e69a9a37b5655b9b5db", + "373a44ab3936104a3d9d26a246d2365127c1726c34d2f1d7e87d7d7f6058f248", + "16022145fe055769433d4c3cd9639ce0f8f668e157bf9711e2c7bd7907178df4", + "6b34e7d77649a72887fa46cb34618d813a32fbb77e4ebdd6691527eacf5fd84c", + "df915fc7ca49225b34f2d419554d2594c8b2ffd834b2bb92f49397fcff381eac", + "6002c0ef73c781b20d7d4d9e01322f8efa8aa78e5e9792bce29a9169c7032f57", + "3a0debbc0ee42304a9edb10db770d0b8180c403971f42dad8832cc16e395c11c", + "6d76b23d46aae2e574ec2947c490f2ead9acf9592e417069e72050721224fba4", + "593602ee2803bcdd743fce93962f672cb877edc7d009be65c02a562b8604a74c", + "64759e44d4e426d1c147b405330e2fe1ed1afe168b8db60a87c935cf07ea10f6", + "d8f1eea3b2c56c9bdeeea5cc57d5fb38cda7c2dcef9276ed5602d27b9788c194", + "0da1328dfa5efb29034215279a87bf026e70aa7ceb2793e8c0ba9354580d552b", + "8bf631c56ecb7ab31c0d30bf31dd28a62877402cd6825d0ecc9c71e26f4be643", + "ae29941b70abb79f690de45e32c8b55f8114b30cae4376e991b963d4e03d970d", + "0b87ec546259a15a8c8752634c3ef219459c382d383f155f219c04d9d5bd7f01", + "87605721bbed02e5f279a29dbf5fb857a5e1d41ffbc50689ec4e1ad1ed3f2b76", + "64d33c169662adb9ae01cc1a902b8725ea84162226d860b05b22c1cf8b716b88", + "dd3e22b9aaf98f165f20c449618ebf776727386daf5695e68847462a1573f1fa", + "2eed687fa82a0df68fb2a1ccd9c7e0679f5ada28b831b105718dbd0580c0aeb6", + "3748437a03b2e086825a1b26b9a363495cb04ee3b5c6125f16fa7153d388b38d", + "d6f69cb9ff2388175b1e549df23171ea6cf0f181c52902e90740dec0fef941b5", + "81e173033c052c2a8b6c2ce928c7d9e7f7b956e9744156c8b40cf815f093de69", + "627027e95f078f86d70df8b97d509bd556c6397693025367ae4b43edc9b6fdf2", + "9c8bee1ae58091342dac7901e5846df4acbbc409656457b03e1702eeabbfc8f5", + "ee387d8e66f5bf75dfdcdeb97d28b4dbbcb1302bf20d6bf8465b7b41832550ad", + "681f0145ff4dd8a84e42b02ca5bdf4bbe8adfdff11e3ae57673740a4f98f0c67", + "9f7d29a27a80e64603c98125d6f77e23afac3ae85ab0fbbe91e74e498fa69c89", + "f23c72f39810dfa196ddac4a2b30fc97f3e05cfd67d91ef2e6ab063ee29483a9", + "cb02c55cbe1a20d200bd53e718c78f9f2307d0476082bb425d4476a70ef47172", + "2c56d7a46e2e989c71d754a9ed2ed22285adb9bc7ea0ce85e7ad0146798836e0", + "161f8ede4832a65d9d082699e93299189916a9feae06703aabba7aa7100c8078", + "c14f24845530dbca3372c7d7453441113682d76ca7c5ee93a86f8705dfa6754b", + "98e2302a8c27aff16705eac3c9b4861e0d670571ca493dd0077280785d1d32a3", + "1af7252bf32de08559d47dc9273c82a7e6cf89c2ad0d755084116bdc30e2dcb9", + "b662f05da0258a7218659df10926cb716fc6d092654ebdf9d8a0efcab72f311e", + "ad8807bdc9c0db6fff8ba8963af51f14ed809ff5cff5944d27b1923bc5cec68e", + "4502bbe76bba11effb1f91a822732bb4d1c6cbd20a12722cf7cf2276bcb224ef", + "bd100da2cd60b60a2923299e505c6c0fd23cfdeebefbcec8119567fee2b8d5ac", + "2725060e6d944dceab24729b82d3a64275ebd82cb42ab330af43f53cefa27893", + "9cad96ee381a6d63d841569e475eeff6dee67836e2d05113a8f18aee590673b2", + "c22c8e48d846ad43fc8319c4d3ed8c4af31ea7c3e079595cbb557b78e50f0a60", + "24a9bcf359c2345fdc35812a10f9f08229d11f17c5951120cda1264adb2e6d71", + "54658722a2c31e13616e4a38a817927c01d2df37830dab7e76ec16e7ec93e298", + "fdf9d5820ce67e076d78db09766a5827c6d14678d1336c8f936608d80ae83d72", + "f3c55939c47563aecd9a0be2fd16c8d839331c811403ea22d47a8dedb636f389", + "9a62ce2c9a3238370ab983857b27a6ebba407540c6a55b8d3e62fb012b9177d5", + "32ea29fa721d7727b0ad77f534bba0ccd4f08b16b1c809c60da2e83c37cd052f", + "0c8eab2d814623966122a83e079ae8629897d405fc471fe70eb8ecf63d53a6a4", + "b5f81d6be6c5ed881db8e195b850c3ebfa2cb455d0a1d14df79740eebe5d549e", + "ff39acb8667a42faa265d97a90fa7d5802c6a581dfa6bc868fc6e953b4fe3b10", + "cd1a9a932859370e971587fa26255782a1aee9a1efeafd17badd1aca3c822363", + "780034cf738e520971387345b73f42cd13770dc8cf68948ec9e8e1f6a6327f61", + "0d97a70b467fbf4af625a6b539035b19e86d011782bb7a9ce538bb89d81438f5", + "ca8dcf9e3454d6c2a346a01372c798cf2cf8c49c38f9581d705c42e4f1e7c4b4", + "0c21834cdce637a85f931e8d2d961573d1dba0ab7bee93189f74c6331995c914", + "c8aa251601172c9479b4ab2c1382a2d6608b547cf6b7a4a04a75a91fde4c08b3", + "14d36f3db5b2cfac7a99df066016eba59a21ef651e991d631c04acb11d1d2b51", + "89006b74bc61637b0808bf786df16039d0f5a26d1cde1348de0f33718d47e75c", + "098621c303cb1b9dda387a0ee66ecd267172a70defc1b403887ea74c53d5b911", + "f9428c1dc527138be5c9ddbfeed43077d06f2826b75edf9218e7b90df8b5ec29", + "245270fc3442e19475818ca0d5cd32a4728a25b5c4f2231d571eafa281e61e18", + "753e79f5b9edd2e49c30c17d3d5050a2c0265027abdf8e661281d22e6cacb701", + "7ac9b5239a33b2689c63739de793dfbbd0955c044fe062bab28f7b9e72af4755", + "ba6c405725d69b2c74661f3e4259242b054d13f197db5700d1d7728110e56955", + "a7ff3309d156a8d4af88c92e5959c98617e0c1b95a10caa544fd761f93715b80", + "4185a1fd84ac173ac31493ea80b4a6d38d37325e8d82d07ee31bbd7f5c661e17", + "78826fde40d9fed3355c37bcaaf0656a4236d58404a892d1da402955698e2904", + "9f591f6bb853601a315de1edebc0927bde202e6cdacacace8dd32bc824a8a094", + "3ae950afe6ce59737c943cd90dc8f100de10de1ee952b2d295514c4f710aa7d2", + "a75089fa658b1206f03622608239f2de628b4a4693548c864d6096e2cef76490", + "efa7ea258d706b04547b8d4a791b1113fc77a4e5547c8a308a89bcbbf8161e56", + "f0c8cd991b168fa30685d9adb508e9aeb4f2b63dc08a153b814b0cf1f989183b", + "6f1946a94c3138276073dcfe0d0d48d3cff0452993b34965a8fefe90161f0fcd", + "2bc40489956125fa47c9cff896091977b8dd0b7a0e0150f614592b11a2e4d1ff", + "b2f96fc311567ddbef1bf0fc487f973145f717f283883606ee03b4384da74003", + "afb571e0e140ddda917b23d993474493d61a8414b0bd191092b6780bed309756", + "687a64c739ccc3941a0eb74554bcd0135e0063922933ff892c7b90b88e21c7b8", + "5ddc34627e55a68c4fd82c789c10d7fecf3e380b458ecd18eb8c5ae041c0b288", + "ee7584bbd58e279c2667ffc07c1c4977654fc948a63e33633a563c757f0dd9fd", + "3b7fb2e668fbef730803f10eea0e6cd9a2819eee7c1c1418431d1dc683def7f2", + "b4e18431bfedbd98c924a64497c60ca9e4a82f22084fa1ad24c09f79b2ea6a94", + "a5073a937e8b5cbbabb8915cfd2f2fe7307e4e334d5f434077995815475f0df2", + "455b0de46808bf01d625c22f0cea27120780c8c69c0c11cf837e98823faf58ff", + "a2b26681847ad348da10f7b578cae28087a3f5aabc5f84de188ae7ba9dc754f4", + "f03694a80d8dcf0b98d4c4ae69a797218cff7688f80ea97e627e817ad36f3795", + "006670166e382b2bf369907cb2b4834d58ccdc55a4bbd6494e8f9c129f5140e6", + "ba02ab95f8cdf7e40f03a629e47a0f7d5b77096549f17a158c73e2b96c6cf0ed", + "e1180c4ae2e094ff0ef2dd8107e5f2b02bebfad6501b18965b75ce8689efea2a", + "a8114db88256707afdb0f589c4e375a7b165380a0f8c2795c0c0dc0d02f5684c", + "a8ff66f47c414f56d8129e33a45a0bd9b9d1a640a3a3454d3496ba3a800b2d1f", + "f36ca0f95e603f806380dce52d91036d8e3a5545e1dd7620a19196397b91359b", + "1c207b576a1e9e765209ac6756c7948fa3f252a4687696d0f8e831f14ca52b92", + "cc5eb95f7759dfb6cc55027d49a5fe9c52f3006568cd97a0c544638be7a887ee", + "4e15d5885837d4e5e79bff21930dcacba97d2bac39e506896e005fd55aad7e67", + "5e2e62808fbe25793a6917dfbfd06029ea30eee0892116fe6ea4696fcf62ef53", + "334d575dcfb7f6b606866af045d0a378f53447ef51d5b3d1afd83d78c0aee1eb", + "591c6dc98541b9fa044ab89f78856374c3f42ca5f8021bfa0e119d8a04a28646", + "c502d6853c8eb8a2657e069659c1f3b06878498d1b1a15f771fffaf886a1ae52", + "2d4b16c2b6cee606af3c02ae8d6a86560e2393306fed2ac0b72456a8e35e24b6", + "b2ee8ad11dbd4ddbca245b403ada57354b5df94a9de54bdf574bf0eb41645c89", + "a895e7d8adaec8c44b603648f36774980707d30ede4f5dbaf2bd3d2e3d4e90b4", + "1e76fe8a5aa1b516db5d04f3496e69170d3a6910aea79ad7554e2527b81b6b5a", + "1576d0f08a566bc422049efcdd942ba455e61a1bb2d6e1ad470e098899128f75", + "ffc42cbcc82b0b23c5616301e90aa6ed76f485cd63506ba52097a42b5c17420e", + "27fa1edc6938b7d3658c5c9d894136857fc0be8fa743924a86ee4baf583599bf", + "4c8a502fb162a9580502f4f2618be83041782f245c6023e1d6c3ecd7bbf2cbd2", + "5b72391f63f61ba48fbf8fd308ca6afb7d72b9545d1812538f2e4d1414c6fb1d", + "1968aab09560c49509dc7cdac8c5c919629a3da0a39d7628b172d5e7bde99c20", + "97656482e3f4c9dbd01cf1fd91f0deb0b319c15a89b5deec6a57fac647a81753", + "d1392d6f39252c12b76aca6762fd95086785433913e5960b3c14a708401c2897", + "c11244439c754d91aa30a2fcddeef5a0be7e300b94a5c98967785f112cdbcfff", + "21c529765b6a1048c8c946578be14e63750eeae28521fd845bf7a3ae669f203b", + "c2f22169832b5de185aaa5642f125738f164001083438328d2541c15a7f986af", + "293583e0b2b86f041204e4ddc9546ca74431407f88ce36665337a5602dbe3bbd", + "1c270bcff184b5efcdec7f630e707c3e7c90a519302233fdc08aa744d4cc0f9f", + "88a9d5a81d358a14cd92c14d669903032c93371915725d764fb01c7c1673d4e0", + "c733a54fa8d1ef07070c52ce77898eb93f148b13cdd1b6fa69884dae4b62144c", + "acc7546cee77ad0a641bd33253869e0459325db8e6e3a9c7e3812916939e5345", + "edb6a177be83d61f8e67db1b3837db0316d0205cc7303c242c697fb3c2a1af25", + "8e1e002d811734ae8033333794c2bdbe826f1c69f4dd6197e595e75837a3ca64", + "316f26c9d7b2dc523838fd4d8a102e7b817a44d00a4f31dcd30bd6529ca3070b", + "0ae34d23335c3138e2ad4a282b3bdb89542bdc29a4df69d202ac41f140e15aa9", + "6c6d48776be2e5a30e91af97bd6b0bc5a5eb76a5fd713adc399f8664918cee9f", + "dfd7c371eef529314fa853918ed4fac86e473d5bfcd9dbe261e57201d70c082f", + "0046ae85e6230a07ef27a9c7c75dc6f6ccb7742433cf8be17343ea8666512259", + "dd9fe83a560f0897bd19f23474fe744c8eade026cdb1f048e84f3459e0f19303", + "3e6a71ec2a652defe81127e60e22728c5c2b6b7ebd3f8946e347d662ff3a868b", + "a4229eb078f80905f012a474545e0e2786f98bf7fb2073dcead7ec73c65fbe9b", + "7dcad9638f5473ae40af1a5d9334ca763a09c66812806f67e75bb44e457f87c3", + "374ff61877eb866ee324a87077eb71d0a269e9845847e55a5e10fe5f3c77aade", + "ef6f393a0d140250e071d3b82f5086c13bb429266cf944bae8cdaa28834cef8b", + "9651ad41b55c6e8d6abb345d4ea06e4399cf2902d2c1cf13ebbfba0b7140c8bc", + "9d0540e58a6445fd868ea65c40185a7da7858524f419797084a602427ace7477", + "5d812312bfa56dced2d010ddbd98b79ec946a4c13fc545088c80d037e4df3c3b", + "c75d28a33c292a50e293d4e88f25421bc03c83fdbafaba4f526b8c6a7e5c2e2e", + "53cc98e7b41fa8cf94ae5ddb9807fcf8c73c209d910c892f266621fa05782ece", + "f6b589decfffc6c33957cd061404dea6482435abcabbad3485266cf4a8e7fc05", + "e4e03d0c467c2420923815fdef774f530d07c0c415e9975bed75db2ce4227dbf", + "7178a3cf86b9b27023e576a0f112ddbd3e240289bf8c4256ec384962207d749d", + "58c29327189acf5670bf364f2c9023439eab2dff643a4ff36f3d35b43263111e", + "d9b365bae902827f415288ab30e5e75d93038ffc1466356d6f5280863f18dad2", + "ffd371a7a95b47d197bc947e0e1c3cd50a96657ca14116fad6fb7ec96aec3bf1", + "10ab513a9aa33ce2ea4cf293dd0c435b5aad5a1adff65604dd3f07cfa1f340ee", + "40863aa43e50ba80bdafdba66ae053896370421d48f59c2aa962ec5e2914bd96", + "0363498ed651cf53bd469fd0dd8ffdcee6c6b4641a67bf52fd177ee7e9e81324", + "7804a9ccc78fcc0e6b6583e2fe28a32d3b409a08dc48061233f8faf88dbcfa03", + "68ae59987180df7089fe2566c5d2a61d554b267332a24dc44ffba97c1ab1b596", + "f2c29ca8dd0b0429df3fe25fe3e36e6fdef1170dbed8baa30cde80f13cd093f5", + "06d4400c68d16ce0b1b70f8fef726fdf633af85eac4e285694db95c0f8e503cd", + "8db9481d31cd20f8ebf15b2ac559ee46796f0da83b8a94139ab998e2be19f035", + "4c4a93fcff164e070a9eac32c8b7a582a00c6d32f8dd146cc603520d91fb11dd", + "3d70cb493fee1f8575dc7da9c7627d31ebf42c9842275f772ce6f0d42e06db0a", + "9f28537ea916ceebf76f1a42544c43e13e9075929228f32190d9ba4f5bb5c9b0", + "673af69eacb58312c01c9b4e93167bf8e5bec2ccb4450ab833baee81cdbbd95b", + "0d0b61e12e81bed69380c9b8eb23a2384a48bb13a4b7808a21ebdb00960e7e4c", + "284b4b1d0f126cc76b743eed1d255b181a4ea45839ef0c1dd2749ce5ae83270c", + "70cbb1998efc52d6b6aecca67551e42bb44eb6dd6e400241d2e2bab34ef9c1f5", + "a47582681ac364f031f9d19c42e0b68edc0749c5b3da9581009b5187b8b503f4", + "808c8d746109d74eae54b97f45ab7b9ed24f32835cf8a1ec9d7d46c3fae5178b", + "ab0e3e550b11a95ebe15edb506a534d70e4bdfbbc9172b251668cb8eaffa5c7b", + "8d24d23ec2012ff57024b1241dd401eea2ec104645e5dd4088b32546fd8ff904", + "523fc99da5bd881da326c94b303a852c0796792c1f3abfc1fdc7590c9a1ad80a", + "bef023158f2dfff9af92f2aa64e7da3087afe08b2cd5bed4d6a0933e46efd411", + "bdb056bd0ae30d8b926688b56a6e06029905ecb5b25922fe4f6f2004d4adfa11", + "94a65dc8805bbee393a49a95ce8c95574ab280d6cfcc4a1490cec8352754c6a4", + "50410fb63b9759e697bbb5c62d39820ce5d1f76ae9697e064578a15e0621b792", + "90bc57b7055d84741c13f3a7707770de07ac2118015e3f0d08ac9ca3a4a6e177", + "4b703ad6aaf1cbbd6f4fcaeb9745fa82f6fb055611d7164c572892b0fb786114", + "0ab836aa1a9ab4467708652e8dbec2885119358e37b32f51b46d45f33c88046c", + "1a118c7d922133ccd430b9f9857b62f8f2ce4df7d648a197f6edd3fefc07cecd", + "40e846cc774ddf6392d673f69ce95245690f25786770051ba5ad49461310b0df", + "5aadcafbcf0b1b4723d39c5d4048a8d9f3889a3f68aab66cac0ea4f0ef7d549d", + "acdd2f2a548cd325fc06b49c257bfbc9b09c016ab0859cb947aac64c4570f6e9", + "0b3ab537044f2e6142ff425079d25fcd87e0dd8187f03bf0dfc479142d8b9abd", + "497c6521a301865863f7dfcd03316ae0db690e98546116c14df05d98e8cdc893", + "ccdf5949457b2394df193f21d8cacd4971942ea3a1467e2ab13fc0cf38a2aca6", + "b38079f35e81d62e2ec89a5511537abd020e0d49f47cfeacb1fdf0000e3898c3", + "b5477b70b3e6f62fc281e349303e8e160a5cbf6af135bf3c991955b3cb963039", + "64e8bcf3fdb18248ead8b6c5a23b156d76cbc6ab5789815a2b45c365ab20d353", + "86c981eecb0af6e8ada8e68a9ba9a6cb93d0ed527a366121ab0dcf9f44d2a38d", + "93ff8bbd4dced9f09d343b2227099d63a28c121389dda386c8b58acac7e020aa", + "b9b7cc4a4e8b9f5a1ec1116d378e3f90ab3e8e4605d4cdc95b3e4b7df2815a1c", + "f2f37eaae0ce72d792c65a906b6c7334c2377c3c31e454063305e7e63e5329e0", + "cdfcf71c42e3d8d056cab0fbf3625c1b13704870709a4e0850006447f128efc0", + "83566f6ad1536d4aa2543258bdee98989807ffa133d36af3a26672708ead8410", + "68b89c27a8254d5fa39096943bd9bb0074e950fac7e686209c0113df088ae054", + "0e8c8d00c7e71968371f4597da934aeaedc4142586c6454790fff7a4e61af30e", + "0bc7e2ff2af89d45812349f03973406f27a4475c44e71589c4121a9cae9cf37e", + "53738bebafb5a06ce8140b83ef922a31a05521c07c42cb09fbcf6619bf959697", + "809f6e691647916082cde741c82df020254de10148b4b712744f1db345bc2694", + "82238f89893476756a48eaa42cffb4fcf07c8cf4cdbe12a1c43145d2b67aac07", + "ee995fdc7633e803741fffcad712a48aeafef16e5a91aba899e4295bcfa3b577", + "b65b0489ecfa62b9725b7a4c4150caf0f01350de9cfd613c90c60d0fa4875ff2", + "7988a77872e593e831989c6ca5e5e6861b63e1c2a42030d684b51fb88ab73004", + "914ea37de5dc3b2f9035cdb7fb80e6e692a0a5934756beb6d8e2ea8eee7a3895", + "d9c28dc8bc385283650fb2aeb1efe10caf848c39de37a78dbe6f47b8f3bd8b51", + "49a2581f6ebc753d70dc30d3b093882153fef2d0825252354e5beb1b35eb90aa", + "7791d37475e1e15bb9b817b180811d784963d2398c5e736a851af2ef8e622e77", + "3957f179aa7676eadd2c312cd6096f84b7a0424bc99f05c8997bac9fed057ae5", + "9d954ab5224ffd2e0301ff9b33ef834d019596a6c3f56fb634ccc9779b9c694f", + "e6235eda52b721bcc3b997bc8da554f1998b5c7ff43cb86c2d7f1915150c4ded", + "c0684e197ace1cb57ab553203b16fbccce5c993b876b90499e96d94ce308b956", + "70cb7be7ca2d01bb41c09c06fefc04e8d6dbfcdbd9b8df4bd38caa3c7fbb6001", + "96235ac39c397550e28b63f63a4bebf549215c5e72cc78488efeef131b0cdcf8", + "f3bc2994745fcbbc792d905ce1858978814bcec9a5a0a3eea4913bb87939378e", + "caf6345a1bc6013c428933fcf151ee366a11ee41070c3de2a3e5036a65145194", + "ce92dd8170fc721b8bb69c261cd868b82a2d1ccc556bef3999128a3c20bd7c5a", + "45e73df58de1b26bdc1b842b5a951da84381708cd15b40eab87ad44641777c6f", + "1a57c8e01d94f7455719c85c662f76580b9f4f530aabbecee9c5abe00fd7def4", + "c92b4679c9376df3e41d79131f42961fddcdf99afd2c476168daf9657f769373", + "2a2893a114df2630e4b32c47bd60612b4f3d9b04891e944af55fd2767d260314", + "98dfd86f3bb1b56d0e5b3d0a9b3227642b09ee0a2ae78f19eb504edd1bd91ba1", + "2bca879ad4553c1406e38ab072cda33740cf1681f18e7cf548ce040528fa5d05", + "5c3039135e50309be3292559fc0f0d6a5de7ab2803c83f206c8cfcc06683b54e", + "7b46b23f8ca2c45baf6124165030a1ee5fc27f1ae3d1226defb2574177cf728f", + "b37b504ccbcc5ae9f8b787fcc8da194fdda3a25b94fbf217444c13fa4a0863d5", + "36d5d669f4afaf603a54902269087a0f65f89962f87ba03be56b552bf04ce81f", + "f57f94affbe464d876235351bd20660ad5896e5f5bd38978d64346564a089754", + "4637eacec37dc577f7d0c780ce46a57de6da9b5173f4d0626963af480b95213b", + "e22295a0995837f730cf99e9465b0e5c41c1f4a99456f6b27d26d66b8ef2c4bf", + "33d01fca1bb71c53d131b757afe6d97342006694ae752c17747a461717f42f8d", + "ed239b46bcf7f8a66b92c15135488e1249f668625c6fd0a08a0f49c733806de0", + "324c9a8fbc527d9e9357595b1b3509847847f340cd557b9305e78fa2ba963be4", + "2fc5f4089b8b6375a41bb70257be3281cdd32778db05663c55f96cbeb976ef1c", + "691fc9f8e0e0fd9c0a0f42f17f33ea83c3462e35d777dd54734108bf273b5a4f", + "e21e3a565655f1f58ebdb421c94110e19a55030777dbbe455e350868020a574a", + "2c3e5c5791cb68bb2177332f5b79c8f7d1cf4a577a903acf225a0d11e45aa5c5", + "5707100bd5a978ef7fe1def6ba895ad388466a4db13679a899c93bcb802d3a7f", + "fb537a7e26384ea9867ad9ac845067553c1461f5c75a5bd50df1afe40b063b1c", + "afea4fc11996d2552a83a4b7bec586c9dbf3346cf2b3ee7a862b8f6ccf163522", + "67a83eb66b438519cd92e6962566f3d1277395d2edfcecdb06565e33b9b4200e", + "a763a731e9fd04d5ef02aef4e7cdc1726a34c7a048cd78c45ddf9f05c52773be", + "c992b26e5d9f56c3f1615fe30fd5d4a70d639e192583e5792909baad8695e90f", + "5f63a2abf7fde874a260f469f56f7392c715f43581fa0e0ad99522dde7c047da", + "2d92ebb998b264b0c7fe1d9dc94c8f3ac171f31a38867eefd8687216342ad4ac", + "36758fd2cddfa3f6c1658a649508727ede3db00c36b5a619260212b50b3e5da7", + "19237f8184a40216542e1f326527f7e512ada4ff9923380f9a7a7e73a0c4aba1", + "3f2d551638a07d4c6b03265f131adae2639d1600a9f31a337d5561bc22c41af6", + "212a3841885d5890d53c698bfc2ee6114c13ed4636483f65bf9f43e2a23c2dc7", + "30c6c4fede61dd964588586338cacee4e0ef8977b556b4fc104fcb6120d45d3e", + "413981cdcd8428b9e76a34010c95f810ed5f39ee66edfcda92ab0ad94f030871", + "c70a24bad26cd871d82aa4563199ce82ef8d00cebc48a1cd698ee1933ac8b6a1", + "d228077b8f08942f4f5c445f5431f6a388d8dec122471f8c24a56ac158c90742", + "6d25f50ee65fc7f46a86e1742f30afc9b9536b65762f75d9bfd1f9ca30df1d75", + "2a7ac6f4b98849eb4d6962c424e91537967350ccb60c5cad40400d46fb0dc3d6", + "002721d6c430ad953c9125dac7b4235a3f5edf80309b23fe9e197d0ef12ab0e2", + "5464573c76aa26e98ba45180d7783bf83a1ec7939c8fd8a210d635225ea1ef42", + "18395b65b1a46358ec085bafcc873706fb482866f54e02e042522ca09ff0452b", + "f4ff0049dc8e5a4f06c6274a1bcdbf8b5f8b841f921a264ea5bff329378fd620", + "19bdc09f26645d82ea05cd1fa0ffaa68885c66530b3f0eb05caa55b88aee4efa", + "6e749013aca97a5ef3b156da4dc621bfefa7e0186e5e4780c31332f9c62ac7f7", + "83800ecc6922e2a6ec0c34c1736a12f2dfcd94eb9f27c6948a5b28d961bc4ae9", + "86b6fe2935f0206083c952c07ea38c25388955eb8a806c10b69d3e52d6ec26e5", + "c7db870f8b09b9498d39b22d828b60a806f7a1de694c43d6bd29f3d76a11677d", + "b0a5809735c83af8e2a3eeb93da682bce95a699392c0c4abc8d39188563e381f", + "63af21f2f1f82d3120e4d8d98e7fda2d9f39d102c57d9c4111d07079a9ffcd47", + "ad654b3ed5a11b9a2a52fbb8dda47e91a72e4b7bac3a4ad75c074786f14c046f", + "6d9c4a44bf22648c07881e2e92a81ffe002c0177d9c7892416ce01ed1f693872", + "16f279dbb7b3f6c3d4f190b36fe2283efd5fe4669a8f70d209eeed96bf55907c", + "f79a83867614dc4d2e47353f53cb5a833582446329dbe4d5e8df941b0926dcb5", + "0cebcc9ea3729487c1b20adb8eafc6fb1398710ff6179a7fdafb1985e5f4a8e8", + "5a53e473fda88a9fd818defdc2c8e68bcea0c5aeded0c1910d6004b0e77ace89", + "f97965894c4a296fca42905242a27228e5a9a0150d31a55e65d637dcb9e60ac1", + "ec72e9e2fe2f4ab9db10b7075012c9e7cd2a4ec02b4c3b76df17f6eb27348ca2", + "a4e92900f9f5490b66e21fb66c0ae094d106eed5366a85418a86a8a3f8f87a28", + "225f9b50f64a97b9eadb625662b1a3e1b400eb86cee50fec8c62260027b4d390", + "7cd4971b0e5453d210c097904480403bd53c689c19d52e4c6a37925d14e4272d", + "3edaf61be8b6cfa06e1d42d7abd2df32033bf63cd14df8bb3ce337e62a822f16", + "89fd8d0e59d8d757b7b8a2c3a2b826e799c028df166b6897a42f08f2284d3bdd", + "0aa2fcdd4e83a4395cb5f514e1ef61ad4bb517d7c60f81bea030d5191c3c79b7", + "80a275ddf6caa4bef4039860d4c5b0c4bd1009953c19ec2c63294ad2c5c56d1d", + "e85d450859d702a0473363070a6e56d5021ebd1111232f2af28a60963acdac16", + "54d48012bd8af661baae940fb6f0d10348137cbb13c650626f42051ede55ec8f", + "bdb3a9bed24420afc6eb80930694b4afe0992318e49428156acdba5762c69e16", + "cd277985e4b4c30a1999e5bf7bf7d1cc903640a04678a93efaac1f9bb5482dea", + "d1319c6862feb76be11c089c753f62873519ec29a0c83b186efe57f50c4e342b", + "1662d8e6db309700071a801a3d066ce97f942abce241c8085f3c023258927eb9", + "e179e018705eb4a49ffa4e5271ddaa4fe1273029b521aea7a69f8f3b764ae017", + "0e7347cdb154f6d96fc8f9eb8991d4149fbb21aa3270c21fa5df61ca140a4fe1", + "34f182c2f44fab5ac9403543493be5006814b2379e0e6b7603d9e0fe76390367", + "5d710a970ba007bffbecdf12d1c920fbda1f57023719d506825e0895a2affc49", + "b99e0f318386f90df96c1d1cd4dc5bbe4726410f327ecc108108ad898e450eb5", + "7eb9ae1f66a6bdc4482175d031bd6af270fe4ff8ed1138200d6eb49e13c0b8e8", + "c508f259179f67bb6e05f2b9c5c5ea751f3a013eb925c6bfcc2a2624b962e059", + "f3d0dab6f1d30ce74c97b89f551ce0820bebad2bc1cbf4a449b11d5408cbc140", + "c116f6b0a0852a30c8a913f9e0f1680ff1be06165e19eac4d00c4e426ca9b6b3", + "25df122cffe53ab88ecb94abae69e0a1d4323eadb7537c5e4894c0e082d58529", + "a30d97485d4162e910b12effc549374221217c216b0a9487e5a07ff1fca835b8", + "5dffc63818983f6a7db2ab65d621819be9f9edac58df38571887cba22aaaf1cf", + "f042fb4eb01dde4b285f2df328f191d20621b66d7630d7691e7c69d9dcc04aaa", + "d4190e2bf10aa9800a1c2395c0c4a38737c21f8bc42dffb23addf762ff309aa6", + "4745eb0e771c9f2144ffd4abf39e63cf6d0ebe122df5f32ae399cc15fe3f7011", + "806c6fabb4059c51865f723ae619e0c55718f973680ce7eb84c49b935f59ff02", + "3352add492d3a7fa9ee046040a3f7795ec375af875c87edfdb0bc876ba14fb3b", + "27dacac03594cae3db57550410bd0f8d166863992895548f6ab782a8332462c6", + "1708396a0792c2a33b8174dd950c4a59ff06e969db873eaf2a7d82453e233b99", + "5680174e172ddbe6943cede98f8d854dabb66ba65e3349889cd6262cdf462a80", + "4960f0a4c46286d62e5bae021da9236ed0258f6c4c779e1797a9e66a47803f0a", + "06af18499315d7dd1c8d16533f0e13465d606fea2946115f23735f3fe2f6d882", + "73b0ee02de1c3c8a9b2bb8a9c48705c94df76c9c2d68a222c8493848c75327ad", + "42044a69f91d56cf8b993166dc1c724ec7d311eff01c8255987cf563d58fa5a2", + "187382209cf9b45c1df5655801a651463854a6495a7aa303887d65ad0e5b915f", + "3e44b609a8f97ea0b412abf88d7748edfa62b7ceaa174bae008e508b1b133e7e", + "d7b1c0b1bb02a6411eeb888ae26bef44c20364113bcfc1a4a3410396cb52b950", + "5534bafcac199f54a48fa713c6f7ce0568abb11b341e38b4a968bddbffffa507", + "84044786d9f286e241174796073fa47ce25c154616ba27bcca69fee1482942e1", + "45eb4756d3895fffeb913bfe149746f56e184f4c336dad29f6f81fc2a3c0168f", + "b583374fc47629a3f17476f4c9c751e851b7c4881eda50e4bead35afb77808b5", + "4a99c32e81aa36e824bc4be78495458020b1faa47d558e7263f2c0477a16e6d8", + "888b7d3a32f4566f21407c251b753b3890604001db91f88d45773bd8a53b2541", + "53eb65e5b425e7acd56ee6234ebf3d40d8fc45a3dab9968ef4273cee307c38a9", + "4bec2659f865dfae69862e128e710680f6114866b3478de257aa5ac596a66555", + "543cbc31d271c10cabc9417a4d4b6dce2dac73c20ff29bfffbbe55584e5329a1", + "4970e6469f1ace57971103d1cb2324b595ff12e34504ef8249971c76296bf578", + "010810a21205384741812fe98489cd7369d9f6fd9e6818b11dea07c78a9073b1", + "6114e32fefd54c1c2352ddf9327727bb6a34a9d8b3b7d01ce59c6e0d32700c7f", + "ecc7248b7093678e7e0ae58333dcef0eaa18e3af261b2d1722d29acacce2b8f5", + "0dc74b6f19b2bbf2f0e7f511440d4dbbc67952050bd1d962516b683ff6f9ec9b", + "fb8447ff1d5200c3acc768992705d2108c37d61b0451c7474304803e76cc09e2", + "f7f222b44c49d2d927fc2e345d64e2f0ed5f9a602758fc5f5ccc73d3b087f8bc", + "295fcc7e7fcb601b3e79c1fe403f920b622bde0f4975419c3641c4d432de8f50", + "e016f042fea1d93fcd4b2326b3ef6051e293b4abd066b3fa5c430ee7773e443c", + "7c8347faa1ad96169f5650a1d138cb10bfb60dcf861d5e2dfce6a986997d2dab", + "060928467611f28b99eaf284bc6c09d6760904ab5a6f9e072a44915a646e3ec8", + "ba582c64b17221c46d27aaf6ce1e2818562639b321666c9513c1e85727a623e6", + "e73793b5e2f33f60887c6ab428339d40d21fcf75530640977215a18db34fd7b4", + "0ea32380009a639b705fda94d93ff032d88c3126503c0d153bd0f104b039bf60", + "13bc02f7f971910299b8c61d1a16782726f22f63b31b2c0c34faece873f4418e", + "a99b3ff6413262f46cf9a44c3fd5d0424109a6373e2580fe97938c3dc04e60df", + "72b149e7ff672ec6b23352493cbcac166062282911e46d2b7ae5228d5814b720", + "58d67077a2b7c836dbc19457eef1b1e1f734370a800388bdfb1012840fb7a240", + "32beefd48538abe6eed6ae137199e03e05229d124628205cce7deb53f16f8f16", + "3e466fb18e2a437292f00acf9f6e662fdc6e0b7cb9c0606e613f81dea3393e39", + "9e537b504916eb6e9838f13eccf0c753869351778201c0813ee35cc19205409c", + "404c0560dbcd46d1aa9cd76246654bd01f62ecafd40ad2b364c426f849941c5e", + "89e5cbab667b81f08eeee6203be0af9cbe9dc0655af3272b0cb3fa722116c200", + "2fac933a1b980ba75339703bd02f055199d057355a8dbe94eda724b79c2ac534", + "c0200b5442f41779316e0e250b7bd011e12551b2babc96c9228f3187fbd17eb3", + "f3269d2e80bfa7e80397a8d32815f8fb8fc0f8dc01dd1c75cd57028a23e5d462", + "d6be8612904259aa6ff43ff30a2bbeba9d58e02f479f56480a8affc2cc9bee64", + "a67a26fdd90d624fff7e501a2a946681134e32e3afe3094a0c3776229e7db123", + "43234eef33f6bad45358374d23ded2731a35c12f472cce58d05e6d9ee56c9535", + "f0217ee0507b13f56f8417d0c92e3c76026da461c22dbb7fdb653442dcf65ef5", + "d9572135cdc5ba7afbf9871de3ffb68a1ca933e71b80e8766e678bde59ad2b52", + "9d32d8b859188827a48fbbc7660900fa51bdb1bbf3dca2ecac1392a1cbbf4757", + "daedaf79fde7f4e49a736f5b81f8b6f505884a073c5aad83997b93bb9685ee52", + "fe7b3ac50841778d1283fd836996b1865f45392d53b95f3bef90ff657a49c498", + "b89b62e4130fe4d40232744fb0182d477047acd40d32bcd15fb6181ddc890a6a", + "0ae8786dcd5bffb824b81fa038b10c9127b811664e0d9655d537d95377d6d93c", + "8608661859b838a7e8ec16e15db1114aa9db6b141877851e09439c591b46d830", + "f5351c6f978f600be1a71c95abb638bdda6a7f6587e3f66d53bda682c7aa631f", + "b6bcd591af74caa9970d39e0747bf20b56590878cc5b76da411525fd34b71cab", + "bff91200c32ad862bfee68ac1fac4b85a5e6dbba4fd00a7d09e217cd226ce777", + "40e5ebfabd309ba228262431317a2ffa268074cdf83556682d5f5e8c77660c5f", + "c379dedb960b327f684e794ec212b11fd1eb8f20a6847676554be85aa4527dd2", + "e405b5be582345aec0595f1a8a4aac6f938b539426fc7910d1271f3e2d1ba81d", + "e27a84e97deb5450a38bffa685d0461dd90e2fc9c42aa1261da9d3b9fc8fa0b0", + "c8b7e2506c4a067bacda36ed1bba81327b18a08df3fd373be25690754b9d8439", + "5b8d011d0493a5217b74d086f4b8db0bb38cb357c76b1d59441af36806ead1f5", + "4ad67654188a9c920bf0bfe25d22c433035d876e28f79a7c09c5649ae7b970eb", + "2a0abcd054b303b2077ba72f2681a7832712b526b145b62c601741efecb360db", + "bff88f3f33650251f8b9240b1bd94621460baf5d7b224030ff9619a6c708c56a", + "d8846d0b025e2eb7eb528eabb478902ad5990d507837cff5c90b1113addf4c75", + "e57728788dee9191e55c5588236680bec3f0d2cb8791da18064a6cb341b5b7a5", + "f291bfd6644103b66aaa5208ecfb536a28d6209462f98493847ba6454896a369", + "8b0a1b5d290d1069f771d50369e662c5f3c777c2ec3efee722d1a0ea58c6d9f8", + "cdc255d80343d5967d7418ad4edb8dc99ac7138c1621bb8beb0869dafce44cb3", + "cf363923e12340b2e95d7397df7775e3d6d98539001d8262048c67539be6a7c4", + "aa8e17f4657e2aa01025d8c99112d7002fdb1abb086cecb23d428081c87de6dc", + "59faa9c953bfd7f7d5bbfd363c25ed4089fbcce464b2958c9aedcd5729d4883b", + "fd0e1bdf4b5130bfaff244ebd4654e4e0df288abf7673df010bb909286f93ce2", + "f354946364f1449f4cb93847742ad2c2ec82163ccd7fb4e0d5687fccf1a2a018", + "8be151375abca57a5df2d52428068eff6f82dad14d5bfd08a699f8e747ddcf99", + "14a1cb09f11e6401ca5b119808a3176f893db83a0c23fdf9c393ac78139ba429", + "1043a8492b8a8b6a8b9355669a29fc3f2e1e01df59741458bfd3014420df60c8", + "338caae1b0d36e70fb8bbfb3e230a25fc9eab8a1c5b727e1313d61174c878411", + "8e1509bd3f536a9e263d5bbd5cb5d892aa0a860c104f0ac7af74493b9d164bd6", + "de5f3b69cd11a453b41b1595ee919dc2175ef867541c66571e6e71abba2661e4", + "653934f935d080105495891b1889aaf0403223077af687feab1570d03cc08d29", + "0cfc8f472eb4c7e30b17ecf91ba335ac1b479a01154e442d33fd0ef3c0e5dc4d", + "fbc10b32be63288c096a64a70c75dfc76f162707be9f3b09675eeb08d98a4f52", + "d76bf859e647ec2d77a7e5b5f726e8eeef59c6ab7fc1bb01c981b5cde26f3eb3", + "64fe376e3defceb8149411cc4cdffdf3dac15aedb2ae45263a0b94877c8b9ac4", + "3faacaf41e61750af0ac6df1b5a79927dba509d293a7fa2fa9e76bd3a574fd3c", + "a7a80a4cd46db9010cfe1fa8859d658d7b4534832e7b44fddee51c53d6be6408", + "c0268812c0e6cad694d5632f819b5623383fbc3e551122cad33b82536f733791", + "a5a2a39a433131759be0f1f6339c4730de3d401c54c71813066def40e6cfb046", + "61f4ea199859377dd681fd26610068f87e99081866cbf9aeaaaa1163e68fd950", + "65e2e5e77b37be178505f7f40de4e64fd4e86acb9c6236ffec62392e8184692f", + "bc82892fc9c6dc5d7bbf16ba4b2bea2cb9666815bf3f680f72be395384a299e7", + "db5139acccf8dc1294eeb56fae06397f17a040e093d307a9e7b0d62ad6b42ae2", + "43e63ea9672a65432e1b1c21baac7741c9a5f1d6c4e60ae0b4618587415c5a9f", + "75e8ad0712bf9f279d89b252e7fc17ee0add1587df3f355568999fda4dfb0a2a", + "83491196e6dfdec6dc2d09e12380cf55fca8acd4ed8af12012e41f3ac2270978", + "b31d6592a4bedfc5e5d93565a4824f2917326566070ea981e45bdaadaf3022ba", + "ad3b86deac6998141aaaf372f299bc1c39e8bbee4807cb2b6cab7cff32749815", + "e2ea5f557fea88d002756c080dff0350e2ae9c15dbbd423a2be45d660107813b", + "7750f2575cf347fc1b24fdd5b149eba76a438109f7ae02a515e2451179b2c98e", + "c34cafe9369b18ed88c2936cd7cfaa2dad9ff105a9c3a53e49da5e7ad4ae4b4d", + "c7542b4b36dab508ea0de2e1957d451160fb3616c4c81ab967d2aaa97cc0d8c8", + "9da5cd367d3dc450b2c76dfa1ea0ad1e52db56eb7426fa031f2d08d919eb39d6", + "340a3d854262aaf7a8a6bf005c21ae7ef62d58d052c1d108e7e09e708ac3f0d1", + "dc2baad94c21cf7e37a4208a6dddd74bcc84aec852eb308a39ce9f81f6fb883c", + "d21cb6a6f34d9a66c167c4939619a8823be213ceac2bf401721495628d2ea3ff", + "55f3144838496089eb2af67adccd4b27340155cf2b1cc885427f4c7d4e7a8666", + "86ac6e18b9931bd435b38f96d97aed092e1fa8be47089f00bc44b6f7897e64ff", + "b4044a883dfb2eb9f4baa42d259f87d5ab39821470f977548a435cc65897a162", + "d75f0fca387134162a37b5037c5006c3e25c991fd5c0d3eecaed3e75df90b30a", + "4ef8fd941ecc624d1f13ec8c0004744cf907bf91f0a25ea0986294b81d5e45ea", + "015e5322f9f2153091b70b0341912ea37a0da3ed20cfe9b8356c1ce882c00900", + "65f0ffa646fdd7a98f1dde849611f19b2f443703a11ff5a61568b0ba7f85b0ef", + "d9515617ba3fddf4595e395e0e033e9eae484810a478be85bb0eb3851000459b", + "e3858e031fe47a5edc3ec572173de5ea77e9ed292968af2c6ff2fbd98e5d7996", + "4fcaaf279411aa84d92f548b1dfc05458af2afe9ff577221e4b1d82a2e37368b", + "269b84393f62de52b612e2f9922a67d06e523e27d4d2fe6d3aa68caed7a2a190", + "4d179db7af1ec1b6094dd55ce21d26a54e395f23cc3428c0de6ef171d6b015ea", + "1a508533186b12799e418ae46978c8bb8bc88fbd8a478fbea38b86987d197ce1", + "ca4e5990f8a59ddaa61ea319c9ecba88cf321558b8735fc262aaa172185801b1", + "0d48ac9f64fd82bf2e95ffb1eb9f1f73a8dc778417672fbdff374e5093a35f23", + "c2b88c4f59cea3fd60369fc386ac89d22f6998a69b24f4c8ab7fee63247851a9", + "75fa241c04b786f0f2670e0b1e45134aa76445efb26280cf6dfd926f0e77446a", + "b6b80a7ed9aa4d50ec56812cd5a04fa290591976b0adce14e6657815764d1645", + "c29d8b54a265d59ba2e8eb1bed27450e411ee34ccfda853d3a83c1da2fb75b3c", + "180681a90fddc13a6f216b87128055a0947736248d967a5170149a50ec8020f8", + "f65b08dc2c498a0f01adf70e45c5ba21205e259186f928305961718fd086e08b", + "43e8f556219e8e6a7508a2c5ae19d6de0cda7b9f6e893bc19bb68e361c37c786", + "367709af13a857bd2b97701b27c88a7c430a0e791a76c72896b575e017d8d9d6", + "c9207c31bf4517e18513602801a0780242fdb91c1ef85924c2ad4ded7bbf1ada", + "9e48f7bd1d77dc7269d121584eb914303aacf25a336d42914300659e0f67871a", + "8a2b8b728756feef55e9c9bacc138eacf609589afb4422ba2ec2bf9aaaa6b0fb", + "5440c68bcfe6a804c5865c3bbf033033bcabe29e20dbd9f36f5968e2eb0bef02", + "432af6a45d6f2a44d5d6de8c82c4b43ebfc8b289b08866d0c4b4dbfe7337b31f", + "4fb77ac98b549ebda2ba41cf0ee99b55c23386dde4cb505ccc497c4337b243be", + "3f1af916ee3982bb79c31921da5c90a315c9719ef93d87be323bdfb13c6e2770", + "4268889b9776502d948382bc1c9e065793104a2ed0e8aa75b2153b62b4ccc097", + "e14719c99770cf507552d3d64eb822c8c7a6c4d12c769d6cecb6e6e05db85e47", + "70adecdd90fccabd0f39e9d41dc19c6ba9afd11ddb7f09c6ca9c3e568b9e7194", + "c9a94d170bbcb06d9170badf89f2f3533daf8e15b633dbe7ea1d02c62fd2d847", + "538c3cff924df23105e51cb100c844f356d0d05d06e3aacb7c30f98c91292f72", + "bee2c32fb72004ceb24d207bd890ba8fb4126848d2a7bb02ba580c2669f7d27d", + "98db2db86a3da56f16411bf6c2e27f2755afd38673bccc15af8991fca283cd8b", + "fd8f89cfc412008ec0752fabaec3ac72d6c89ad81286980e8138edf6ff4cd992", + "009704c84f0954d45036969fd15c7293048156ff97a76d9c72a08248a4dcd725", + "593b8d1e74c299da61950db52fd3b08f44d6cf972ec05a5c5914da9f230cacc5", + "e8a6d9cff900567aef290b62a57d8d5528b7468adbe2ee0bcef8ac47e2dc1d7c", + "9fbba3e66b12c7c926b88afb5b4a9eb03d1afaac94f673d45639ce9d4dd51416", + "b7f9c83320d1b04ecb9b4f714da462b7bb4e4ab7cf3625a5f0bb497f5405a038", + "4da28a828ab79336ff2e766d3f32e3a5ad11af8203c4428d97ce5c0054aaeca6", + "d1d9ee683589c16d6d043f383ec2023953c482f9c32704fe1a461ba17da05ea2", + "0706b6349613201f5bbb9c6125c48306ff53d4688f64dd994405abad1872d450", + "b22580d4aeda8ed6d73c59bf74efd26c3edbdeb0865c0c4b6ca61db39fc6fcdd", + "978e4abd23ead67962bb223e372c8873f283f2d6bc3b0b0ef6a21fdedbf48420", + "d37e209428acd60797e1e77636bdda5204c56af262d487469b6eb069dc5ba0d9", + "44622d438823091e0823695e5c08b633b3f0c46af0286989798e27530a20273a", + "8902b51c808d5143cf6e368faa11e4e86ff0d373cef041553662da373376ff59", + "8250faa8be69d0ccbc2df66b1046d1ccd6750503855943542b6610a9a6e9cfc2", + "e75d7f1982478da0379f975f964410f4e5645f954ace65f0ba0eeb1ba7d7b190", + "5836da561d17e30faac4342fc555878d144b1108f27112af7f425aaf90f42efb", + "b0d06739420bc6d96ccbae8336bb9b6a28adcac61f1f5712915ef42456222332", + "c8839deeb39bf4e9743d617cf5efc29e9a59f39d681128baa4e900203acab6bc", + "9fca90ccfc4d102cb60b2a6d992c9086a71b1f742dd805cb40b945e598bd255f", + "b08651ed30172874bdb63711d32ef549b43861bae4cee530da74b7874d0deffb", + "f59d3938a18249d47eae4be568d3098688df1c5eee393a913203af87f92ab871", + "68b30e55a656422b4f7757e887a7356781f17ec91acd25d2f062bb9012741423", + "ba79b09ddea8bbdbe7723a906c367b4a51cd8aa47fbe2836c169ec28b4e51af4", + "51b2803fc4d01eb5e9f011c74c5e6b294327f44052e9e56fd0b75475b3f2167b", + "2a40ad807c8ec22752fd4886570008b62762d208a5ea24140813808b64792c48", + "f2abe05e84099e996c1f9a2be789b77259322c5b28a1a24ec418038e77a7c252", + "e39996b22b171c30c0d9d7f3f9b89c8cdde606af69dbf6936ab50d4a8540a43c", + "6782c67e3849c9ce43967ee303d29b12545af84fea12c5cc4cf1f973214eb2d1", + "3477e62c947ab134c3242522425c234318e22998b4f6d7b08422427e36d01004", + "c2020358b71b1f043bff47e117027bc8112d4dd0d65131063e667da9e0974357", + "5e3c95247d735f46b45940288d0e8c0016ee553c4939aa4192ba7e52cbe63b63", + "f3ef5179680400a2a83fa5b01d823bc3906d68ffd29ef18923262472afb8c685", + "5a2f1f9be05ce5402fc0728614f98e6c5d8a30a7008fd7114d2e32f128b99468", + "80489be62cf13a2efe0f72ae08422eb8b9a033bb24783bb075e5a030b12f4d12", + "01aa376514be84131de256c9fd1f3cfa05edd4408a332e699c2f0512e8653203", + "6a59eb72f291d9209aa9ee2580a977707b28e3714d04859352826b7003759f1a", + "1ed1699d9c8af25571a1280b6f5251cf690d0005bbc222829ed1b5c8368fb7d0", + "2b8966e5eafc875e7df9b828674d7adfca2225a5c5458efe5d3490932c0011be", + "d0f30db6d5b92d763aa39c910bfd007d8ce08f58cabdfdb67e38b12478851a81", + "2b5370d6abbb3e0c0cf3d9187e5173d7784622b770dfba6c33b8d672e22e12c9", + "7a874cc8981afc43943b02be3428c2150ca2d94dc2735904be475872fd48bf4b", + "ab8bc740762787a90716c3f1fee072e57031a1525d52ce20214a984e793307b1", + "40983e94cc324c141ba33a7cc5682f3f187d832dd665a2503c4101ce3710b2d1", + "ea73153725b2ef43b2e10a05b7426aa8e241284b1702214e8dd9cd04b38df68d", + "4ab724b8efa9a325df30695daea6894d5da08aaef739f20e716619f4e595885e", + "cceb8bbf3b3bbef90da4b219fe547ad6a3edd9dcd2ae48066f2511a8769e286b", + "b8c442596d32a409e8fa7b4401d52cc8cd2e82ac216d374933dd9013535b443b", + "0a7d2aaff8434564372ad339e2ed316650f490f9932afbc9cfb5f9726edd227d", + "117e018ab57319440ca476a5799a3eb87eb7738a793fde1b95c34d32cbeb4522", + "7c84152e37659e4be0e0806e11fa7ab154e5dde6bba50409c8c475f480016120", + "4408ece10877a6ac3f4320941617b72c27fe122bf0a0b94932bec6f7f6c5953f", + "5b425b6d0c9bdb4fe93fdcb1314b2ecf3cafb77cadea5a3fbc1a377c04c77aa3", + "2352dff4400aeaefaaaa61cf37512d2076e3f5bea92d2ad51aa99f630ef25a67", + "08c9db753634d99a25264104c5b816ebc15b95e0d2f84855d32e52aa90aa5aa7", + "91d5571d7b2e0566426cef2498a5690d32e30268c0a3fc17d81e4d3acdaf65f4", + "71b408acc816438a0acf055260a433b5b0fca4a71e627e54ed7dc96079846c44", + "4c925a3c7a5c74d7ddc2095fd3c303fe9af708cc54ec1992f89719a45dd2e98e", + "60f56a735c8f2f31efd7580c90234e9fc622efcae366849c63a796bf80006a12", + "43961c4f8130c54f32b45f9ee39fe88faab957a734501e7b49e7d7ac8f81ba91", + "5a503cee0a815b1fcc4ee6000b8d88e7fa176f7fff7ebf9abe82379a9b87037c", + "b19e6f323d3ce96402d8ced832944a02728d9df0674a9a7bd8e18dc8896de94c", + "a743c955a7b559a34cd8907b9eb7a40d86f8f15a58e90b91c9fd87414a91b3ef", + "b40cb00eaa805c0d27e86cc289e34dd354d820453382954ed2ac24b27d38c2d4", + "e6016c78523ce714a360f77ad95b39bb8a724277430e6d2ea5a2130334011727", + "e8b448a80c197809e559b30fbff6a7a99d1bae7409ec00c766a129ab70e47f5f", + "4b42f9bb88185504e200703c9fa8dfbaf850978a60827cfe7688c434c502de0f", + "d48a1aaa1f36222f4acbaaa8218e249d5774acf8abea087c9e2ee9e21dfb2ab1", + "cd11b4f441f834863a99988fbecdf8b19be665edfd19ad4db62e9d290a98bd02", + "2d6152c003c48f8f6a1c2693dbb5cc5528e7e164a16beb798c772dc85bf2ee21", + "0f9af43eaaeadb7118498e562e45be1ee1e9b7f3787cbec3aeadf671a2c3138b", + "e87a2dab8e4945b8ad5fb7866efc8dc76a990638822542050506d1d0ce78fcb9", + "190a2cb2ea8d06d989d23ff075bcf61b236841a27975f89bdc085cff823e55b5", + "9b09d9ad2f3a069a4c9b33e77a833fdd6605a2095e8062f8b79a5fe197dda466", + "06dd4f3da3273bd8fe869a976006745235347ab53612247d3e0428fe152787a6", + "8dd9a7dcd863f7a8fe98ed2f452615a74560ab7ab964ff161834a6a6d5b21b4e", + "f68bdbccd9955c40848cd3acd5133f991becbcfbcf167e5d2018633229d1c0ee", + "d1990ee38345d528f0115a99aad45cf41d3a579ee1be601bee3820f181819f4a", + "b09339818aa6b2bd5bf955109969d4ed43830a672c479cf34bca1661e31eacf1", + "c5dc68711d6a27092ee6db8a779fb1972d769a4d571b759e6bca257493b1031e", + "6f7e4d57051949b2abee3b329b299d5a6f555d06cdd83811fdc7d47531ff7179", + "45862ace8f36c0a3a6d4326817f0f80f3041622e85e055e2eb89dbd20d0df324", + "b57841327c49f6c475534d0cd18ff578958790ec3c819fa5b97d8971252138e6", + "e83615f0779683ae32d0f019b20cbf2da935403f509bbb9d2856dc6201b613bb", + "be524d2863d41baa171561328c9435163eca92d6e085ebbc29a4e1e3cc6753ca", + "9dd3c020ae3e8be0f6fcc97e6fb0e8c76ce9694dd5ca8a2e8a122862f552a421", + "a7e5ca5d0be1d24c69f4992a3c1a8f6acee7929afb2538045cf737a5da62898b", + "0cdda68b16cf1e43b41e2d2eedd6547972eeb5491930f9057a3c07743219c1db", + "5c83bb522e9a09d6c81af19b7802ea30ad22484b2ef0c609ade92089e7cc7248", + "078a6ea5a4b2be65c7c423f7ad5e4ad909248729b08264b8f880ac5d90f009f3", + "47a73b39d4c6c631d148b9cc31dd43311d20d91592a9ed38d87579491cd9d855", + "7c625f9d16a27bd30064682fc896a15c4b504819233610fc4a88cccd271059a4", + "5f4c8d2cc5c67a0c393493474e74f770f8c09751cf4d712f0c906d9d3903fcc0", + "60a628f0347932c8679436a3990fe3dcf4d02fb9c24fe4fa9754717c176591e0", + "2fb12765c5e1eb877c55f574c7dc61a4123ebf7c7e72cec7d7e83420fa578853", + "2ef3b7fb03472c1f5a850b40e9b35d9a35d87f05292c1d0d43e863d7e7ec6b4d", + "16c54fff31b4da393130f6eb18111392a96e85421fcd1e928e17ae6359172221", + "56ba9c544d0ca5de806803a36a1a19f53ba914f286d92bce307f91eca1bf5652", + "e8df51a49ab47130db5332e1dd3f1ddafcc2ce80150ae3c37eb100f5627b6bf7", + "3df4f7f14f87f1ea6df31b9b8269cbdcac2d8590a89a2753d7bb11fb8ec576ce", + "ed2560a20088fa40572837ba321394fb3665cc60c15b19a2fd07333101636953", + "a5ecff5434cd0aaa835073066abb2b9f9e493dc4cb7b26983f93c94016c09297", + "af3ef059dd49490d483b8b9a99b245695483aaedbbab25bd1a54852a26b28802", + "39d2d8d4da53e0c373a1e39b4847821317f10d9fff8b7ac598b59b7e79e2be6f", + "b1b6815684cc275278a6a3b0a7de9d0c7d4216f8f1b8c8f1d075be64f70b79f4", + "fb2abc840ce8866537feb12531f420dc89ca9de12b4e192e1ef7cbe765cc4816", + "c7b6c8d3d4ec9457b4caf29f6a6198f893c51263d73d0b12e8bcf5630fea1e7f", + "aa5ae0d35bbba924165abccb826604c80f6ad55781c49e6016bd8207196f66bf", + "650d0eedab56b6492743e7c5692651e8aad37d1a9939963141a67482a686e9c3", + "22d0b3451a44adb8ef87a116097e3ac46b5f8345ebf0e5dcec960d2d55a72d93", + "e3110bee29e6162f431aa68c170d622f819bc4e289f6c3e1d695cc444c8dc171", + "92fc6ae91caffeefc1e16e816a0ef299e17c63a968d41bb91b7a6f587d34f8f2", + "76d986dbb1668f0069867df25251328e1aa15e6f31233eacc856e61b5d328e6c", + "560b821428968d6304aa517c6b0df4469059a097171e7180c0bdc1de4c8c2dfb", + "199db247cd1e61526d67823aa89c7d3e9a849a1c48983902f4e732d37491ac95", + "f3000de6fbdf5997bcd5067bddbebd4c1057346a69164446e4a2fd0fe575461d", + "a36d62775f7abac51834f25572e05a0238de20394f0b8a5cc1f33b47984c4a02", + "b6df530ded76cd3cac5eb5f6516b7b5aad5ee43beb0876c8f1eddc38f4e173af", + "a939753b3d570927cc8d90e512239cfb6e52d81b5d56ea80eb4d0c46572d9ffb", + "915cf3bab38606e7266a5f7c2179ad469b07b134765900ff6ca2a5ab83fbe80c", + "c084076c1d75861928f6a28f566527ad2913a2bf829489f57dfc8947c368c50c", + "b71e574bfd980be24d08395d32430a000953a021556ffc4d7693190c00e74e77", + "4cb8e212261c1ec6fbc52df8cac337d6f5d189537645f7d5a7f911db882db716", + "533abcd8780f59049a8462ab7ef505f5ccf1d342150c30dbab5949fb084cdf21", + "db533c3913044c03977baf092f0156071c2628208d14d81872162436b87123fa", + "ca8ac0dbf15bef994221f78c0248b9a6408ead1166e623c6db7652a16c016c06", + "0990b3a317a95d36b6b6ff1fc6736fea01529a39c0fe06e49b7fd564080b6df4", + "787469298f138d1c490251895a85d6080e52aa1df375643d195dbe7e87847949", + "68a371372ba0f489c8ab8672e257f265a418e50c6c969bbec4b24105850c371b", + "f8ba30581f3cf31050db47b7a43a5551d13b9ce7001c896bb79409f4f1d0a31f", + "23a5c21151808ad600e97139099b79c3e1ce23ac1252db87b2d4cb88359cde4a", + "9886c12c93ec0ea5d14478fa4a9a38c4a1025f8b9d595deb371d6aff79f93b04", + "4e4f81cf1628b6f43db1db218773db5f7a405311aa7ee5ae2072148a867a1437", + "a019556336364cf6a38501be26e917b0aa2cc2c37c137fc269659aa44a3e5ee8", + "7c6b6a1324fc40c2373c6215ad699f723b826f02397eb6f6326c1a5ed558a115", + "bd21001934e50a47ade3d713cec8dec036d54da0a976ed3b690ac080a7bc9ac4", + "4db0292758deb6ab35aac12fd6387276b2f32cdc859a15642f01931c845fb0ef", + "5431a87a0cd32b11db5ced545effb22f220ffc69a60ac552844db86bc7998e84", + "c6f343782af6f6ad784f9f85c1123fba0b82ac07119a61ac39ed2fbd92fc0a90", + "c412d2dea34ebbda964b17cc12c1da70e7dc428c92eaf494e17100e9ac65ffa4", + "e9b947aba6755de52a67725be3849e3383e187099aeb1bef5315d22609368b23", + "0b2d1949bead9e9995b717a821f6e5dc3136857b178638350a16c1a32149c6ff", + "dffb51c4bc98c9f0c07ce18a88877d48947706cf57f56cc6a29ce82cf2f74c07", + "97dbf9a48f3c92fd4674b20ed6f635bbd92af003de523feb5ecc4758169e5857", + "cf701be33d1b3abd3357a74ab2f7f03fa78f7ebe0362994256020fb1fad75251", + "fb26930bfdbbdd6f69be65c3c01536e09a30cedb747b69719035e31636e47937", + "5725b8ebf2345fe7e660579ae94b84e251259487da4428a5f1f0cfec19db0639", + "61b92f8043e5a035b2144fad7b1df3ca7130c56cabbe0dd2757dd737719719fc", + "35a2f85adbd0b6a818d84a16d8ccbc0947bda389b6cf43ce6ee9aa9b4aea19f2", + "69dbaa6d8a1d44259bf095b55311dd39ce028ab20de30ca49b1a173d6171109e", + "3b68203efda022be691480952c5132129d514b2cdceb0d3a8f137595be7f1c25", + "388d3dd82eecf369794186b42c964871575c03fa9772d57d31280d678f87a636", + "6b493f049d19b3d1e268babf0211d48abd018c912787a913ff71457d54ac3606", + "53261340605f5e63ecea85eabc5716f31be1d16d1c91c04f3a090773b288e62f", + "7bc9e1a6b36320bf97e60da91091c0ea848653a56cfda13f129e42719623c500", + "7347ad263d90e20635eca6fe1bc89df06a4ec273ad50e1ef78a27fb9fcffca13", + "fd286c0a1d9e4a0e0aaf97b7f2da4b219e385d7a9afc4982698574d72ae49daf", + "0f42f9f37bf77fc47e1c75e0206654e0059703f2b7dd261f91719e49e586e7ad", + "5c1eb8efa7c26ebb6a3e2cb19148783df0b7d5dac0e80b3c793ecb556166a88a", + "0fd0ace6df8a7ba03aa6d52f78c3e58a4720a3852ad936bba80e08bf7c61a287", + "020d473e1c9504366a2204d566b1ca6c968320e30382129f95a40c739cba7f19", + "ff7051698eaa84a3ffc25d5dbcf923933a1a59730db33e8161698160a090a45d", + "df0b1d015cf77e47512cda3d49be662782e52715fb747cd0f73934109a066ff1", + "930edae2fb341f18849c9b8aace01d5ed2cfaa013f59647cb542daf33ee60db6", + "acd5d0212d8d1bed17253050038a481d968cbb8007c5c69f35d328cc887723c1", + "17f76ba6af39c2d64b95554ba27b12818e60284d166a9da60a3110091c1dc3ec", + "e03914821b7b6185e239eae45598d504558175443c4f34b700c2b329beaf14cc", + "cbde6cf03818faf19d93a082a509c3601a6b043a66a222d389cd44d4a91d38cc", + "c42abe41dfa793e60e2ab797e626bd8a5904e03633b61c513c9f80c2be9ef0d0", + "b47f9040a5561c94bb5c11ac8042f34d80e144a3f5a8b7c0e47d7aa3c10812ec", + "07f44825dbef5d2c5fb089411e2eb2d4f7e7eeef6697852084acc7b1f48aa77e", + "ab143316522be7e11b267b89677251a5a1d48ce7c90ee27a6c66e1857d71e576", + "3e03c42ebff10fddf113e731d91c96cba59d8d29acb8f6e63e0fd171657414d2", + "12a073e1548bd7551d477f3ffaa790d42105596f7c161a623650f1a053c6f053", + "4882fb1b192fe817b836fff3baf788d89442f75d3ae7a529297febc3ea896779", + "a3b5c64eb6d70663ce13bad27c28cfe46ecbf721aa12d667f3903e155f37471f", + "19283ceafa4e91731909817f264f997836d529c523881913fef99c04c23f636a", + "2c9ebde51c783478616c534f8cf30c1d9bcba627e82873678054f8c05f72cd20", + "6addbacf1779166778a472eafa5388de1e51765c9264db7f44297808f30cc940", + "33c39d4cf80eddc5b93cbf11f5cf6de02023709b71c3388dfffd485becf93de5", + "73d7856eb00d8f0eef1cc6ebab299384aba68e5e7c90e4526c4a33a3f7b9725b", + "f45968304cd3eaaf430b7744dac3bd2c287c51ab1418e02973fb2f6bfd3d7847", + "1e18a797ff5a58a2a3251358d7ea2e84895a939dd63fb5a3c77753f324b9c86e", + "3ff531919376e2ad060436f9d27129b7ef17fb8ff29cfce1ef3726833aed96dd", + "92a1626588c06d5a3d74a59f23e506460f99323c09d35bf5d49bd73450803f50", + "be288f4cfe34bc80521860940d76e5a42c85f68ca190a4ad71dcdc18c4198103", + "9339b6786bf2c8c1dd4830c384e03dedac22e92bc1aa9cdc3449c674bef72ca6", + "1b22a0e578c641613e3602764129e027e101e2d423d9fde2a6718a7dbd726eb7", + "c6be938d80860b8ccc96ba087fdb2a00d980808a61dc79483710ba87f1529dde", + "4bbb241fdc1feccd155ac9feb2ab91cbe714030cab20dfbf34318ae72ecd4a6c", + "706c4c5215f54df3c0c256e9d6c54de897716618824d1fc630d00d3af059352c", + "93e883284fa263b442a033252e84dd637a6943f49f9db224855327139dc8417c", + "689dc122cf1c3b550ac11e0041b45844b794a45c31a2896a4238242422b23819", + "1aeed96414641bbf27a32c1a94b5bc224947f87bb915a53dd2362a5baaac9ba0", + "0c0f53761b2c3f168b01cb9cc05894cb86ee53a02ff0c5fd5f160a8d0d15ae76", + "da8b72cfb9146b496da6719d87da1c52f295086935e1f15da60a86b24bf29b97", + "2389058fa5e0174f217260522f49d07b76c7758b0d3f7296809104cf78561b40", + "0a59439df43ceb691a13038b87b7ed5ed2d05f7acddeb2cc502d6df537b71311", + "ad9989dc2e5e4d1a473e2f30f9581ad5dd161f68765adb6d2e70c8effb23f3ff", + "466309e21fe5c8555a231e5214596e196ad44683a8e8376e453daf42b8085d9c", + "a6ceea7c560556cdba4d7b433400688ffeffcbf27bc7d05ea507dd2d4ce948c2", + "b77e4ed4bee931d43571d6f22f0ae253ae2dc63871c5e4845c6676c7dd3f0eb7", + "7ebef185856fed824ebc3f38b79f82746b2983f8106c2ca6a8b64e410040de10", + "e9a0458703175d02b8fce2b8ca87d2a6062270e97499130aa81aea621b42567d", + "ae17cd0fa9460c9caf2c158e1ac809d0baa510933aca2413c316840ca42d69dd", + "0d485a3ddf329e8b8dd47b569c7ba6faf17d4dbe280521a520ece56136ecc218", + "20241e5f5bea83a9efe54520cb4e18b2bbda3eb83b0875f44c62994cc2f4a481", + "ecd706d3f483600f37f3b2b268cf38033db1290e0359a089e7dd44d35300eb39", + "326a9ece9955f2f872f0f6e401ff4281f1992d9f22f247ecaf25a44f5661c732", + "36f0d608c831ea7365b0ab0f2d78d44f9b6cf0a3edda625d02a0e8db1250c644", + "fb476cab0554ccbd17ef87ab086fb70310f1b468e61050e4c47a9c003f71a88f", + "eae0d52bc60c959f73cbf3ff7a84f1005b935e3d71b64dd076f23851b61e19df", + "c8320f5862f6c97350570d41ac318ab729aac137043d0ca73f467eca670fd2cd", + "ad83f844f80d1bdc54559399d55f09160e3041f63bd85f9482ef0e0159e576eb", + "faca90225e0b75e9ecdb380a4f45937016f843f188f48a956b24601dbe2e7eeb", + "da4e04a00f8b7a2724c60b78a8df4caf6d3f65a8f5b8a86561ceb9933eb87a72", + "c17975d1893a8b62915a085e714f9c6f85d9442f7916ddec6a822090c8dc655b", + "7b7f3cb7bf35478e8dab8f33acc8746aa6ed70189e0daa8e893783deb55fc50d", + "d0311b9d76278b80d7f55b8c4bacaa8dca293cccea20a784ea9fe254abe93b5a", + "91d99863528e63d01fd353457a910ad683d11614458708b31fae2454fa224b5b", + "b0ae1e07f65c9dd764db153aa46cf42f68d780e4b28e642a4559ec90e8e3cc39", + "43fea848ab8ea34e591ac9c9f98783b3f83a812a9bef6339e81bd380d4d0b0d9", + "80695fc224bc3d9f16e1be9078e62d91b1769c4147dc44421531a213b4a69460", + "ebb6cfe7e39f666efb4e256af94b5b21aac14902ef3c23e6788ffb32d16da24c", + "ef5b6beeb6cf57ad290d3be9fe6790221fe4685d99ced85082f66cea6b2e2d96", + "a4556671f393544c118880d5e2b6e0b6a9d571325e35972c8bfa5ae70a9aec8a", + "0df55165d1eb0fdd4b62a25222e060b21a4e5be7b8388e29380b86b2940efdad", + "19b60ec413d47f58a62173098d1591609281e701dd079656a09512e8684badaf", + "52e4284b527671369be86b4b20545217254f85faaa8abe39953b59fa1557442d", + "8eb1a4325c5783a09e0febbc365313f3c4d35a2c1618ee6532572ab2ef1124e3", + "48a251c051cc0c8b051b806600f00342ab81e3aa111e066e74adc044502379f9", + "97fefae91a85838a8b93a4a8cc0edadc2e50b43c1ac0e36a259a40fdf8a4e213", + "aa1342bfc905f3f255c44aa09df477228c41977c060fb5f936fc6b4838c5be7a", + "b9e6594d64492899962352ee5c918c3ade3efdf92e16bd4320e0da27ab3ce2ae", + "1a5db6b7d6da5386f76ef77cd6ddc99ff67de917819afa746dc3c5378a95f30c", + "bf64bc337a886d5d799d839f03eee210acab95f613a17db30165d3e249d07e87", + "37b794fdf56c4ab8d2b4d00506ae67f02060625d55adf6c208cd2f53b6787cb8", + "7df41b919abd69e7acee85a552ed81023aad7a4f1773b35f104af8d253e9020e", + "71be6114d2cd31bea00baa69981edc59bfaa7891ed5849178f3711f37e5a9223", + "7e8908c6f9e14300d44c2254d022578f689d8c0cab56ad73ab97c748a71e7371", + "06a70fc0c9e0e0a3a67dfec26eb5367836dc932f20f65770cd27511f76a4ef7f", + "5f902ed069cac194a12c903fd9231b3b9b73ae499e47206179a5db78053b7d48", + "c4587b59006d177ecb76f64a083c8bafc926b243a6e9ee6ef12bf0f3fae367ff", + "eeff57364b4cee2e43ed7811728c6e0dc155c94bd8630567981c00e7983c202f", + "d9ed39b27fc2cd3e1c3bdda317c856557795363bac22e5475f58614be9da5f0a", + "5fe8295650a3ca6dceb6adda98a9b2a52c8ff5aa2b879dbe74dc479c14df783e", + "bb74a5396ca5059aee5f6973f760285858dc7ee84f34631100b641d8f48b89b3", + "bc357b63fb90baaff808482eb645d5523323bf52d98c3fe493ac8eaaa4773677", + "2d052d15ac1ce4eae8e7988c586779ca83b7f20134899d1a72bc05bc94c0b35c", + "ec20f72f981ef2d1397d3f05c0cdb32110e4c7bb9b876a9aa8296f5daeee6cc4", + "039cb433d2315ab0011d4f1b3d040ec7e1f1006fc3278b96e9a8e0491d1a9d60", + "2dcdb4800462a1c11c0e832d04a1c49d9f62103f2da332286c48fe94b456b92e", + "4a8b6e2279a7392cefb1e7da03b42371a678f84c856de77cf13ff9c0891d9b00", + "57bcdcfc0ded16c30a50204f409dfc81df4ad4b1fe673d99920d9f79a397d8b7", + "0261b6d4b9c1c5a585ab93951761c05ae9cd62c56359dd821486d2b1fbbb3177", + "2a71c8ea5108d9a459c6ccb1a614cc2546ddbd85efa1eb39ab3ce0a504cdb916", + "251698409786d137dbce1f4490f83fecedaf384809eaea793c3e688bbf61811f", + "2ace8689de6c19ba00afddca95b6c4d32b9929390c628b9339a77ed21dd9ec97", + "10e81239fd71cd6fc6a7b858986c7c4e1d064811ffa0cedff96ec34e3fa9f454", + "1981916f528ccbc3547bd0daed092031c4045e1d1426398f88298f7d29e61f8b", + "271cf2cafb0c86c1058d1247771cf90cd398916ee1eb4988b19765673a2c03e6", + "9d086afd05abd5ed8e554891229752b2bd98bb124d83e5c517c727a6e41750fa", + "5f2c7ea24040a74f3badd941fc602bb8ba2c2f3dc8a6f12bf3565579587b5ac9", + "0cd14ff3e26eb0253503592d3c2231f067c0d34c9a08478b3ccb6f9708e63bb3", + "7d8c41eb7c106d34679fda959f40538e268441214561488988384d52bc4959bb", + "ad844d292ee314dcfe7b9673493aa46fc09437a3215984514f7d6e25516d82e1", + "1a2a86d0c46010632d2cc42225c28e81d33831a762904c07fc165696e0b21f56", + "dd5549881dea36dc21d79af4fe8fd523ea7198354b09f11d45cdbc16f2f1a593", + "f163f190fa2603da18b5c4d6a8ce38135027e1e27765786dda3d4aea2a9cb80b", + "6e09fafb8b6e87d6189f46e244bfac72ac097f881fd3e20548bc9b2b77a06494", + "050e25827579544b11a0659b33779e36091a9386e10529de15661476f621b4be", + "2e106b4ee4214443a32a625a8050bfbe23c994d5d11f7c06604d67449dc4b65c", + "f46f7dc3ed0045f94584f0638fed6b4a1428e757d0286e64a7d09b90bd379f44", + "4a2c43aa7fa22812393b64ef4735c4ce9dad96b2324d06204d17cd489d7ce412", + "98862e2f2fbafaf00755c99a60aa0476cb449041ee71556e1e7c6442e149e51b", + "4c118375e7aac9cd504042c81c94f58f123924bcc2dbdd07efdf3ac283bd06ec", + "bebddec9428e7802423acb00e58d998f5daa5a26bfdae57697deb463e9e86feb", + "53b03c24b8c7b3c56e56ee1e708df3eeb032bf383d7cb1b2d3fd0bb11cb984b2", + "784badbf46476520b5c6635401f74e99ef7c7a003a393aa1fd57867f185dd721", + "2f3caaeb46d182b613605bcb8bedd3831bfd1ea4e7a041c4f76e0d4718aa9eef", + "1b3aa9204c47e2d2ba2657e13382926d2ead35d868353551eaf9a66a70c669ec", + "c9f2a7d878c3f80d7627ae2b832007dababd27fd2fc13709901faebcb14b2932", + "5a39dc81cb0b6518f00f5c66ac659dbd72f881a78e0c0b13dc88ade430abb90f", + "2f0ee5fedfa55f94832fb2744a80af32bdbf7708ba65a3e0d08b446358da1499", + "1d07c7dda8607fe8e06cefe30123fb4a38302ca4e091f5ee9df4013778e13fb3", + "3fabeb2e7e8cb221b62d4e11ded6d5eaa1db1380b7639c943f60a1c5c30cad09", + "51a1fd08466d6b37cbe7a6bab184c21ca50c9feb5938283d58d89fdc1035fc9f", + "0708c2dc87f7ffaecd46d3c7dac910700bbdd5f9331f262819ec9fc90545d3c8", + "2d8f59f27fbf1306ba8453c032a2d8bc6a18ad2d8f3fec4ec260af16f6213dde", + "3a7455c90f216f2692b108f4c907cde0f64872ed13e6f75b44e5f857695ad824", + "5f1fe505ad82e5de86f16977be619103b511b0ca09393daed964ec61962cb942", + "3decaf9907eda934c124f603d3252929ccb9b99aeacf745018f2cd612ba3dcdb", + "4d232fa8d81cb6abd1fb6806b45c069b7fa7011248f55d1873daa335312dddab", + "55000d6b9fd4132be58fdfcc87b55b28dbb2bc259c33d50e03a9d470e503553a", + "225d9a9f991b12fab890ccf06ee654afad4878a3a8c0bb3ddfdd54af3ba74ee3", + "40df7f285dccf966031f1e8815bec7473a748f37c9405ad98b0ccd40c87f986d", + "c4156b9659801913909aa5955548725ed2a251e9d3b3a587786094dc299a8f61", + "5dc3a95d6f96b946237a9977b4c1e05b24799729f03c191b084c6118106fd712", + "118bf0b7868f02b1154198107b4a86873ef471153967ec7bb11341caf70c4355", + "1ad3a5310730b0b698512c8c7d3e7a8552f75b5c7eec4546860bfdc622f75e7d", + "fb74a1c13b1eb19423dd805a4d85ee771ecf52e3956b51b0409cc5bbeec34569", + "7405d158d51c83cfb8dabb3201f692e3bc089f020cae6b62a2af2c88dd8f03ed", + "08f20e5a8dea1f3217c990bc112fe510ca5aacdefabd5a2a6958339abb88ad7a", + "dec3f7bbee141e55a68d1d164e891d9bf832cab148061a1551475fe33c1bc868", + "0a311678a6266de62ceb20154e6ba5ba5560507d58d8c5798474967b4924651c", + "b4f48fd17a7cd9e0b26e90a8adfa2f9a150d47e9a203cb89df114585645c87b6", + "26a5c76a25d02b0feb4b22e549ac88447abb8fe7c86196a73292f13d8eea61aa", + "2f41b58911b04c3418bcf76dfcc500d03a55fbf1749ee83a4e981fce4e32eb4a", + "411a18b6785d5cabf9b666a9a70cdfc5a1e29228f92f80b9f8d613face0195f1", + "b17d0a2cb46dcc365e065267bbc2cc36e92e20bc0ce24d144089d69282e6fa19", + "30ede26b25bcc2e6cc42f248386c61499ae477d4fb392ee289238c40c88ab5ab", + "f1bbaafdc5f0e00a5dad95c9ddf659747c74d093e1cba8a0e72f1db1d5cd904e", + "09e411ef140daba69cfdc2c3c32fd84cd572a842f53388124b51aa2eb1e40fa5", + "2c408b685afb2574e9fe2a212e92c638090e851026ac80c9a639daf234fdb984", + "a342b54ed7092264595569a37a5e5140a48d5117f62833de3dc1923516d5b917", + "b90af846ef17cbdfdc06399f2d6790194fd2d781babbcb2c3ba005031a278650", + "16c65a8054f7c7c6beedb172498972f2c3dcf0e8da240cc8f120dcee7439c95f", + "8979da949593c80f9164828107f04602d70ca494947d6ca7722180ca85c8c0d2", + "eb4ef64a1eb138e6ec7414b3d841d24e61d6aca94a870d3568f89bfeec28ce5b", + "8db828055ca1154a1c1b60b992299b2a3cc9f9ff217e15a329b642c8d5515344", + "5813ff09a79cd74b6fb3307c277caf7e80f3506afb677eeec65555b7fdb8bccb", + "e15883831714acb3d5c08a3898ddc6280114f1b41c450f6e93a27bb36e7c6766", + "95ee3e0acc204a6bd5f5b484e907056695cf5ace25449fabe87f25d61b1262f1", + "8eb0d5da9c247c962eba9081b9b58413b8f7aaec03df2813c77ce44573a3ab1f", + "afb841db4f6c64fcfda654cf058f0a883166b4066fba6be6df5dad564da30515", + "68fce267894ffd76f551a169c9508e53810bb12e773015aa4c547a56a5806fee", + "6e614d1afb783e6b45301b2fa03788a1fe27f2808cd2b513eabeb1226e6241dd", + "6bb0cdb7d8686d79abb5453c3f5045c1acc2e268b3f2b32444e60796a18d4c12", + "9b99c863068f24297df3431025ec6564b66f2b9f64c1678b528a1dbc4bf71b16", + "204b6f2f3ec5bd0f1907fdb460e4ed6fb0d25de5cc12f777a1ce10aacbed40c4", + "615fba5e47ab0d959fbcef39996936da656e2615b954f03b61fbfaa8e1a4f500", + "529895a7de063cf6e2497d30d849a8df3c4f7339e22a486044cbb3e7496fec22", + "b7fd2e4c0b845850f044f32c5d5123af9d5eaaa693d4350c4ca8758bfb857b19", + "059e015cff4b33f019921f9242533cb45cd65e507ed882205367b746b27f4442", + "1b79153a3a25bc82c7f52a577c140d85854d76b3268e332b5871a4de4eaaa47e", + "b9730c36bf7402a5d06c8226dc11ebec9c0a4bd6a23762a6e9e78d1b996a4630", + "d22427bf91dccfacba55357d4a8038f797eeff2c4dab56220cb00d9ae897b783", + "da19f42f6dc60fcc3d075d6f38671949ce2dad31b37496a638d341757822e92d", + "78132589eb98ac63e53a329f7cee32584dd447bb9fbbbc01e27245ca29c23a23", + "e467ea63a727b9c36e666d435664875c822f94f047f60ac0d89f2733bd4d1f01", + "f4f89149b1a5779dfdfa3a9b7dc5b88791deb32c20db6d611fa013b9a2eade6b", + "7b40c196aef97aa032f8a1ba0b88aed12baf4331acacae6513e8f4fbb102dc98", + "dc87f4206941dfd0431dbae606358b03741e52c9c9830017847042f99caa1a89", + "293c650a8f2b442da199ba6dc31cbc7616c09bdfcb685d216fe5f6964ebdcc19", + "cf3736739c87082a71653540be254cad9dd2b80c788910ff51833053de0f6833", + "b7904a9d282ee147c41a87ab1de0c68f6fe6e8681b728a50b07dbf82ab61990f", + "ff43760bacfbd8a86b77ee50524df729bfc322d221bf39d73fbdcc5172be25cf", + "94b19511a901d6c9863b4e698bd3203bb45f459a706f17d5fd0217b7d4d2e880", + "5fb42d90123e7d17bdf84717dba17f8d6e2e4cb9856ebcb5dbf6892fd227d4f8", + "d403da452541afc98ea45671522db8605d28a1373baa96279149d70fec56e8db", + "c515c1f3132d7124546c2149d2f96bf8899954ed28db737d81149cbd9f84ba87", + "7559f506a7dfdd3ac7f13dbc616629b56b88f8e43e6039205182b17136e45e6d", + "6b3f153c83caaa5f7bff608f4fbc514feee1ad49652691393b26e60a3860ac49", + "b25dc960443d800d107491ed43a224db6df66c12b02b7dbd3415ad4cf38c1f25", + "a70e10b5d457c0c10ae765d7696e96d1721bbd0557386cd00db27f7dd61c8039", + "c12df355cfaa94c9154a29848e53dfb2d1bcc7b68cb1506d067f5feebeca2134", + "c9a697b7434305777c8068af8b5bfd654d74bc5ad82220f84fe16e1cc9cb2173", + "261dab494a2e8adcbdfa76b171a1dfeae0a70cfee528d781883537c35ad37546", + "df2aababde59eec1b7b499858984c1204557f599659f05f6671d1d66d2273d68", + "36bbadcddfe80758e7b91dc8bbd2d2cfca89aa1c50ad41a706c99bd192c1042f", + "bbbd1e0d041cf1b340f9b0873d876ead1b76c60f593e8ba82e2d700309ea5b4c", + "07708bd4113b5e2773ac0d65323a793e5dcc366b1260eb55f6ed80f6fb3eece5", + "b7fd39d21af1c3ff30225230a86cb382680bab72cd83c081495df252bbd31dca", + "a01f7348d23cfc9c72d4c6c8d41d00f25331a9f6b28f704e314f46b9a5fe8836", + "e14b6869725e9f7c8b4d0fbd88d1cbf9a969e800bddeb3b426ab13a9e1c6873f", + "ad16ff69baf5690783aba8de17d09a92af4fe62a985e33c0969ed734d09bede2", + "d3edfb7f2ee7cd654793fce245666efe24655095b9098152093b3e5a12cc41f7", + "50cdf981025f02190aef3c09b5aded26472fe5b155c689acd1600b6f163b0aef", + "d944eee476fcd32ecf4d85d5796e17ec2adca8f9f6ce1c1f66687e2d99a29f2c", + "6eae57da9514dd54a3f5920e1891529982d88c17e7ad6f6d8d24c731e1507992", + "ba0b8bdba07211878e29c71e21bb5aba5186e50fd45b4303f846897b8c409ae5", + "16828f8c425552366cedc41a7ca7f0df1c5e5a9dc543bfd9b0df65021d087a48", + "338222d8ae91833de9008c65cf519b1d021c6072d09cfc08f78d002300b6dbf3", + "c0a24805f980cbf60892cd89af249fb06c079c694e3cd84f0cba7f21f9e6fc9b", + "fbdde53f5541ec1feec1a381409d5ec9c6baffe97ca0c7e06f24e841d2b1161a", + "1752a73a37c14d5080efc1ec13bb1dccb9bcc0281a4d95cc2ef4b853ecfa6c31", + "fd2e0c2b50b472b90d7f4bd119085224219ee533a99922ebd8cb0f2cbee9cd7e", + "3a1d28fe96af222b8bcbbd2e937ad0d1f4128c7de8c7ae4ddf7015b07a1f6bc8", + "7919fe4c1265bc194272f2d3e0a0a0dd4853b9f318baf6287484549b01404b68", + "10f6bf39d6329af3916a571ea4ae1de70cc23a9818a866be45c267284d3abfd5", + "cb0c691d7266460c4af283861919c3ee427c09831e70161c0b7485b3f420c051", + "69d7916a1618b94a9794dbfffbdafe155f921b57b40c6a55e8ca17405d1183b0", + "0c59dc8c3caf16b480847e086cc53f4bcc55c0ad0b2d6954f943eedab457c86b", + "8accbc6c5c4abd38dd2574cf0a84286709bb377aeae9d50668b2145190ff94eb", + "66213a6bb4469587379b204233f5c4cb9213c9443c6ac8a8f5748f339223bcf8", + "1512294a5bacf0e03dd013bd22c0469b02c6a56ae6ab03d089d1b59f9bf47ba9", + "a5ed327b1392f514a7ca393657194e0cc1f5f454aefdba63e867a3905c9f3ba6", + "af21c0215b6b801617c9b590a5bbca4874f5a23e7fff46e2fc380b4e501b1b42", + "48d97a310cffb3c3f576b5615ca7d30491461980e9e6ddba01e9ab8b69c6fb87", + "c23b5db245f1cc4791a6a7f57dcbd3a72750431e1a67f5f783668c8a2825a0ea", + "ea2c2b9190a3a2e7bca55d33bdd1566863284587cbcfcdad74a617a571d006da", + "f00c701d6e9eb10e8ef79276cdc154e8ba8c4422f1d08ff330ef3f55d054d337", + "43a9d6c9ab48dc22ca3c9cf8250db069e7513537e53cd632424035f96371b52a", + "9668811e88849fe80a78b79fe33a05252ec3274a8c4c924609b8c85281ccd46b", + "f2f93449bf57bbd79c852a412dc55922802044e40a10d5b70b940a585ee720ee", + "d20bda52ea5a7e785ab94177f1d90f5688d21b3968326e9909c8d26cfe160b6c", + "1eee37e54171be9e11fdae188b2edfb0bc024b5f94de55dda4b175596d449bde", + "0af11c745a8c4891e888eed30309edc37748d712e154a078d4bdd5b13cf220b1", + "15982ded9a62a5482a3acc1ad4c33159fe6c42f09aa14365a292d1f8e4434861", + "429e21c28b9f0e415e2211ff953a01b408b9399dd2b67d0cbf212653aa9e3de5", + "9b21c3118ea6bd778beb58728a3e319c2bdc18fe9bb2218024e4d7d537758988", + "730d819c74a968d95a749664b659fece7f28f85ebb9e860542969b1f83f7135c", + "07734cbfaf9de00aa8bfdda5f205dd887fd7df72535ab729d873a5ea23080c8c", + "472f6c62614c64e30fa642fe8370b2108fc6c624bfc3652ddcabdc943b93d12e", + "41161aea39995afac088fa514c8df9905a21984920ab7ed9f5ef52f603932f4a", + "ff0ac19b15cf71021ba255bb88b9b48b8d813446364defcbd3648719c0ab4a4c", + "d9836a3268981bfe94ad1f77f2b3ced20b7079f09025391357de906d03c40e83", + "7e55b991f5f34ac4389f5574bb493fe43493e65668918329489fcd9e473eadad", + "d32bc70e6246bb9345bb5efb2ffb68f6173a0b675a8cd4c7ca74b354adaa2909", + "b9d8bbf671e794da22b3d2318c9ba690448e3a41f406b903f638b5025f7564d8", + "b28a5816f46942f4270bc625ec876659d37c5e40aaa913a6223e49e7576235d2", + "43781bb2014fa51a94bb3498c99623b76eb963f48238f9b1df9b6f31322c0970", + "8dc87e8f4df3ae436fae06c170909b87542352d349384b9c7027e950b402f65a", + "339cbb45be7f44b72f31f96503860c5a25c5f2abde498a93561fb59b433f293f", + "f2e6d476a7d47acc13d2eabd4d309398684ca739f8bce57eb55b1e79f3d1e519", + "56af0198847aa027beeb25867b66c211c78fddbf74d32c1d833d87c778120fac", + "9ac6f60c776459541ec6bc8801fb2fd02e36585debfcf3212fb0b1614ae2f6cd", + "e1792f3d1d3d161adf761720889612dd3cc96eedbbe2a432a03f7539b0cf405b", + "0618cceaafb6f8e91a6c592127ea2b5b0cb1194c3210c3d643cbd25e047a2c5c", + "c7deb398d09de0de32c114870e0843d7700252917eb69fb0546830437308e970", + "e0ac7eb0b844399d04bdf65c93459a19b00c9a23bfbee05b3deca40a05b9d389", + "23a16f8f604e45d067ae5a3b94755fd327505b4a0959844aab30ff5808f79b46", + "488b6589b506e1a4501b2485091e717411867900783807af78f87dff7a46cf0c", + "56c965124e66ae2c716bf67050bdb320f38385280e7a7c6cd51bd7f7ce43d32b", + "19edef6e3e69990ec222877913340628049da39409aab79c93af2b6948a93d58", + "128eab2b73e6a6e9c535e15b22559826c3e9d678859d92653a5b0611acd12d52", + "b60504a4e6fb685fefeb7053d02d89bff2c41c86795c4061a89d0c3685da8e84", + "6038bf6c7728e0927950e6865113a34a6646cf0ec0a2d15e1f1015b87ed6b091", + "1bc443a013d98a82943adba3b89cbdbbb38d8aabbc0d4df57d218717d50f50c1", + "9765ae2f7bd51d2631470a84f4acb6d9669f3e211ba7b3d1480b80a0cb53dea5", + "cdbb6c307084183baa8b44cd6a93f014687b2e8f6aaae063fec6eb2c61d3a331", + "d386012578fa5b91e761a714209d38301129c8ca4d1d393549a4801a9b1e0094", + "05387de7db27a628101147c5e01cf481a68866c5f4be570f35a50e5a96dd8976", + "ad2775261f5a12d7fcaea9666b48dac6ffec096d643df0bdb53dab400abc9fa9", + "c95adaffc299856ba5bdceb376860a43c0a55383ccda166614af9333986f18b5", + "2db34fa42ab7e9970883aa5744fd4ae926463a3acbd35ecabd5854f369bf3f5e", + "068881adabd87a5d568f27064212ab1bb362583ab9fcae18fdb03dbc0f3185aa", + "9ef7cd94ebb5f2b74db7933a06459ad972f43f21260ce84baf26904eae9090a6", + "99afd610f6087a27df4c16d1faa3e80af5dba4194e7aad1347dc282f493951d8", + "d036a9a86f34c9eda2f4d54a8f9df4c470c0f97695c4e5dc9de60e85af56a1e2", + "7f0137290a2f6b51638f1a439091c26c66ddd7afc9a8525c86d0490952957f5e", + "a755f2fa40c875c9592313dfad65fbe58d17940dd1fd5ffc38f9520f91b8fa63", + "6690697131f9aeea3f3a74f83c6e34475cf3c1d6c63e1555174ad875a908ece5", + "26f83c084041837eafe3647ce38404a2e24b2c3f5a870fbc57ee2f75fe863156", + "aa90255d432e68be319e26ab5b1423400d2013117121636cb27ec494a887ce49", + "72a7f09acd5ab6283df40bd92802a67c66c5308f661e98f3c12b2dd3c26817ce", + "de85e7574117c4c773bec442121ac01cdcdecd533946624f5956258644eed440", + "51e4500dcd4477c425cf9276a556041162c6d9978d56f394d32a10c963f3049e", + "7a37ec2c99e082dac260c89e7fb72c23a1f958c31e50624217391cb31b715ef4", + "1611fa2786e97a786d6f0a9eed8bf4b9a30a2158c6b9722eba150b4d53398e1e", + "0ce07e0ba171681afaa50f91d3ba9e1c1801f6c9d49d7d695d2bee3ca563dab2", + "4a9cfeba7938b7c64cd17a04e6191e9ee3b07eb39720f76794518b264817eaed", + "ba2e9a35c933057f7775202da766bd3686c4268f32d9287d8cd3e0a371002d76", + "41e92e37ed2717cac094610c3da3c64079d81b0f18adfbfbcdc46860082d05ba", + "1dd6a4446b8615ea23597f2f1fb4f01e8bb999c5cd098099694dd2d9fba09e71", + "a32552daccda15299cf311e0bb4c7ef3378f303997e5b2f3892cf3265c9aafd0", + "4a17194413577d14d26f1c1fce778081c7cedee904af0ed3a25abbde34774885", + "2d5b0ed95502bae6f4bd6738def6edf4e7e351f879b8c034646fa7421d0bf675", + "0c32676576aa6862d224cbc313ee6576fdcfb71c8041e83a789380c58413354e", + "f1c53b8e6989c4f0d55a5a08239393a9b8a5517244d8f04577300048f9e21e87", + "4c6e479e828fbd26116908338cf72758557d523d190b7cbe3c33cdb00f91f281", + "fcbab6fae7d8bacee71d82f2c1ab679c48a85ea750247f93478baafe4d8682f9", + "f79fced898dfdd08f18178825490803c4203a029faf29cbf08d8f1602f032430", + "d2f0731c4ff11f4cb6234e6f22717d0a2ae395409329cac53785269a7c26ec3a", + "535b3030171da4c461763c488d7da9e89c8cfd7c1c2dadbd360bb99edeb9b0aa", + "9f5e861a8376f92be1917f3157180c119f5b2bea66ef40bdfa77ec8f6136df7b", + "9a7f91c1a05612ddc81567d6637a6279b614dc9fba3cd0322777f437f6cf11ee", + "3e1474061b17403e96e750b3ad725b570d4ca96f98f583e0cd5dd26b84877cbe", + "c046e99c6ceb99a1ee02f92bf5099e2c44a5adebd5290dee742cc5ae9da7076f", + "0736d3d6b42c0b68cea5a9c774183db0d7ac138e6303036e46da245dda405f44", + "2677862a927266eedf5f926a1844495ed7f31f671dac9f58996ae17c637ca140", + "df622da8693db1d4245400e8bdf8a28190c1d29c27d30046f9fd1178a7554c67", + "b0adea932e628a977282825083c96a26a276dc13e623380a45a590fccd543be1", + "1c54ee5a29b8ed8013b00cdcbdd654272965ce4be0e38d7afb413e65cc8525e3", + "62b605565cc85abfa0861daebbdb0b13d58732e59bc1163d9ee5b411fc6f7af3", + "7b7226d49d687fd471e0eae0f54d9529f330d58b40a255aa871ccd36c8b048c4", + "9ea175109678095977395a2337781a4b7a0bad3984d030b6bb0223772487d27e", + "949862062b71287fb7eba6eb72efed9a06402d4372e8d4c32de15961298b9455", + "4200d4862ab89d7e11f465dbb15e722eea2ead1b6eadcf55eb76cdee2dcaaeb2", + "daeb4b3ae41eab84db5c30d4032541872bd9910f36c2f3708467b640f3d56217", + "8340ac58b1ea529e8f4f0bcf297dc402d89f412252deb830d9e6ea626ec2221b", + "b1a09ab896bccb3a2b2dd487d97392e5a3de1f2a8937cb30091a5be8320e43d6", + "772812821e2409ab4b668507b3cf42fe3bef220d4970081db69cfbb992c54a0b", + "af59ff44fe6084dcb0f148dd58429a66985c8f445d58bf7d734ee5d6cf80002c", + "308def2d531cbe674b75dcc421bc3a5ef9d1ab5c8df737243643b2692a7280a6", + "e207877fa88deb5ffae9b19ebc61d0ee6453d6c7bf911aa01263030cfa64f8a2", + "eea9b387577ce6c76eae8f5d2d4fac9de61dbfe4b03382024a752c7f90a8e366", + "ebd0fa6d638bbf5ef8948054b2c2424d490b16dcf19ba35801c25cb388a4004f", + "0f41ea73a5debb480585af132f3910f3c7b98a577fd595882121631431af470a", + "4dd11303be6ed6afbdc8a9017827c6fbf2fa8ea263117a9f08f09284559bd3a2", + "77dce608339c3e4933ebda49d99d52c3eba9e585f81bb6b94de1eeb6c4f19aff", + "e6bca493413498ac5b2ecb083cd17687e02f2fbfad5da93d4c31a02c9eb72c81", + "4b7eaef4ba7578f43422618c729ce5b1d362a11c437f4d9693da8eee9bbe6b01", + "0fffe2e60e034ee0092da0c22f560bcdce5553fc95eaf8478c3aed2d9713dcf1", + "d7dc0f79e6077711fbd11be2c49a57d984dfe43e2357de902f694f561cce6691", + "51cfd0c3aa1b96da6be92c65ef7dc25ac5f10236b515403cb7f8492791f3bbc3", + "309d74e89ffd04cc8f050dfc4be50085f197f6c23ac5f84c22f79a2729ca62b1", + "a139b028838d489dce5fa08da18ba5ea10969860dd813eb64c755f9f8a0c856e", + "2987cd385ac6ee4a84b0c2c472fea3828306b7addd11cdd28aa416ad9950a9f2", + "97e9199855dfed3d4fc6a8746ed9dee32586d73066e024e8a96bb124a187b3c6", + "d5aca4fca7529a7917cd26c7710c20eefad94ab118b592db8fa22a4c258e99ff", + "08a67a8a0f262f70e4039e10091a296fcc196e9520d5ac37f836ab109205181c", + "e71586f3b63c70b6d65672a7d84781aa67ce0d70f41e4240f56066ab67d1baeb", + "f2bfba19ad8573d2c7d48527ab2cc275f91fe08d82829908013f53dacb9478e1", + "8af272ab08b9367911a97394a580c59d6e6ffe763aadc619edef7c3b827bc927", + "8980fd4836c9ff27cfd5a999fa5a535ca557e32adce854752addef70c4144aa3", + "5d976c7296257e3a0b8411bfba3f372f8d35bd7e73ed92bc26af88e322177178", + "a01565d4915e7fb6689db3f1fb7fbddfc9e8647381c115e1c0a7b1490e8f755b", + "a8fcedb1c3ed358fc5305bdc8b618bc0f354781472eccbb8960c8ca3a0f3233c", + "094ac7a83a427bfab8eff10ecbce65c26670bacd70c941f623ff0298d354edc3", + "d7b2b22315b3ee15e124dcf4560d3bea585842f4163daf0b879a8b0c55390637", + "e6e8072a24ab7bffa9ce6e9a359c33b210e8b66172e1d35a5c8ed28871c40029", + "39c1bcabe1c30d0798d8bbc275d83a503299b08fa2935552892ce03bad2c0bc9", + "2ce057410a13bb152f276afa305eb94e0e31c5dd82a2eb828d0a01710a276756", + "86b8f99ed5240df548dcd85fd87fb2dbc78e21e117524eacebe7ed88d7340a8f", + "0ace53d368852d27daea0ec59bef8f1d5088ceda03719add6a02bc70e3aac785", + "48c7e89b19636c77e186a093b1f2946ddd692f964ba6f2d2ed136b8c2f490566", + "6d9e0dcbe46da467509a5a8a2aa709a31d47795f9f3bdb3a01812ced98dddd5c", + "1526564dfcf5b6ae528444ee7a260771bbf30aa846da37d07dafb1d743c9f68f", + "2987aef1d875722ea6f1684883eefb0ad566fd0017b7ef6094a1752e2ba68550", + "22bc33eea0b2fdfd447021a90c90d10d24652add142991676418a72e8151682c", + "0d72148b5f75fe9f6e36c5f042244bb5e35a27b62777db7ca1e1072eee9a8e48", + "ef71137c514cfa65fa5fda9b517e3c267a9c7a7a7fcd4b9177dad57d0fc2add5", + "11efa0f3f84cd47c1cb0c2d4634f317202ce00e52fa18468910a68cb317326fa", + "750711ec221178fb1ee7549333d5dfa060283226953f9096782c24cf78771345", + "9d621cbeb49423a46c56581dd621246766d6c1d2e885f38cd36ca18f88c6966e", + "e459b29d49a394e84000903ccad0edde2b83096f73e93d144cf8a4fd041d37bc", + "902e39fc44a6a9a712c9d42e0b98a08f793f9eb468ab13c120c87bf6309b68f6", + "5fd3dd562fb5d0c97a66fc5d6f2d047b7c12e9518b95d6d4b96d3cc649e893c4", + "ae5a35203af76d78f8d4c506b18fb4e1c67e5798706d7a1b98912e9fbf6669ed", + "8806e940bc2274296217b392b69987b4772bac5aa5a96980a848c1ec769bb8d5", + "bad155b7d45404ede0f07382e8d74d344dbbfe462c8b3658dc6ead106d418e9a", + "fcf3012d4e101fd7affd972b1768ea8200fddce6e0fb7e403e4fd9ba3f01bac0", + "8ce68b64c7b5675e0663e81e61c4b4379cc5f8845dbdf191031b591a0e10914e", + "ec2253aabaa2731500b94ca5b60c8226e405ea23831e87882fa127cacbed1e70", + "8333c77cd6b63b5e538dde44a5f1c37822c5e1259d7c6674ebc98d17431720af", + "abecc9a68c658a5c31008c6c594d581d36fee313f8ec2aaac469f2fdf4525569", + "e4b493bfaeeb85f35f83e84a318b3a1df7df1497ba1b03b498099e747956ef52", + "8dda479f521d6dfee106e861380824d6d06c770d1472a189bcf251a1bf0a0f55", + "e38a654a52e216ff4a200af142e4a8b9c38c448876b70b84dfc9de5211ca7024", + "12b0bf0288ad6eaad0392192143f5765bddc84d7c99c3ce7e33711083251e878", + "98055e28a5af0dbe2cdc674820e18c92a38ac89cd2b6d3e2f87a589e1f054eab", + "8e05d03fef5e1e2f69f9d4fac674d82e351e34106ad29073ae9490e9ffc15b25", + "bcbb675310f133824ce336a3c43f36f8f6bc1dc9be8efbfe3e7270fa0e7eb5af", + "20af3852b4aef5ea3fabd3847cd03c064cd822b30b5c1652b91b414d08704fb8", + "cc18a3362023966193c072630da0afe74cc08d9eb767866d85c557e6a6f26387", + "950e603915e7fcedf0dc158daff822db034c0a240f7cdc5063bbb428d1408414", + "12658fe8bebcd857c22f584c8e31d6172a87f089e1d883130b7dcb4c1600518b", + "661d8c02f86e16355681f3a682aa0efea86a985ad29e2b64b343f5315a9268b2", + "617a333e2cd2482f83715845421a1e25506724d4737f7bf8dbe9f3faa5a94aa9", + "1cf208a9abaff984c259300f9345c92683eadd83d11a6a4024ee47b3b4d1d045", + "db15ccc93275a34f2728c92076086f0f4c7b2068fab2b009adf7f0d48bdb888a", + "befb08a1da6b7c3136dca3d29c7c0c853c2d91644e55e4303cfc8be64f699538", + "ca3755a1d62e3b4a74fc458daa9a7faa3316fe7f6fb40a62282e9128882d4675", + "4fe123bbaa5d874195f63e85aea06ab08355e0e57f2204308439917ffb080a92", + "cae10b8938dd5806cfe5ea1944b39296a0a73b5957a82f308d9141558b21c6eb", + "124dfeb77b60b21d782aeb2e4998ef15ca8755a3809fc3420b1f4a7125a1f4f5", + "406e044140ce58e8cdf36d57a41892fe4b15a16871f8318afb4ba0ce43365b5a", + "59f62af4c197b3ab5b58998677a39f42ae62738fb962f775529889cd7db10925", + "7eaa2867fa01e79b9e64e17f41df246f099a3f1ec05f9feef690274f55b00182", + "3c0125bf287ec0efa250f7dfdc4f784a4b11475e7bdc5ecccbf7c6dc591d0284", + "06b9701560e87fcad9f9f9eb761b9b78bed1eb979bcbc252b9ebaaa465319a2b", + "64bf409c31578efb2717df97ff8125c218e5b2740cce771186288de418110e2c", + "91adc1e2ecabaa94988334b107b23a78af49d6cd6a8a7fa45dd0a3de7e7a5bb5", + "99ae7e7cbf973d9eee35901a95212ced0cacccdea07eac0d88b5cd29e980b956", + "563e70183d98c899d0f0fca96634f77ec89ad4bacb3f52ed2f736555149779e0", + "7f01f980bfd6c3c60f3628d61afd9bc5c92f50998b3ebb0813db0487d2c23f56", + "aaa5cfc4028fa4a0b2f1303372ab35e5c4e4a6d5816bd6b819926e8949a4bd73", + "52968b2efd496a5de2dcf26a6502e8c6c74dd1249a40723a17e84bd331dae9b6", + "9137b4da3041a7e4a3b88deffe35f371c0d8f746867c9959eca986d4a24034eb", + "1d81a4be4ab49d3baa09a94794061d770bd224d3701fe00b76680297ca819bee", + "628cc2eb26c7278894ed8ec317b4c60afe8ad5fda359f151300e1501d6b27091", + "cc9378c99c54db8ea7cfa4261e74349b30e53fea0232dfc65e595419b7311c78", + "3f3a959ed7bd378b9ea2822b166760ececc99ff65c340e9a99b550b4e233bc38", + "234aeab4fff850bdd013574828def84a96ebf53a59187fae764d691c1e16fc9b", + "b065e4e7d29530028fe92e1f48fd4944cfbcd4a4bb1aeedab43866b694f0fba5", + "5df39fea0e235a747476259f34f8b66d739f9d7bf47f50c19891222fb66ed6f3", + "67fef34becf3a89191220819e985761682583e9e2fac7fcce2181843e567b13c", + "587293d74305af4ad1bcc2d88f6ac16c47ba265e3ac5ffa5f0f6235ea8cac952", + "36a9a03e9fbc0237bd52ecf7fa23626d8ac83e91d54cc0471cc9277929582879", + "78a673152ae87540f2b3a38ba47ec5bd5cf047da62cd6be3682a723bc87ee1a9", + "8b48afe535024d84514d81fdea58934ca0f23e031565d7f74d05ec7a7d946801", + "d81ee7e407ad1311d68f0b53994e6fb68131c596735329f76cb4d18c4ddbfb77", + "16be4607f8bee1e191857372b0263ed401bfa173f2752736fbe50abb7e58da98", + "d05083dd5e6d5e0a2c320a1752c724d3a13428b9dbd2833fc67c86628f6eb1c3", + "0b2f6370271a17242d09d0292a1e0344d2806cc89e8e6e125c72ed47317171c9", + "aa1332229c78544ada666a1e7cc54fb5fa31c586370de5388bfadd86176b2250", + "d605e3d0ca62ce111f73d2ea48c5fbb56c5d47c2bc28eb68c398515703119072", + "9e0549f039e6c530dc1432adcaa40a3ee2d69329fe1a2245c6c0461c16931eaa", + "cb2f3789ae09d724f9eddb3f104e755ed2ddc3f5bac87aeb91f76fa53784e881", + "6e3b3835f989decf5441e8c524aa95b6b312d5732d614fe24d80f9dd39bad1c5", + "318440cfef57053d806d6f9c03c1331947018cf2f479b71d9f5ff4d1bf940ae9", + "51050b2569885e0a61fc2487c8428e6fdb8861727de7e8d97f7901e6f583acc7", + "02e37462d082a033c382c04ec6d90c629bd96cd0379eac6399a718d9988bf3b9", + "3515eaa553cf9f35d85fdeb367eb3eb930a79dcdefcdaf2e6eb00c550deea9f5", + "1321c47c060f371589bf9a1dc6557b9b49434a4ad6ad15f6749a2bf71e26ce14", + "627cf526b8c336e3a3d8c3e82e5503fea6338f14a438e8de6bb3dc5674a10da6", + "7cd3b2289dc1190b6ea21b9d011ec5574fa251d222af63a52bebefecbd89d7c1", + "9bb3ad4efd34c6839bf4b641d1f745d8ce60c829f22b773de0fedd566c3ca60d", + "679c477e999ae78bde65ce99acb6c2bacbcea8214ba4b7fd477ae91b1d21792d", + "bd3ec90f50cdbfa74372cfcee0f017fe7342935eb93d9367ca277dff7b02eaff", + "6a73b3ac5c74db3e940df13ea5e049432e2488b3997767dfd700e9aeb34f9cfa", + "a642e0ef2b4c17cbacb868ec0ca7d43ae9cefee5e0dc29e59e561b44aff05615", + "1a77398f0d8673a9fccd3342338f6e5fc1e96800beebe04f4aba48afdd41f6a3", + "d2bf9e00bf6050ad68e7a3ea1261b3c9f9b6394d53da2f9f1712199ff5fa96ac", + "d17c0276250825f6c8ab121c999c61cc892874488a4bc95a104dc0382e511aaa", + "7be2cb478a857b81957d7169a01c6692a254e7b029b42995b6c262e6d30f0038", + "2af54e9f62193195b0b927462e63a661c5abe69fff22037e047257dd41089646", + "120849c155437bbec39e71267d3320eae464e949c7ed1e812c91f3c5ead6ba1d", + "4d77309ed2522ec89b7e8e0f0189cfe9124babae465007ab5c905a9541d63a4b", + "4eebf5a0cca448832d9a5224a39250ecac4a18c650baafe2d8804241c274dfc1", + "5d29b8c14ac83e531b87534ef17531cfec84f49c764e6410c8d5682fd80204a7", + "625d9a10f136b103e85f0e7863df74db8049f77a75e52361748873fbdb2a3f73", + "eb21a9d33e58daa0ead2dad3a71d4e5eaa7b0321c6cc2d4e86464c70cf366634", + "6b9066c0d9049b0634edf92b34140d02a49e7a6d76483d14c32125f3ea41d8f4", + "b68f5ebb20554bddbcbd92371c4e536210a0674e61577d552b6b405939acddfc", + "2f8fc98373422a6dce98f183f25488520794b45b947c18ab0e1b924dee353b61", + "64643340086927f78905c52784692557869bcf723b2ad19b69fd0349601aa7e5", + "39c72e17874ba3a3850231ad77319c564e5fc1f23414c80d0531f8e618895ac6", + "9e2afd30de5bfa30e237d9f2f773766a82e20d1a7efa10d34e37048b172d2e91", + "c0888a46377fe1194a84313d3e9f64e90cb94648cbadc93ca1c077960b2946bf", + "c7a54836aa914f27df8424e40d4c42183c7dce32798f7964fade258768beaa4d", + "485865c169e3e60c2b0cd97fe7caf04bb5592a9a08c5abf53de92f5fa745845a", + "77038185b0024d9e2e0881b8d57e4dea0b59fcbd2bc2a776c8d7b29b5356a240", + "19be080d3f5d828b2826c221f35a07200540c47c5bd5c374175f8d1b55dcc8e9", + "32d5a77433130085014cfa02eb0b0ff36be96763b8f100502ee09a93fa4a62bd", + "9572c95caca053688f8cde1436a7d5652f1bb5bb4da92dd200df11d2a27da378", + "b3db618f9e7680be0c485377b1f9e874798b5b9cb2800a098053b025c47e70f6", + "67a77a0cdf101d5fca5d17bf2b49f3c00632bb4a53c4ef06617762f36c8c8e07", + "1fff3805be7779014868c91462838db4e8f88e7759179f144008534a1724fcda", + "efd453c249b898711874f643c4b66efd58e91ad5b3aee1aa6ad7a477b7248a94", + "bf94e5f1c7b5c8483285a8dea4345e7519581d486fcb27a8da43a2b48be22346", + "7ccb34279395a288bc3ce100245c6de77fd9fc35956f9ece35e6475bbd6f6ef8", + "810ce536aa750b60cfc6acff3ff65b36515f60c1abe5816aa5e38549b0cf21ad", + "be1c2775730ae524f44e05fc989ea33b24d262db5cb630cd13d56a2324f8fe37", + "7b8bf85e45285fa4b800e544da002c0c43c2b87644dfb62c42090631c5abd1d5", + "96e286ddeb65c5e54b4a8325cd695b53f4aff1fda635ce72c7b14ce1e926c7d4", + "12e0310edca52b08cb8a18dd2e382da62908b476f14b1a8e0aa993a945e84399", + "e8ceee44ab4241cb19d5da582abc8b9c7ec4d71bbfa456bbda2d3dfb120c1e82", + "e3951d2637df7a24c3fb48360da87247d998c9f092d7ebe349bf5b493cf89034", + "2657c928cb139ac30cb495a790e8a125d452a541267e71ca1635cb69d830c349", + "4d0fab3685c9e14d8382253670d0453430010fdb95da9a0cf5427760ff8f4bf5", + "85a65d69081dc88743067a1ae0f5abef0adf79ba23500954217f990bdd17246d", + "b3d6e83386b11a0d736d43f1eb3235388abdfaf2c90e94ba02d4c50a3c830a66", + "f1f207e702a02318635f35a70141ff96d50b1058f57c01f84fd5973112eec5bd", + "10e412a4f7c37f47b63d39cbde4721f65670bc2085ec8db8a6ebd2db0f19531c", + "a7679d3b0d86f7d19b865f9c25aa91c1bbcfcc89cac3415138393f68958699f2", + "9be5b69545813b368cb6fd8e8e2f0843a3299c268f3c78691d62f14cc08e1fd5", + "c7ef2594ead75dd56e8effb151e72c72efd2e3151c22f113a9952c2d2a094ef8", + "43406fa30813bf114ad791f19f91783ee623dbf5882e9361dbb9dd42dcc124a9", + "f45c2618d639e25d60a6ff4e3d3c54aca3afda8887feeda48b3931d5fb3a890d", + "021d36f9b63ae9509a139f5919a2c7c6daaa8b775b67d175d9fdc2caf1545247", + "221662e3384acfda6b419aca64194a7e2b515c470eb515641a43ad280f5d8c64", + "93a2280c52123349f7143d8f90b8862985eab8856ce01c1e65847f74e82f3585", + "97595d2f3e15492623c1180c3448d91e27e8caa91a2286ffdde6e0e01d73487c", + "554e51f4e422d00765369712a73dbe5e18c4f59aa55546b9aff3f1b94c8f75ee", + "373c9b4a6fa3fc747881123d342ad6fd4bfe280c1550517f329e7b5fd9c90824", + "5466a29fb42efa17c090eedb46bb213988ef7656634eff61d399c4540bf185d4", + "68882db32387229d3c096e4f075c9075d1128759c3fdf5e5d8bcdfe24072d559", + "f703f98c5b75380e73f861b3ceb685094aee6341309d5161b761fcca78acda0e", + "59f93b2d63f4643f566199a92d955bd78caaf7ec7e3078fd5264f5b478ac09b2", + "639731cfd7ab582be092da8ec8da53718c206b8f0fa5c75953fcff3cb116a172", + "2bfe2a097a074fdff140fef54c104953996a032dc162edf32dc357e09abb3fcc", + "466dd64bfa5468a071705d8b3f86206a481673ced10e0a4f172d7486cd29e8ce", + "a40e511b719b6275dedb1b46f2c5f8b0cd725312ab239132c5b9b29547f6f764", + "6583f450fd9001e5355551acb58a3c95ac663f8f58a82cc3f9f31135fe492442", + "2a818d069c34b05be52769437dba3051f931aae1b1453e950c591b3920c3b3ee", + "0b26d32e1e31400f4c3a5e2750fee52239b2dc3728f927ec455ba49fcc16bbbd", + "7d90705556fd11248be8e53c04bafeb395b123c3b42d3e0e07f02ae5bba74681", + "79ad2329c40c96cd2e4d694d51d2610b8a28971d6b253f5a54c22301afdb293d", + "147eed7b0ff213c3ba36f679ba864cb2b050de968fdedcdcb48e6916bc264574", + "d027dfcaaf6e04f676c619244515b823e7adff3dd01da6730342421168a61c45", + "e2199dbc29264ddf0edc446706d684cc19ab7c81fd19f2c25a6aedfaf38bff61", + "74219656debd84f572508135fb03c4ddb80a992c629a1483a4b32535156ee1b7", + "cc6b74e9616a1facb82a483e19039348ccbc6a387a3c6991f50133ca844bb499", + "3084b541105fe892ddfad7efe4d428a8cfb5890c6970d4c4d05bc1b1e7ea38ef", + "a0b5c69bf883147418969ffca6cd45e9f710223b8da9bd93d77ce8aec76d4bea", + "e08c7e844d6fa6cdcabb04975fb6c66ceda4cadef3b1e4fad7367207d674e973", + "6033e2342b470e18a442cc73eebfc5e77303cd4ecb3507930f777cd2fb740a15", + "3c20d132ea8e829fd8fb7b18cf5f9a5a71dd88bf35bc81f7cf37b4ab4cc7b961", + "0ee98db4b01bea147bbcb1bd492a66c85abe9e20985738bb5a035d587badff8d", + "bd78400ee797aba19c82c3bf060a13d720be4cab6358a39075e1ad721cfd0b99", + "91c0f7efc5e43eefa3e587d1e2685bbbf68a7b064c06da9b21f3e38be2f8915e", + "f0c2ff1abe61e4c0c3092afba482e7eef6f0744c267b5334e060485d01974b18", + "dd4d830bd248066832624a2a5ceca133b9f5b9fa66c1d3a41eb4cc29169f955d", + "04175c02b43883fa4b3c4b990334e2c3e23e85ddc36edcf04bee9f86e8a116af", + "2eafcbc1db5005dbf74c90a56554ba71201e21d21ef4a77094f4b1c6c6d10497", + "f6c826a659eef7eca7685ce1e574066a007906163041aaba2ac90a12f0315660", + "2dc4d5de794e832b499f493c99fa948e10079f74552f23fc5d3700722e8d0f8d", + "c02e4905c3876a3fa382c1a7f90cf91496d66da1b2495cb3f5a345993360280a", + "5a375be2718ac37a38ee1c84b84476695a384e2a2ff43068c8d7989b4fd3cea5", + "b70b2e6d667be606bd1c511f7e2cf631f533948cbb5af26db8650d4101253f63", + "35a49d20ab277ac8c092985c9faad58320d1af7427124c4828888f4bd7491b36", + "2a67df5cdd3e8c2a1241dfd24554af46feee6b138b4809663f86e9012ec47b27", + "b50c7e29386970d39258f395e05f3caeb8648d9a40a0c14a603ba1e729c1782e", + "0db1d79fc1686e0388b8439fe7331862314b77ced506dfdaccc2a21faf74f44f", + "6b3393eacda7dc18a2ce451f2541681f5dd4e9af21a2624956542b60268a9405", + "8e17ed5e690277e1156294c6904f6899de1497d15d8862a98cfe349c5296ec13", + "e44b9eb141e8ff07acd5bc5a2fdd6d334192e27b27b5f4c26a225ad28cd9ab9a", + "02b6fe45acb7af83932640c81519a7cf4d33af40e8c66510855286f1b869df7f", + "c8aff4d8670456d4e64051e65f018226f7abcb40dc77b7a36a934f4afeb42888", + "aa4a4dabb9038bb1bc8dafa93397b77b81e263fcfff59eb86c07920e6c8c218f", + "3b8203b9ed708044720313b67f9b98cf9e96544e95ada0200bc4ea0e54a3c633", + "524aced0165da7aa4aed8d02720276c3895a37fcfd8db9cffbc9d353ccc960c4", + "553fcf8e9e53d37da153a4f8bfd16a2c5379da0472076719b05a26d10100801b", + "fbb7b3e31f1d0d1606dc34bf1c624db7c62133e173203dad5cf4c3792351fbbb", + "e62eddd4a815b35bf6916c615a52f185395baceaa52cee8a836de3a4db0c5381", + "9ea285ecbb3cc9189511d2148e3d732bd98582324d2a597ccb942e8b0158548c", + "6c9ecf00a9f605bd50375d0793e8ebad382060daebbbad0ec1427d5501b95186", + "00a5392f9bf5114694596652d3f47debb758a1ef4a408490688a8b3b6f4d076d", + "c9d5bf16c017b12a95d62a92b270c9c100fee78fc85c91f830c56be00235ce7f", + "68ea43f029fa915aafc14bfa5eb8ab1ab1e6e20c467c0985dfeeb72af1859659", + "172c4fada590aedd1eb45a16ca2ac669cc0371f12aceecf1467e0dd6e2af387c", + "0e8c60669d5b505ff9f6fb941f3247bee6458fa1d6f1829c1444135c55b5b58a", + "83e174c96ba972dd05b14f9e7d8dc0923ad96cdf6b2926c81baece8c7c3ccbc0", + "55699232a0c96bd45cf5bbb81ae2e033d22b534b2fcec3b70ab661d22bc25b0f", + "9d4ba8d5cb7404ae253587e6a583c595214407007c049988a08b51e8c05a67ef", + "1d63978b718b86d8d84bd1f6e7cd053fec76dbc922c597abeeee64066d02b57a", + "6d03e10f0787dd16296266414cb6bacb961387fbec686348f586a4d053718f02", + "e4ae9203d1b61a37a98b21769f110e46de7ab6772d15b4af93a9f39cb2d096a8", + "a5de66a7fa9666e03e6dd9e114d3cbed11e14454758fb20f42586685509f315e", + "f0accba58611444a9eb7c3d41218740ffed85556762d5245f4d1bbd6d958d488", + "95de4a732eaee2996786c28c3ae80a7bbfd34ad276694cdd64194e7d45d6b1b8", + "59b03bd978a4a5ed212fd094e90048aca406d16c196410eb8be8122f2856d1d0", + "2b6c3018cfd7cc55746864434c9b2c610a4971e60d9128a338451d46c78bfc86", + "2428da7b24364be8aa93c6a255543d7da27f4cbe57dde5f14a8512fa5ffb981d", + "bce26d655a0efb6f287be49da07393f30feee6bb6c2306bbad88e04c8984e7d0", + "0fe2ad6dbadcd9c423b5eebde12b78365ff1c4fbc85e933206523afc2a2f8a48", + "67482decfd58bdc49de9ec5a8147945130a928b98202457647c66865ca336c71", + "7d22130b398f4085766f4909a68b4f7fa2c4b26db1e107f3a2ef29417ee5cfae", + "98778612b5124e3d7d455f725f4f2462ef780f0323e0c36cb9bc81811c00a045", + "51e7511c0f2ef6bfc7e17c7906fd604e42e862f3f4475d50d86bfc1401099d0d", + "f120fe5e0ef2b223977b4519069cac7e4e77c587c5b011663d06aa08f80fd477", + "9d00ace943765ba7f2a3a414f534d8e751af83fed0f8e872c36b311a15ae21bf", + "93cc266fd13aaa341f03d20115f066cb126497bc0d6d5de719c9f80604a2e57c", + "ddddd5d3636ab27be531f09a9a93e5d8b39fb2c03a79d4b7908f2b40b52bd1f2", + "626c9f4c4e3bad8d65523b0f4b820f4e59a720007fc35aa925bc99d9deae3e00", + "af9d9060c20e16bb1f94b3da86b8f4c074c472f2d402ebcadb879d9d5853dc78", + "309db5e72eed85d70fb56caffd8bfc1dffb79fb9b599916468a68982c975dca9", + "0738d677af3228b95cf444baba3469dd67aaeba54a93c91ae62845d2ab12bb3b", + "720666a5e21383c6546a87b8cc93be7044c87c015ee2995f87b55bb291e9924b", + "21d91ecc9946962e60ae1119f45687a670eb725a392425e3dca05d108808a314", + "253c1ba525d54f4075c8aa01ee5383d0ea6a41b62d243fb06d4f90987e557578", + "73429a7a5981f25624a1ea521c95c1aa4e744bca69c6b63f495e3c72571d403f", + "e92100e5825a11d06c55a544f83e71a1cb86516805549d0d16c085a690645682", + "3969b8d71699943cd01e410fb2b072638ffdc59d10eb3316ee12a98783c09181", + "3dd6b9d720af724e3bba14e7fc41213a0c6b7fd5c4a6d6081cdde8b05941023e", + "a8c6b92481b9b52f50fc996f3fc023f720b832f79f306aebe9e1db5d5d680f33", + "84bde27cda62db77fac212cc9a399467cf466e0d5af32984b04f389c28e6b0b6", + "d416c534ceaa07ce856d3fe9fb6d8cae21f28aa45eadd4fee27b791e69c8f045", + "0b370fe3369b09d22ddd43f870a428c4b65974430a18261c59781ae77d941480", + "906ad75351144066d6d0d914c5145664825d2004d778aeb41aec56644f81ba25", + "3c57221b786a6de68d8d8db9db8ed3928a20eff9e688ad1121d73085a0fe9d75", + "e1c262992728e3ede684e77f4a06efde3e2eebf8f12a0406dcede1aafe299648", + "c60819a050b2d4e6ac4d72ea6683fde04dbaf6b66bd74ff988fbc7d2b29b9bf2", + "e8468896b3f9734cd0f7c721ee0896ad1548902340242b7fdd16e39b361c4ac5", + "6a8f24b12c66adc9e771cc6b52392688bb8535c1ba026ca7f79d26ce2c7ed9a2", + "f5d607b7d774d7c5d81d6c8d58b4a7ed576403a4dfce8f2e63e1b7ce9c7f8a02", + "011d439d089fcb3c7d44c2d5a6420cf60cf1b8716db31286f7c9ce390a0f2fa6", + "d6fe9c609aefadabb0efff44e5375ea4eacc5237c21b58a2e199cc2899dbfbb0", + "3a0b54bd93d3305354b07b6bb7807bace50476c4e120a0b327de1cdb93617079", + "41e55404a5998f71571a200c032127a6241b2e7b232e82147fb6769d8256bd29", + "5756ac43c1df14927d3af8ef161c403ee1f92b1ed296d0adc72e2cb45d98d7de", + "f82cf2b7895ba7802d0478d2b20b069d3c8500637fe98d4194da02c884cf9d17", + "3a10b6cc7cc3bb244fbd0d49910e588abe7dbaf759d4cf44783a7139af064824", + "5a9b9b31eaedbb651f42d207a14b502b4b73885330c37f8e615ad89e3e61213b", + "e61688578b28b999d906798525736d64a5aac1b5544b7507badb20b34b556d8c", + "45b27a6ef06340c3ed81433083ac81ce99abcc951f5e334919f3ff56cd1c75d6", + "fceb7a9bd727f1e8438f617d02674039902f195dcbbfb01b43d5f72edea02831", + "d19c5e3b8aeb8f8b4c08ee8b61f34277bfe579398a76e28e58b972cd69089b61", + "a32c7ab77af334e2d5c4eb3caf95d3aaad689f28a2dc3a7557f2528be402407e", + "b6c17c21cecca5ac92a1fdc1032369e4c6d9cf2fd4f1d5b4406e1d7e7606b2ac", + "be1b81276adee2c0f4df4cdc6d1b82df87dceaf516b7e4d994e023a937479f9f", + "439251ab04c231c48b891702a2061921fa88cfef44741ee173853b6bf8490430", + "26b7068c7272d9feaab03fef3083e50d49885e375f77792d59ff0cd502c03df1", + "85dce2eba1a9ae5a41d06854f4db88dd7fe022727be35ad6b18e6087c8b91f1b", + "e4f7178d36429096577518bf47e2d296c021ae7349aca78e2c0a4c606acca5a8", + "e8d88f547187b278275c799b25a94d814f3174bac46077df9820a338b4d9c267", + "ac8f48385b06e1c43da7fb74e522d347603b7b973ac3c3d06412ad72f5762fd6", + "22f39a936e8e401a404457cbbef921c3568ce016a5c75de9256011b988c252c6", + "77fa7038e7ded01577a2ae592cdc9663311b5e1e42f5f87ad8a66ae0e9a54c28", + "87f6e5ae2adc65a736c9b1746ec1f18340bbee45481d7c1f5509ad71d5f3c9c2", + "180e9ee8a321a1e91c490cd90afd74d556ae2a9bae223ec6c094587f359870b0", + "f6c87366cdd0be058ebf9295d8b6b42729be58a0f7f057fe3243f9b2949ada26", + "d45c27211e8a4b77a9e76c12db25e6e9ba9d03647f3691d158edbd8b7c38ab69", + "8ebac15b55c4a2c5d3f4866858fe3ab9324bd785036dc0ce8ababa396419eb35", + "6a80d86e69694ebeec5f6f5a5d37d12c2f813147c1116d5d26538d25f8882729", + "2cf7e4c33c2a63ef54c444020bac3648738bee48779db4a3a0f9e72ca5b4e380", + "9d101afcd85b7331d4008230f29b761b9f798e8814d123140e8b648b230261f9", + "c6ee0ade266785f9dadcf6892bf4b98d7bbc292aaf2bfe5b7dd484785760375b", + "94d239b78781096e3e8e7e6a7d9ad70d0804adeb6a6675fc5da4f05d1152c19d", + "55ca49e0e313f14ae0f214ac0a51843158ee07aa5f381cb9d596b8612dbd3f36", + "e3e3672131fe9a71f54a41b0e8143c63bc0aeb823288527bda069a1616a41529", + "ad1b386b668b79c7cada251a6c2bd19c53e51045e4a4562b4b9212852d334556", + "bd08a3ba2d8603332b31b2394a556edf6ef949ec8d49e5d54a13318213a0da65", + "b34731e855ce7fc834aa0846c72ce7f455f40564b11181b340ec07a003787e88", + "ae292c8d03de63af642539e6559960770d80dc4e9590c46f54ab8a74b68f145c", + "10a28c04e90016bc744986e50d2a240ff553cea32c8ccbfc218445d6f9e32271", + "182df763c8b4dc2c1698567546dac90ed5de00c033322c9f731a64f08cf7ae61", + "eaad2e6f6dbc000fe3b8f60fc7ce7011c22c4d062ae2c33a3621098190027381", + "1e7d630f59f8c968c54a803f8c70e15d85a1214be4e0f8e82cab9b2056df4ef6", + "af485e0990350c9814bf8a8b3db91f44d4d459f513f9e0635ff54a444a45055e", + "c5339ed11420ef44af6d7eec085a7495ac1f59264119184d3183a937d7012295", + "ee4c98e7b5e1b5fc19c8e5e75b9be39690ebf15bc9b1e5fbb3ecb75a2ec89d3a", + "c88b24689438c8f097cec81f2178fd162d4c53ba51f1b835c9b7d1f33f3bfed4", + "b3f12ee06f773536bc4882441d7b40153fd02ff4abd97a5686bc8ec294de5303", + "74b69c91d7b24cbc734d5bbfccf3442b10aa94f835dd49e84aeb9fb93d48f984", + "7249af9eedd2ff198a93c9fd96d3847d05fd24e4a2dee3213f8b0157028a7a1e", + "4c663e23616d31135cf9b4c33a89556d9973b2df0fe8b5a851163ce3e770df33", + "4afede394dff0a2b6f0b1b3dedf82614ca5133814c9466e7856a53cf33a83359", + "b7f3d596f87080e6fe4b0c5e17364dc87ba269ca561689d20085c9890270145a", + "a91c666326b9198e9954c4461da9e173c21042edbc8e63e6527f69aa06144ad5", + "c11375b5ba44d9be64cf8ccc8eaba4689ce8d0a1d75f4b932d14b4e26b962ba8", + "c737cddd051af963d4109063a7c98bfbbe500abdf2761545fb2e3f592f1c3183", + "9b09f03742db7491a7a96f20d2735d669383b59570c919c5f8f4d804ccf552af", + "112ba278ae78125b0ee2a9ae9d4c857d7b09d3636d8afd9e175a3557917409f2", + "3ec2366bdb151bc04838ad4459790d217d7e70104dbb6a13de48c8e97873c874", + "e592d021807bc768bc2eb1b6b7832d7326565c39b82658ce9c340ee50a443626", + "446de54c9e312644e2fd20f49b8d9926edeb2c995e627414da25b664f14e4fb2", + "c0e6c970fb25f5ec11f0c76316e7eff5bb180acd21bc777616ad3212edf5cb3c", + "32c3a1e954262361dfea8dcd6d4e74c436d53e6518a2bfd080302187e454ef3d", + "af6c5f72dbd65e75a91a40ba9c6abad575f12b7fd8b66c41d88948fa8e026f3d", + "2f858194e1da082ac8ba94373768de9fa1842a1adba13b2bf6c67474d0a233c8", + "74b19f6456e34582f963dba6d7faebb6289050227f55d5bead6aed9f6394eb41", + "e249aa1cc573b722bfb4e6613dfc70e2a5d6cb7df94ba1002e7f549f57aedcef", + "0ad80c6bbea362b8bdb966cbc50135bfa321bb8cbb0179f76f445dde3d156c8b", + "898f699a5c52bdada6a62d24ac05ec8cb043f5679b554ae5a9b4d666a440fb76", + "305cf536a22fcbdee3353cdf59ddbd42e24ea1a8b91967d5038ca3f9094868c5", + "2847f3395f902b011e2eb0ce5e995cd97af861c925e04021a5bd893447025060", + "91f09953dda135db571d5e159e07a495b0ca9d2eb6ba584b6bff019ae5693624", + "648126d6266a94fd567040da2a9becb5e6be4055f9f4176d252ea8b29915b8a1", + "46451db7b8576c094fa561a81e6d1300220b42b886e3e3b5faeb95c78090e6e0", + "97817465e9219742eaf8121d0d053224793083991313a7b376093f5cdf7329b2", + "4e5c52de7f2d110910f12126b36f9ee712f4a70ec48a86ef10b21767b876396f", + "6510078a1522e2f137600f9183802c1581f882bd9fa0f8f1391484f84cf0fb43", + "2b687ea5a740865b89abc5657546a55b7d5f7fe4e4796354b4b4266f0c0ec10c", + "879d6960613b3138e2a92c821026f5858a99338db1f769923ccb7f32ec4afcd6", + "6c37cd022e745b6666c0388b81296be90dce442972de52400fa3adb8e734d2ee", + "e58f22310f59fdddb90479cc36c763c1fcf60d6e09501d51c495dcb95c033d05", + "70ac02944106daddc97b53db0a0f9bdc1cdf7e789b698b4234a88d8acb945233", + "e797d50c461ed06e255f8106694f1e3c3dff45ca342b631c9d93fd6868ee7862", + "d645d1ce9d365a5ea79ccaa9b24ec597a029e07e667bdd5461a7cec83344f83a", + "2fdc7e98eaa0910fd04914e2ac4c1216e2a4d37621e23c69daa7539b6d403ad6", + "3ea00f497980193dac3261c3e12d7593b1668465227775c53b01e0029a3dbaee", + "eff07e9c0aa7f889f74bd0daabfa20017aeac105fb09d11647059f37eb1027eb", + "69d3cce33ba2f60e895201dd8d0a92db55e4db99b04de54976d3f11ca280dfe4", + "64aef37268e77bcbbda83cb568ba365aa650c4363e22f08ebd9ea8dd4c5ec3ba", + "837f82d3bed0341335bd5ab9d633bb5b1117a0b71664315643f710321451b08d", + "52b1677f38980e1ba9eee33faf8a60f50d76a9020fc18aadad44ef12d102c630", + "c5727a2a22170ed9a40b4e003e7039bb6f51ef719faa103159869f715cd79d64", + "2bd8376008ef3339ad49383a12754f63710ab01657cf717aa945f8ebeb351d26", + "617d5e3734a176c6aaa970e78f412532263398023f922604d69d73a34d7c9600", + "b508b6f323862d1393522ce67c9f267f8f7a08c4f17ec2d7e9230df6a7dcc83d", + "003b1c8857c2366b415d5b1eb23ab8c0705065bd87fe08e7c25114098bc2d020", + "9c18e71750ab5a52520fa5c6f4b48047d34af42b9feedfcfa96a9b9cf6c2231d", + "c3d69220994caf490d6d8f4c7f7689da6e533bdbf775309af7b13b25359c15db", + "64945a074fe5d593413f740fbb0fb036efc1585073a5c769e6b7bba5083bff67", + "da64240fb5aefe994b80dcbf86c6775e1d8b9827c90ea914dc89ed96994c1b06", + "f3dcceebebff82171d500aa5bfa42d923af47fd9d935ff3b24e4246c2c240bb0", + "5e9c33d78cdd3d5f71a74981aee540c199f9928c132f047ff975f9c4528929ed", + "e83e08ca018747334d5bef54eff1527d03fc478a9a2ce3bc1d08eb74b1e05c5a", + "2b4404ceb208a4cbcca4eca597cc25e50c156d1d4320b937795c201bf62e8e27", + "6cf6e54742a3e3a2469296b51863123d6939a3e4f18973bed3350f361a0a7fa1", + "173648ce3c3faebf66b1b400e2ba1d09a6f7a0e90c4f6eb259cf141cf2702868", + "1602f84cd08034833d4d6b6f0a63017840b0517fcd716dc5bb82071f81cacf22", + "5c9a5a89db7ce103784cd0a185ef589967930fa6e92aa90fb8c02031694a9404", + "a99a83be39be7d0d247f3805d7c78f47bf16a3babf9d63cb68e28c0f630e15c2", + "9a92a01899a649676ac792b2a2be8db3ab24f1f464a1af6b3c03b7f67e572510", + "e0448af4bfdabe13a044362ab133a5560a77e8e6605df7d9b36f22a9ccdd806f", + "8f675662f98a9961e9a67114a98f442359b8797f729c94c5a041ebdb937e0b57", + "6dd8437315fb9be06fcbb22ebc82103648da5b142622857175dd75a0f78c7a8f", + "6c4a96d440dbfc0f26f1bdf1e1c0ac273b573c36f14e55342fd98435e806b676", + "700725449099dc98933ea1c8f1b9fdaf21ccb7f7439451cbdcf763e7b633e52d", + "22c986a29a24c02703a8be6fc3a17f00975fe6f323366da8284a4c1070b18160", + "037036f85de49e6136d1f54c2b1f754d61c262bb88c806b72a156e272d60c535", + "8813017f3878a97b15c272982b95b3534a4475dbc3a6f40aaee3eb9dd593eb2a", + "403a7a97d3c47c57e89982086de458cc1c62d01449945e9adeb160bdb487e7c8", + "e1ae2a3ca0a102d9d7b80462c5c75ce1fd49a341a3ae60a497a98c9e84dc4d2c", + "7d3cf5fdda0516c200a2a5d622c256d481f9764d6a72799995e01b31c97f4f26", + "a080f0e1bf33dc8190c844c3b13c56c058ea08af44e1c7363d55e8fd8a7a3afc", + "a611f43917fd4c23e5bb65ba8dcbde6db510142346cad2b9ab596fa75635d9e3", + "ba068f56418356e2c0ee98119cc3030fa7a9b8c27d5530c93f134e899afedc02", + "81a7d27fa4e8d911410a733248f102e5a9eff4d8f5617114e97c1267cbf3d348", + "ea711ec0f66cec18c17b3bfa39967b00480a0df0575412c0e8a7aa8869ab93ac", + "dc2efe412c0c7cf5d7e838641bdbe2d59877ca74357536da58aaa0fd1b711ac0", + "93e321c3371d69fade1451f2f537579c54934aba5271a018540f70d4ac91a78e", + "8f1368dfc99618f3e1b2302ebee88319371106bef78471b0eb876f2f06a3a818", + "5fffc415587f815bf9645acdd481294fc4d83ac5b9a37275e8725921eaeed7f7", + "3f29e067de0e036efd2684b9c54e0e6b2787fb9d83504c1201c4d0a017a0785f", + "07f4a000178a78a4a8faa370d5aea0aa5628ff669522fbf6ee0d016ce016f8dc", + "773ac8e936b1618243407a82d1ec9a6feaa5ebd9cbd82619d19b5bd7f0297536", + "af56203cef3a9126c3442d964fe09bfeba50d9243fd9bb87ec38a961c59b5960", + "fbbdfc94f38b514470de4290bba2ebdff7aac5523d97a0abbb20859ebc82065a", + "471ab6bea6f7cae837a9fc601f70c7c8e53e84a544e43008d945875d61df04ad", + "e20fa6fa46cd4d80d54d393c86e49b282ea1526c5f339e579bad5b39304498b8", + "cac629d37126684710d0bb74fb52d529f2d16c73734f2005de6de9a504ad84ff", + "a953032ffba659a809d5f733daa9db681082aa65dd08c0b05664dbefd87707ba", + "5bab2244233e9f9f2d7191dfbe1ac968652ea1bc0fac48a5557b01c40d260f4a", + "9d09dc8281294423fd516835ab30f15416cfd011e74dff4bf5e05f87416f0cf6", + "893776d2cccda1f86b0e405d13a75a11a96528a45e0872e4ffc0b87513594100", + "758a1e6461aeb3ffb55c231caacce8f663d83eef4d12f8cf94d69b09252b6e06", + "0dee497dd3a7bbc26c1b2b2f807e55bc62765b3557cd50960e8b913a2e3acdba", + "35a08cc32bdce97e91ba274bde6d6f19c74ae3fd3566cf1c40002a0775872505", + "689104a13715e4e881a4640fd5e4bcd46687c67f53c91db4b8cae8d21cf90722", + "e48d19e8a8a211358e7e7ffe964e981d3a1d11ae76a2b44a44faa856566abc5b", + "28d7c2498774be7540d3b85cd67e873db198267ebd59cefb1164969d56544940", + "8440cbb16acf418689ac4fe6d2b65ddd949e96458542539d30bfe2125e3a88ca", + "6f837c7e4a9f4c4c1b17fb202a5bc7479add8cc0d26564766328f77455627961", + "fb786f9dc125141f04857fd1393abe9172ad9c3aade782bd4d8dc23018c662bb", + "be101e3278740014287a572b2b27fe3e20712bf589db356e800fad1e633d34b6", + "02770ff50283c20a9a9de291de391fe7ebb4744da607a7780b10a20c49a753c2", + "84801d626b5998746e0865aec4fcd1378bb376edcc763a80ee96373b01cbf72c", + "1da1dd3aa77a4e2d8aa0857ccd857d4952e58ab49c26a1f0cb60275030f6607e", + "04fefbda5ec9f182c2a22ad6bbbc42ffa322338e3562976b8d32b3406c2e38c9", + "e8ccdcb248078a482359f4f7973d3b2069794b9777c4e2ac21558fbffee59c8c", + "51507a05fbdfcc522a1f29099cc06458591bcd5809d2b210901e275dc1daed7d", + "05e6b311d3a3241022b0152924627706edf857161004dc785983c5c8e4059b19", + "986ccd7ef43c8085b738a0e2eb84b98aa920fd19f372c34c37569a5c7354d6df", + "37c4370297131fcef6101bb43e37ada63ee4c63eadd53c683cdffda2c22b6a5f", + "c8a736669a5a1c69c6088735306b2aa6da1fa685584446b27c53612344a28d39", + "0c829d4a74a40e4338a406a268de64a9ac1b58ad2f0655cf1412cfe10a81f128", + "ea8aeeeb0fcfa66319b5d1560c1c7e805cb445b678f60735a5450233a08f5146", + "0c03e2902e4374dc2ae6e9d46b27028af86c461ec65b9708829f6e341c5dfa86", + "427623b7f21f13216b47d64b3cce69d8695a12a8b76ceda909038e8ed1ad7ad6", + "309bd0bf85e4b9bb130a17df032a6d057d2f4774abca3a8af6fdbf25de9182d9", + "7f3da6402da41b66b0e5e957a40a787c53a3a31228f06d45f6ddfc275c06e815", + "9453352b0b6541e28b5c36d1ef833b7f49dad472ac7bc01712f04a1d0bd8cb3d", + "35b6a024cb364ee9e7ac55b3ee07e137b7dc7756c6e7131d02d603613f876578", + "6ce8b20bf01933590374ff85f4a2fcab43cde2bbf9bf7b56c7e4c21acce02af1", + "f02acb1e0f786b78b042e5d5e4d25fe711ecc05b2cbdc37077a4a1b66d925808", + "b5697acfcc9d3d4b58bf01ee959bb519b165d6e2b3ff799dcd8581f2e4ecc4b4", + "e5dcd86553008584de8d4e67edb1049198922900801bf51da55efcb7de45c37b", + "e0c3e5997bd114db9c09728c21b8b35748eee68f8eb305b5cc099be3c265d327", + "6c1e0ef8496d1c39fcde030239772ddf1182807dde25d54fea68085434a08b02", + "4fbd6bcb31a607f792cca7f514d8259a05f684092be8ac91b242bc90169a7042", + "9dbd18a509716c53037d8713d0353094d55b48253cb0d38cd7bc08959d7ff014", + "a444e2b82333cbd5dfe250e393e245d5db8b78f91bbb6a572761a0a43ed56b38", + "b163cba8c2d0441ac59033f7742733cb77812bd74de1147ae8173faae4f9269b", + "0ac9c6fc35728d1317490b51b36277e825a49ba95be2a6783ef7af99e49228f7", + "abe44cdbc6aea6876b8f009d6279479dca2c4f17c650c51bd7de410bbf23cd24", + "4b9370d4ae2fe08db33f468ffceca5d9fe6b0273ef2887a4ea8522293870e0a1", + "98b3b29996faf548b808efa1fd4ca779882bb75cc2ab3304f049b429a3638a97", + "dfb57e8a34f4911773133529e0e222f6a5cf691b25a65c288f8d6baa68b5f406", + "d1e35d54b3df21193102e9dcbb194a294323522ae35c68597b4b08f53cbf36ec", + "53e8bb244f3c2c90fcc8ceb1662e03aab3e1a781fa1697f7383e0bdc59831195", + "255874e4ced532485777b878f2d779c62853bd8d4d05ddadfa6163465703dc8c", + "6b61c176da93e56226cb6fb7a4f8acf1df87d02be81f823dc851680ae64f80b6", + "f035af0a109e467fd4ecd7c45bed043dd1ba0d02164c2b2aa0ab1989d8eaeff7", + "81b239e1213841934e989034959c9ca0448ad64ac3ab919751c2289600a20723", + "251b4f7823c8449e2e154cbab4aa72fb2cc55495da5fdc2d31075aa5647ba4cd", + "0769a11a881bfd2f02a087803235d344ad2f005010e40ecd7af5db96245dd719", + "f4963f675eefacf4cdd14ff7b759d5752aecee4cd704744e58b9bbbe078e0faa", + "28916a97e55790f8e0a95c1203c1f8f1eac99b866aac5800445ef2d888b4be87", + "575a46ecc4c7757656b9371e9b8dc058f99f9d9ad20a3e6de2d4e6cbc193711c", + "935994c1df9323d2bdcbab8762904e48edf6298904f3ae7e234599366bfd86ef", + "dec5bac62e0ef64db4936f818e325e12aed29ee0933277259185da1298dd0a28", + "3ba063cbea967fc0b7bb9a25c5a12ee801eaa92f5570d1d1a977a229834fd0a0", + "3327cc41ebebc9e82f06f836d5868f9c5023b05498bd73d836f04179d24fc7b9", + "8d70f449bda2223d0d647d7543bc3a2c29fedffaef789dc5bd73d25a6f0afd38", + "9902826a10b226265f357562bb20774358dbc636e790257a57af7f791b600acf", + "c5821a347232865496580de7f3043822ca56eea4f8266c51e3be4a16016cdeed", + "2817d6dee3e6706b92989f2dcef7cd0fef5d2263ed3d1c5dab8ba99550b1e5d5", + "b918f2ce206701dfe60eee1d40fad119540e928b8da813fe29d77a2e26c2738b", + "7f836479ecd2c4aec31c96935e2ce932135249c6077184b837f724fbdd425408", + "1381c39fb5a68c134b948aacf72b5127a7b0d5a721ed2091da25a3856b9a9af8", + "80e6ab5756de5eba8551f62d8943e5f650f4ae8864ea6bdffc3257ef1072679a", + "ae7dd5fcf4688844f5ef72471cc0273e0e2f8f43ba4fc3dd41208f70b880e5d4", + "e0f1d8c1a745ede1198fefbd0555bc118f7ccbd9ee783d8ed7a148f54fb1903f", + "33c8a75fad49fd3b3bb1e031fb283858f79b05a8b461eb1f8f0e9d41c53474bc", + "3864e97cd6f47c2eaf3fdcbb105e014accb4eb902be8a43dfca77dfe0d92761e", + "e524e7c40053d3d1da35ac8a584ad7034cd30de0e9513bf7731f8e5f51f7178e", + "12e121ad8a9be231659d08746b1a8b2b9247304dddc69cec074711970986cc6d", + "149a0d197a6c2dba3044e43f5fac90f6a797e0d4c8d91e241725e963083bb472", + "6a062ba02e79d2b2c001e1689daa1b3ade7c917ef26b30987798123818b202bd", + "99e47c124d128f741f30e7c5bc7575f11a005befaa26ab60b560fe4a65717aa0", + "147961e3d28573540582cb4a6e266f3a677d9e5b24da19c29fa611d3f8fba8a9", + "59261c94c9000fe26b3b050c4cf7ab06343d4ffcbe3934da65f12d8a7b0c6862", + "7428bc174ef7fd68911d46b35da30bbf8a292bb6b9ea99489b0c28bb55876e54", + "6fbff71f7187e67e5cc11f85ecc7d063284c6829ffc2b647d9439cf6d80c71b3", + "d49db17aa1e54596333d1962db0050b54a96abc6184f28b46778ac3eb9bc2290", + "082436ca15376750587883056c2b5fceafd232f6fd91d839dcf7f138583565d0", + "2d6c7614ddf121cedd9db989f73da61c3e5d5d7a876139d2d441d8dc28bef0ab", + "10b5eb9db4b22b9047b2c9a2b40abefc9786d8f0b33514bcef81c462f241ca60" + ], + "key_material": { + "scan_priv_key": "000000000000000000000000000000000000000000000000000000000000dead", + "spend_priv_key": "000000000000000000000000000000000000000000000000000000000000beef" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kq6xcdal4dazgg2txp472hdz4svms98328lqdflmpxppmc6ykamcrv9s7w9g", + "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kqmk4a3pm24z0yepw6aw27pku56md0hq5ace0l3p3jstkqaajwrsjqc5dde5" + ], + "n_outputs": 2323, + "tweak": "022c333a8938ca0c7433c5e39d440b469f58374d0363b5bffc66aca1cc10d7b609", + "shared_secret": "03abe0979dbbe4e1bb2cd2e06524a411c0b829c1d2282052da817b46f1d6e598e4", + "input_pub_key_sum": "02bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + } + ] } ] diff --git a/bip-0360.mediawiki b/bip-0360.mediawiki new file mode 100644 index 0000000000..7a3f00447d --- /dev/null +++ b/bip-0360.mediawiki @@ -0,0 +1,430 @@ +
+  BIP: 360
+  Layer: Consensus (soft fork)
+  Title: Pay-to-Merkle-Root (P2MR)
+  Authors: Hunter Beast 
+           Ethan Heilman 
+           Isabel Foxen Duke 
+  Status: Draft
+  Type: Specification
+  Assigned: 2024-12-18
+  License: BSD-3-Clause
+  Version: 0.11.0
+  Requires: 340, 341, 342
+
+ +==Introduction== + +===Abstract=== + +This document proposes a new output type: Pay-to-Merkle-Root (P2MR), via a soft fork. P2MR outputs operate with nearly the same functionality as P2TR (Pay-to-Taproot) outputs, but with the key path spend removed. + +Through this modification, P2MR outputs allow developers to use script trees and tapscript in a manner that is: + +# resistant to long exposure attacks by Cryptographically Relevant Quantum Computers (CRQCs), and +# resistant to future cryptanalytic approaches that may compromise the elliptic curve cryptography (ECC) used by Bitcoin. + +It is worth noting that proposed P2MR outputs are only resistant to "long exposure attacks" on elliptic curve cryptography; that is, attacks on keys exposed for time periods longer than needed to confirm a spending transaction. + +Protection against more sophisticated quantum attacks, including protection against private key recovery from public keys exposed in the mempool while a transaction is waiting to be confirmed (a.k.a. "short exposure attacks"), may require the introduction of post-quantum signatures in Bitcoin. We believe it's worth considering this path in the future and intend to offer a separate proposal for this purpose upon further research. + +This document additionally defines "long exposure" and "short exposure" attacks, and other new terminology in the Glossary. + +===Copyright=== + +This document is licensed under the 3-clause BSD license. + +===Motivation=== + +The primary threat to Bitcoin from Cryptographically Relevant Quantum Computers (CRQCs) is their potential to break the key cryptographic assumption which secures the digital signatures used in Bitcoin.A Cryptographically Relevant Quantum Computer is an ''object'' which is only loosely defined by ''characteristics'' in quantum physics as of today. It could be understood in the context of this BIP and in Bitcoin that it's a ''hardware-agnostic'' computer supposed to have the architecture to keep ''coherent'' a sufficient number of logical qubits to be able to run Shor's algorithm in an efficient fashion. More specifically, [https://arxiv.org/pdf/quant-ph/0301141 Shor's algorithm] enables a CRQC to solve the Discrete Logarithm Problem (DLP) exponentially faster than classical methods.Shor's algorithm is believed to need 10^8 operations to break a 256-bit elliptic curve public key. This allows the derivation of private keys from public keys — a process referred to here as quantum key recovery.Meaning, deriving private keys from public keys via Shor's algorithm While it is unclear when or if CRQCs will become viable in the future, we propose the addition of a quantum-resistant, [[#script-tree-output-type|script tree output type]] for those interested in this level of protection. + +While some may balk at the potential threat of quantum computers to Bitcoin given their limited functionality to date, some others — including governments, corporations and some existing and potential Bitcoin users — are concerned about their potential for advancement. The Commercial National Security Algorithm Suite (CNSA) 2.0, for instance, has mandated software and networking equipment to be upgraded to post-quantum schemes by 2030, with browsers and operating systems fully upgraded by 2033. Additionally, according to NIST IR 8547, Elliptic Curve Cryptography (ECC) is planned to be disallowed within the US federal government after 2035 (with an exception made for hybrid cryptography, or the use of ECC and post-quantum algorithms together). These kinds of mandates have triggered concern by some ECC users, including some Bitcoin users who prefer to be prepared out of an abundance of caution. + +In the most optimistic case, wherein quantum computers never pose a significant risk to ECC, we understand that the possibility of quantum advancement alone may be influencing adoption and broad confidence in the Bitcoin network. In other words, we believe users' fear of quantum computers may be worth addressing regardless of CRQC viability. Given these concerns, we think it's worth considering simple low risk changes that create options for using Bitcoin in a quantum-resistant way. + +As a conservative first step in this effort, we propose Pay-to-Merkle-Root (P2MR), a script tree output that can be used in a quantum resistant manner. + +===Long Exposure vs Short Exposure Attacks=== + +For clarity, this proposal specifically mitigates the risk of long exposure attacks on outputs that support tapscript and script trees. While some other Bitcoin output types, such as P2SH, are safe against long exposure attacks, taproot is not and taproot is the only currently activated output type that supports tapscript and script trees. + +A long exposure attack is an attack performed on exposed blockchain data, such as exposed public keys, or the scripts of spent outputs. These are likely to be the earliest quantum attacks made possible on Bitcoin, because attackers will have ample time — as much time as vulnerable keys are exposed — to carry out quantum key recovery. + +Short exposure attacks, however, require faster quantum computers, because they must occur within the relatively short time that a transaction is unconfirmed in the mempool. + +Bitcoin outputs are generally vulnerable to short exposure attacks, as most Bitcoin transactions require revealing the associated public key when spending. Full protection of outputs from short exposure attacks may require the use of post-quantum signature schemes. + +Since long exposure attacks on public keys are likely to be the first quantum-enabled threat to Bitcoin, we propose a script tree output that is resistant to long exposure attacks as a first step in hardening Bitcoin against the potential threat of quantum computers. + +The following list of output types describes their long exposure attack vulnerability: + +{| class="wikitable" +|- +! Type +! Vulnerable +! Prefix +! Example +|- +| P2PK +| Yes +| Varies +| 02103203b768951584fe9af6d9d9e6ff26a5f76e453212f19ba163774182ab8057f3eac +|- +| P2PKH +| No* +| 1 +| 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa +|- +| P2MS +| Yes +| Varies +| 52410496ec45f878b62c46c4be8e336dff7cc58df9b502178cc240e... +|- +| P2SH +| No* +| 3 +| 3FkhZo7sGNue153xhgqPBcUaBsYvJW6tTx +|- +| P2WPKH +| No* +| bc1q +| bc1qsnh5ktku9ztqeqfr89yrqjd05eh58nah884mku +|- +| P2WSH +| No* +| bc1q +| bc1qvhu3557twysq2ldn6dut6rmaj3qk04p60h9l79wk4lzgy0ca8mfsnffz65 +|- +| P2TR +| Yes +| bc1p +| bc1p92aslsnseq786wxfk3ekra90ds9ku47qttupfjsqmmj4z82xdq4q3rr58u +|- +| P2MR +| No* +| bc1z +| bc1zzmv50jjgxxhww6ve4g5zpewrkjqhr06fyujpm20tuezdlxmfphcqfc80ve +|} + +The following output types are fundamentally vulnerable to long exposure attacks: + +* P2PK outputs (e.g. Satoshi's coins, CPU miners) +* Reused outputs* +* Taproot outputs (starts with bc1p) + +* Funds in P2PKH, P2SH, P2WPKH, P2WSH, and P2MR outputs can become vulnerable to long exposure quantum attacks anytime their script reveals a public key. + +Note: Extended public keys, commonly known as "xpubs," and wallet descriptors also reveal quantum vulnerable public key information. For further clarification on quantum attack vectors, please refer to the [[#Glossary|Glossary of Terms]]. + +==Design== + +Pay-to-Merkle-Root (P2MR) is a proposed new output type that commits to the root of a script tree. It operates with nearly the same functionality as P2TR (Pay-to-Taproot) outputs, but with the quantum vulnerable key path spend removed. + +In other words, P2MR outputs commit to the Merkle root of a script tree without committing to an internal key. The script(s) being committed to, however, may contain a key or key-hash. + +This output type is designed to offer users protection against long exposure quantum attacks as well as a practical output type with which post-quantum signatures may be used if such signatures are adopted in the future. + +Since P2MR outputs have no key path spend, they omit the Taproot internal key. Instead, a P2MR output includes the 32-byte root of the script tree as defined in [[bip-0341.mediawiki|BIP 341]] hashed with the tag "TapBranch" as shown below. + +[[File:bip-0360/media/merkletree.png|thumb|Construction of P2MR script tree root, scriptPubkey, and Witness]] + +To construct a P2MR output, we follow the process outlined in [[bip-0341.mediawiki|BIP 341]] to compute the final tapbranch hash, which is the merkle root of the script leaves; however, instead of tweaking the internal key with the root of the Merkle tree (as is the case with P2TR outputs), P2MR outputs commit only to final tapbranch hash, which is tagged, "TapBranch". + + +D = tagged_hash("TapLeaf", bytes([leaf_version]) + ser_script(script)) +CD = tagged_hash("TapBranch", C + D) +CDE = tagged_hash("TapBranch", CD + E) +ABCDE = tagged_hash("TapBranch", AB + CDE) + + +A P2MR input witness provides the following: + + +initial stack element 0, +..., +initial stack element N, +leaf script, +control block = [control byte, 32*m byte Merkle path] # m is the depth of the script in the Merkle tree + + +The initial stack elements of P2MR follow the same rules as P2TR script path spends. +That is, they place elements on the stack to be evaluated by the leaf script. + +The control block is a ''1 + 32 * m'' byte array, where the first byte is the control byte and the next ''32 * m'' bytes are the Merkle path to the leaf script. The control byte is the same as the control byte in a P2TR control block, including the 7 bits which are used to specify the leaf version. The parity bit of the control byte is always 1, since P2MR does not have a key path spend. Unlike P2TR, we omit the public key from the control block as it is not needed in P2MR. We maintain support for the optional annex in the witness (see Specification section below for more details). + +==Rationale== + +Design of the P2MR output type is guided by the following intentions: + +1. Minimize changes to the network. We should reuse existing Bitcoin code and preserve existing software behavior, workflows, user expectations and compatibility whenever possible. + +P2MR leverages the battle tested P2TR, tapleaf and tapscript code already in Bitcoin, reducing the implementation burden on wallets, exchanges, and libraries that can reuse code they already have. This approach reduces complexity and minimizes implementation risks. + +2. Create the safest possible path for the addition of post-quantum signature integrations, in the event that they are used in the future. + +Importantly, we are proposing a script tree output, i.e. an output type that supports tapscript, that is resistant to long exposure attacks. While some existing output types are already resistant to long exposure attacks (e.g. P2WSH), no such output type supports tapscript — a feature that may be required for practical implementation of post-quantum signature opcodes. + +P2WSH, for instance, does not support tapscript and as such does not support the OP_SUCCESSx opcode update path that will be critical for the integration of post-quantum OP_CHECKSIG opcodes into Bitcoin.OP_SUCCESSx is a mechanism to upgrade tapscript + +3. Facilitate gradual integration of quantum resistant features that can be carried out iteratively as quantum computers evolve. This approach encourages responsiveness to the current threat-level, while avoiding heavy-handedness in our reactions to a potential threat. + +We designed P2MR with an eye towards integrating post-quantum signatures in the future, without proposing more complex changes while CRQCs are still in their infancy. + +===P2MR Trade-Offs=== + +While P2TR outputs (and the use of key path spend) will remain an option for folks wishing to use them, we aim to be clear about the tradeoffs of using P2MR outputs, which disable the key path spend for the benefit of quantum resistance. + +The witness to a P2MR spend is always larger than the witness to a P2TR key path spend. This is because a P2TR key path spend requires only a Schnorr signature in the witness. For P2MR, the witness must include the chosen leaf script, the initial stack, and a control block consisting of the control byte and Merkle path (if any). + +That said, the witness to a P2MR spend will always be smaller than the witness to an equivalent P2TR script path spend, because there is no longer any internal key in P2MR that must be revealed in the control block. For a more complete comparison of output type transaction sizes, the "Transaction Size and Fees" section may be reviewed later in this proposal. + +Additionally, there is a privacy tradeoff when comparing P2MR and P2TR, which is that users reveal they are spending to a script tree whenever they are using P2MR outputs, since P2MR outputs can only be spent via script path spend. In P2TR when you spend an output as a key path spend, you don't reveal if you have any script path spends. This trade-off only exists when comparing P2TR key path spends to P2MR script path spends; P2TR and P2MR provide the same level of privacy when both are script path spends. + +'''Note:''' P2MR and P2TR both provide greater script privacy than P2SH [[bip-0016.mediawiki|BIP 16]] because unused script paths are not revealed. + +==Specification== + +We define the Pay-to-Merkle-Root (P2MR) output structure as follows: + +A P2MR output is similar to a P2TR output (as defined in [[bip-0341.mediawiki|BIP 341]]); however, unlike P2TR outputs, we disable the key path spend for the benefit of quantum resistance by omitting the internal key and the tap tweak step. +A P2MR output is then a SegWit version 2 byte followed by the Merkle root of the script tree as the witness program. + + +===Address Format=== + +P2MR outputs use SegWit version 2, resulting in mainnet addresses that start with bc1z, following [[bip-0173.mediawiki|BIP 173]]. Bech32m encoding maps version 2 to the prefix z. + +Example P2MR address: + + +bc1zzmv50jjgxxhww6ve4g5zpewrkjqhr06fyujpm20tuezdlxmfphcqfc80ve + + +This commits to a 32-byte script tree Merkle root. + +===ScriptPubKey=== + +The scriptPubKey for a P2MR output is: + + +OP_2 OP_PUSHBYTES_32 + + +Where: +* OP_2 indicates SegWit version 2. +* is the 32-byte Merkle root of the script tree. + +===Script Validation=== + +A P2MR output is a native SegWit output (see [[bip-0141.mediawiki|BIP 141]]) with version 2 and a 32-byte witness program. For the sake of comparison, we have — as much as possible — copied the language verbatim from the script validation section of [[bip-0341.mediawiki|BIP 341]]. + +* Let ''q'' be the 32-byte array containing the witness program (the second push in the scriptPubKey) which represents the Merkle root of the script tree. +* Fail if the witness stack does not have two or more elements. +* Fail if the witness stack has exactly two elements and the first byte of the last element is 0x50. +* If there are at least three 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 thereof) is always covered by the signature and contributes to transaction weight, but is otherwise ignored during P2MR validation. +* There must be at least two witness elements left. +** Call the second-to-last stack element ''s'', the script (as defined in [[bip-0341.mediawiki|BIP 341]]). +** The last stack element is called the control block ''c'', and must have length ''1 + 32 * m'', for a value of ''m'' that is an integer between 0 and 128, inclusive. Fail if it does not have such a length. +** Let ''v = c[0] & 0xfe'' be the ''leaf version'' (as defined in [[bip-0341.mediawiki|BIP 341]]). To maintain ''leaf version'' encoding compatibility the last bit of c[0] is unused and must be 1.Why set the last bit of c[0] to one? Consider a faulty implementation that deserializes the ''leaf version'' as c[0] rather than c[0] & 0xfe for both P2TR and P2MR. If they test against P2MR outputs and require that last bit is 1, this deserialization bug will cause an immediate error. +** 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[1+32j:33+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 ''r = km''. +** If ''q ≠ r'', fail. +** Execute the script, according to the script rules specified in BIP 342, 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. + +The steps above follow the script path spend logic from [[bip-0341.mediawiki|BIP 341]] with the following changes: + +* The witness program is the the Merkle root of the script tree and not a tweaked public key. This means that we skip directly to the BIP 341 script path validation. +* We compute the script tree Merkle root ''r'' and compare it directly to the witness program ''q''. +* The control block is ''1 + 32*m'' bytes, instead of ''33 + 32*m'' bytes. + +===Common Signature Message Construction=== + +The [https://learnmeabitcoin.com/technical/upgrades/taproot/#common-signature-message common signature message] construction for P2MR outputs is exactly the same procedure as defined in [[bip-0342.mediawiki#user-content-Common_Signature_Message_Extension|BIP342 Common Signature Message]]. + +=== Compatibility with BIP 141 === + +By adhering to the SegWit transaction structure and versioning, P2MR outputs are compatible with existing transaction processing rules. Nodes that do not recognize SegWit version 2 will treat these outputs as anyone-can-spend but generally will not relay or mine such transactions. + +===Transaction Size and Fees=== + +All P2MR and P2TR outputs are always the same size. P2MR inputs can be slightly larger or smaller than their equivalent P2TR inputs, depending on the use of key path vs script path spend in the case of P2TR. Let's consider the cases. + +====Comparison with P2TR key path spend==== + +A P2MR witness will be larger than a P2TR witness when the P2TR output is spent via the key path spend. A witness to a P2TR key path spend is simply a signature. P2MR quantum resistance comes from removing the P2TR key path spend. Every P2MR spend is a P2TR script path spend and so requires a script, its input stack and a control block. Consequently, P2MR loses this size advantage of P2TR key path spends in order to gain quantum resistance. If the script tree only has a single leaf script, no Merkle path is needed in the control block, giving us a minimal size control block of 1 byte. + +P2MR witness for depth-0 tree (103 bytes): + + +[count] (1 byte), # Number of elements in the witness +[size] signature (1 + 64 bytes = 65 bytes), +leaf script = [size] [OP_PUSHBYTES_32, 32-byte public key, OP_CHECKSIG] (1 + 1 + 32 + 1 bytes = 35 bytes), +control block = [size] [control byte] [merkle path (empty)] (1 + 1 + 0 bytes = 2 bytes) + + +P2TR key path spend witness (66 bytes): + + +[count] (1 byte), # Number of elements in the witness +[size] signature (1 + 64 bytes = 65 bytes) + + +Thus, the P2MR witness would be 103 - 66 = 37 bytes larger than a P2TR key path spend witness. + +If the Merkle tree has more than a single leaf, then the Merkle path must be included in the control block, increasing the size by ''32 * m'' bytes, where m is the depth of the Merkle tree. +While script trees do support leaf scripts of different depths, here we assume the Merkle tree has been constructed such that each leaf is at the same depth. +This would make such witness 37 + 32 * m bytes larger than a P2TR key path spend witness.If ''m >= 8'', then the compact size will use 3 bytes rather than 1 byte + +P2MR witness ''(103 + 32*m bytes)'': + + +[count] (1 byte), # Number of elements in the witness +[size] signature (64 + 1 bytes = 65 bytes), +leaf script = [size] [OP_PUSHBYTES_32, 32-byte public key, OP_CHECKSIG] (34 + 1 bytes = 35 bytes), +control block = [size] [control byte] [Merkle path] (1 + 1 + 32*m = 2 + 32*m bytes) + + +====Comparison with P2TR script path spend==== + +A P2MR witness will be smaller than the witness to an equivalent P2TR script path spend. This is because P2MR does not require inclusion of an internal public key in the control block to unlock and spend an output. For this reason, a P2MR witness will always be 32 bytes smaller than an equivalent P2TR script path spend witness. + +==Performance Impact== + +P2MR is slightly more computationally performant than P2TR script path spends, as the operations to spend a P2MR output is a strict subset of the operations needed to perform a script path spend on a P2TR output. + +==Backward Compatibility== + +Older wallets and nodes that have not been made compatible with SegWit version 2 and P2MR will not understand these outputs. Per [[bip-0350.mediawiki|BIP 350]] older wallets should be able to spend funds to SegWit version 2 outputs. Users should ensure they are using updated wallets and nodes to receive P2MR outputs and validate transactions using P2MR outputs. P2MR is fully compatible with tapscript and existing tapscript programs can be used in P2MR outputs without modification. P2MR can also support future scripts with new leaf versions. + +==Security== + +P2MR outputs provide the same tapscript functionality as P2TR outputs, but with the quantum-vulnerable key path spend removed. The similarity between these output types enables users to easily migrate script trees from P2TR outputs to P2MR outputs for protection against long exposure quantum attacks. Wallets supporting only P2TR key path spends would need to migrate to using script trees. This is a straightforward migration as it only requires moving to a simple OP_CHECKSIG leaf script. + +Protection from long exposure quantum attacks does not depend on the activation of post-quantum signatures in Bitcoin, but requires that users do not expose their public keys to attackers via public key reuse or other unsafe practices. + +P2MR uses a 256-bit hash output, providing 128 bits of collision resistance and 256 bits of preimage resistance. This is the same level of security as P2WSH specified in [[bip-0141.mediawiki|BIP 141]], which also uses a 256-bit hash output. + +P2MR does not, by itself, protect against short exposure quantum attacks, but these attacks can be mitigated by future activation of post-quantum signatures. + +Combined with P2MR, post-quantum signature schemes can provide comprehensive quantum resistance to P2MR outputs, including protection from short exposure attacks. + +That said, protection against long exposure quantum attacks alone should not be underestimated. It's unlikely that early CRQCs will be fast enough to perform short exposure attacks, making preparedness against long exposure attacks more time-critical. + +==Security Considerations for Post-Quantum Signature Schemes== + +While this proposal does not include the introduction of post-quantum signature schemes, we think it's worth commenting on security considerations related to this possibility. + +Quantum-resistant signature algorithms (e.g. ML-DSA or SLH-DSA) offer different levels of protection and should be scrutinized before use. We are currently researching options for the potential proposal of post-quantum signatures into Bitcoin and encourage others to engage in this research as well. + +We also imagine the possibility of introducing multiple post-quantum signatures for redundancy. Balancing the risks of additional complexity with the benefits of signature-type redundancy will be the challenge here. + +==Test Vectors and Reference Code== + +Test vector data for creation of P2MR UTXOs can be found [https://github.com/bitcoin/bips/blob/master/bip-0360/ref-impl/common/tests/data/P2MR_construction.json here]. + +These test vectors build off of the test vectors for [[bip-0341.mediawiki|BIP 341]] (Taproot). One important distinction is that the P2MR test vectors do not include keypath spend scenarios. + +Also included are test vectors in [https://github.com/bitcoin/bips/tree/master/bip-0360/ref-impl/rust rust implementation] and [https://github.com/bitcoin/bips/tree/master/bip-0360/ref-impl/python python implementation]. One of these tests demonstrates a tapscript that requires a secp256k1 signature to spend the P2MR UTXO (modeled after one of the extremely valuable examples provided by [https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature this Taproot script path spend example]. Similar to BIP 341 test vectors, all signatures are created with an all-zero (0x0000...0000) [[bip-0340.mediawiki|BIP 340]] auxiliary randomness array. + +==Related Work== + +Below we attempt to summarize some of the ideas discussed on the Bitcoin Development Mailing List that relate to P2MR. + +The idea of Taproot with key path spend removed has been discussed a number of times in the Bitcoin community. + +For instance, [https://gnusha.org/pi/bitcoindev/CAD5xwhgzR8e5r1e4H-5EH2mSsE1V39dd06+TgYniFnXFSBqLxw@mail.gmail.com/ OP_CAT Makes Bitcoin Quantum Secure] notes that if we disable the key path spend in Taproot and activate OP_CAT [[bip-0347.mediawiki|BIP 347]], we could achieve quantum resistance by using Lamport signatures with OP_CAT. + +Lamport and WOTS (Winternitz One-Time Signatures) built from CAT are quantum resistant, but are one-time signatures — meaning, if you sign twice for the same public key, you risk leaking your private key, which is a significant security concern for everyday users. + +This would require major changes to wallet behavior and would represent a significant security downgrade. Common practices, such as RBF and CPFP, could risk revealing private keys if no stateless signature scheme is used. + +[https://groups.google.com/g/bitcoindev/c/8O857bRSVV8/m/rTrpeFjWDAAJ Trivial QC signatures with clean upgrade path] and [https://groups.google.com/g/bitcoindev/c/oQKezDOc4us/m/T1vSMkZNAAAJ Re: P2QRH / BIP 360 Update] also discuss the possibility of Taproot with key path spend removed. The design of P2MR was partly inspired by these discussions. + +Commit-reveal schemes such as [https://gnusha.org/pi/bitcoindev/1518710367.3550.111.camel@mmci.uni-saarland.de/ Re: Transition to post-quantum (2018)] and [https://groups.google.com/g/bitcoindev/c/LpWOcXMcvk8/m/YEiH-kTHAwAJ Post-Quantum commit / reveal Fawkescoin variant as a soft fork (2025)] have been proposed as a way to create cryptocurrencies without public key cryptography. The ideas in this paper were more recently expanded upon by Tadge Dryja in his "[https://www.youtube.com/watch?v=4bzOwYPf1yo Lifeboat]" proposal, which effectively quantum-proofs Bitcoin transactions through a similar pre-commitment scheme designed for Bitcoin. + +==Other Methods of Addressing Quantum Vulnerabilities for Cryptocurrencies== + +It is worth noting, by way of comparison, that [https://ethresear.ch/t/how-to-hard-fork-to-save-most-users-funds-in-a-quantum-emergency/18901 Vitalik Buterin's proposed solution] to Ethereum's quantum vulnerability is quite different from the approach in this BIP. + +His plan involves a hard fork of the chain, reverting all blocks after some sufficient amount of theft, and using STARKs based on BIP 32 seeds to act as the authoritative secret when signing. We believe rollbacks of any kind are an untenable approach for Bitcoin and would likely be impractical to implement. + +That said, we believe the use of STARKs (which are quantum-resistant) may prove useful as a method of proving access to external private keys, in the event that the community chooses to burn vulnerable coins as proposed by Jameson Lopp and others in [https://qbip.org/ QBIP]. + +Discussions related to the burning of coins, and other attempts to slow a potential supply shock caused by quantum-retrieval of vulnerable coins, are out of scope for this proposal. That said, members of our team have separately proposed [https://github.com/cryptoquick/bips/blob/hourglass/bip-hourglass.mediawiki Hourglass] to address this concern and are continuing research on this subject. + +==Conclusion== + +In this proposal, we adopt a "prepared not scared" approach to the possible advancement of quantum computing and offer Bitcoin users an option for increased protection if they so choose. This BIP does not take a position on any specific quantum computing timeline, but rather proposes a flexible and unobtrusive option for users that wish to mitigate this risk according to their own estimate of the timeline. + +This is an issue that has been discussed with some regularity in [https://bitcointalk.org/index.php?topic=133425.0 Bitcoin forums] since at least 2012, and there is clearly user demand for increased quantum protection. + +==Glossary== + +'''Quantum Key Recovery''' + +The derivation of private keys from public keys in elliptic curve cryptography (ECC), made possible by solving the discrete logarithm problem (DLP). + +Shor's algorithm, developed by Peter Shor in 1994, is a quantum algorithm that efficiently solves the discrete logarithm problem — potentially made possible by the future viability of cryptographically relevant quantum computers (CRQCs). + +'''Long Exposure Attacks''' + +Attempts to derive private keys from public keys that are exposed for an extended period of time; that is, longer than the window of time that a public key is generally exposed in the mempool while waiting to be confirmed. + +Long exposure attacks give attackers an unlimited amount of time to perform quantum key recovery, as long as funds remain in the output. Poor wallet hygiene (e.g. from address reuse) or use of outputs with exposed public keys (e.g. P2TR outputs) increases vulnerability to long exposure attacks. + +'''Short Exposure Attacks''' + +Attempts to derive private keys from public keys during the brief period when funds are unconfirmed in the mempool. These attacks cannot be prevented through wallet hygiene, as revealing a public key is necessary for spending. + +Protection against short exposure attacks may require post-quantum signature schemes; that said, executing these attacks requires faster CRQCs than those capable of executing long exposure attacks and are therefore viewed as lower-risk than long exposure attacks in the nearer term. + + +'''Script Tree Output Type''' + +Script tree output types are a category of output type that support a script tree consisting of leaf scripts. Script tree output types support tapscript and would support any new script language added to bitcoin which is able to be used as a leaf script in a script trees. If Pay-to-Merkle-Root (P2MR) is activated, P2MR would be the second script tree output type in Bitcoin, the other being Pay-to-Taproot (P2TR). + +'''Pay-to-Merkle-Root (P2MR)''' + +A script tree output type, similar to to Pay-to-Taproot (P2TR), but with the quantum-vulnerable key path spend removed. +==Footnotes== + + + +==Changelog== + +To help implementers understand updates to this BIP, we keep a list of substantial changes. + +* __0.11.0__ (2026-02-10) - Rename BIP from Pay-to-Tapscript-Hash (P2TSH) to Pay-to-Merkle-Root (P2MR) +* __0.10.3__ (2026-02-06) - Rename tapscript-native output type to script tree output type. +* __0.10.2__ (2026-01-23) - Fix bug in verification, minor review comments and adopt [[bip-0003.mediawiki|BIP 003]] conventions. +* __0.10.1__ (2026-01-21) - Terminology and clarity improvements, addressed feedback from reviews. +* __0.10.0__ (2025-09-17) - Rewrote BIP for clarity and renamed from P2QRH to P2TSH +* __0.9.0__ (2025-07-20) - Changed the Witness Version from 3 to 2. +* __0.8.0__ (2025-07-07) - P2QRH is now a P2TR with the vulnerable key path spend removed. Number of PQ signature algorithms supported reduced from three to two. PQ signature algorithm support is now added via opcodes or leaf version. +* __0.7.0__ (2025-03-18) - Correct inconsistencies in commitment and attestation structure. Switch from Merkle tree commitment to sorted vector hash commitment. Update descriptor format. +* __0.6.2__ (2025-03-12) - Add verification times for each algorithm. 256 to 128 (NIST V to NIST I). Add key type bitmask. Clarify multisig semantics. +* __0.6.1__ (2025-02-23) - More points of clarification from review. Update dead link. +* __0.6.0__ (2025-01-20) - Remove SQIsign from consideration due to significant performance concerns. Refactor language from long range attack to long exposure so as to not be confused with the language around block re-org attacks. +* __0.5.1__ (2024-12-18) - Assigned BIP number. +* __0.5.0__ (2024-12-13) - Update to use Merkle tree for attestation commitment. Update LR & SR quantum attack scenarios. +* __0.4.0__ (2024-12-01) - Add details on attestation structure and parsing. +* __0.3.0__ (2024-10-21) - Replace XMSS with CRYSTALS-Dilithium due to NIST approval and size constraints. +* __0.2.2__ (2024-09-30) - Refactor the ECC vs PoW section. Swap quitness for attestation. +* __0.2.1__ (2024-09-29) - Update section on PoW to include partial-preimage. +* __0.2.0__ (2024-09-28) - Add Winternitz, XMSS signatures, and security assumption types to PQC table. Omit NIST Level I table. Add spend script specification. Add revealed public key scenario table. +* __0.1.0__ (2024-09-27) - Initial draft proposal + +==Acknowledgements== + +This document is inspired by [[bip-0341.mediawiki|BIP 341]], which introduced the design of the P2TR (Taproot) output type using Schnorr signatures. + +I'm incredibly grateful to Ethan Heilman for joining as co-author and transforming this BIP into something far more congruent with existing Bitcoin design. Additionally, much gratitude to our most recent co-author, Isabel Foxen Duke, for her thoughtful editing and crafting much of the language in this proposal. I am likewise indebted to those on the Anduro Quantum Working Group who took the time to contribute including Jeff Bride, Michael Casey, and notmike. + +Thank you as well to those who took the time to review and contribute, including Jon Atack, Adam Borcany, Ava Chow, Kyle Crews, Pierre-Luc Dallaire-Demers, D++, Mark Erhardt, Jameson Lopp, Antoine Riard, Armin Sabouri, Vojtěch Strnad, Guy Swann, and Joey Yandle. + +Whatever inaccuracies may remain are attributable solely to the authors. diff --git a/bip-0360/media/merkletree.png b/bip-0360/media/merkletree.png new file mode 100644 index 0000000000..0a702a2d9e Binary files /dev/null and b/bip-0360/media/merkletree.png differ diff --git a/bip-0360/media/merkletree.svg b/bip-0360/media/merkletree.svg new file mode 100644 index 0000000000..cb280dc09d --- /dev/null +++ b/bip-0360/media/merkletree.svg @@ -0,0 +1,1902 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + P2MR Witness (input) + + + + + + + + + + + + annex + 50 + + + + Control byte + leaf script + leafscript B + Initial stack + + + + + Merkle path (32*m bytes) + Control block + Witness Program + SegWit version 2 + leaf version + + leaf version + 1 + { + { + P2MR scriptPubKey (output) + How the script tree Merkle root is computed: + + + OP_PUSHNUM2 + script tree Merkle root + + script tree Merkle root + + tagged_hash TapLeaf + + tagged_hash TapBranch + + tagged_hash TapBranch + + tagged_hash TapBranch + + tagged_hash TapBranch + + + + + + + leaf version + + + + + leaf +script B + leaf +script A + + leaf version + + + + + + leaf version + + + + + + leaf version + + + + + + + + + + + + + + + + + tagged_hash TapLeaf + + + tagged_hash TapLeaf + + + tagged_hash TapLeaf + + + + tagged_hash TapLeaf + + + + + + leaf +script D + leaf +script E + leaf +script C + + diff --git a/bip-0360/ref-impl/.gitignore b/bip-0360/ref-impl/.gitignore new file mode 100644 index 0000000000..db5bd6b642 --- /dev/null +++ b/bip-0360/ref-impl/.gitignore @@ -0,0 +1,6 @@ +rust-bitcoin +rust-miniscript +libbitcoinpqc +target +.btcdeb_history +*.swp diff --git a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json new file mode 100644 index 0000000000..061d76be8e --- /dev/null +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -0,0 +1,248 @@ +{ + "version": 1, + "test_vectors": [ + { + "id": "p2tr_using_v2_witness_version_error", + "objective": "Tests that a P2TR v2 scriptPubKey fails with use of witness version 2", + "given": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + "scriptTree": null + }, + "intermediary": { + "merkleRoot": null, + "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + "tweakedPubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343" + }, + "expected": { + "scriptPubKey": "522053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "error": "P2TR requires witness version of 1" + } + }, + { + "id": "p2mr_missing_leaf_script_tree_error", + "objective": "Tests P2MR with missing leaf script tree", + "given": { + "script_tree": "" + }, + "intermediary": { + }, + "expected": { + "error": "P2MR requires a script tree with at least one leaf" + } + }, + { + "id": "p2mr_single_leaf_script_tree", + "objective": "Tests P2MR with single leaf script tree", + "given": { + "scriptTree": { + "id": 0, + "script": "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "asm": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + ], + "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + }, + "expected": { + "scriptPubKey": "5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "bip350Address": "bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k", + "scriptPathControlBlocks": [ + "c1" + ] + } + }, + { + "id": "p2mr_different_version_leaves", + "objective": "Tests P2MR with two script leaves of different versions. BIP-360 requires all leaves to use leaf version 0xc0; a non-standard version must throw an error.", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac", + "asm": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "leafVersion": 192 + }, + { + "id": 1, + "script": "06424950333431", + "asm": "424950333431", + "description": "just pushes the string 'BIP341' onto the stack", + "leafVersion": 250 + } + ] + }, + "expected": { + "error": "BIP-360 requires leaf version 0xc0; leaf 1 has version 250" + } + }, + { + "id": "p2mr_simple_lightning_contract", + "objective": "Tests P2MR with two script leaves that simulate a simple lightning network contract. Reference: https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example-with-tap.md", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "029000b275209997a497d964fc1a62885b05a51166a65a90df00492c8d7cf61d6accf54803beac", + "asm": "144 OP_CHECKSEQUENCEVERIFY OP_DROP 9997a497d964fc1a62885b05a51166a65a90df00492c8d7cf61d6accf54803be OP_CHECKSIG", + "description": "Alice takes the money after waiting 1 day", + "leafVersion": 192 + }, + { + "id": 1, + "script": "a8206c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd533388204edfcf9dfe6c0b5c83d1ab3f78d1b39a46ebac6798e08e19761f5ed89ec83c10ac", + "asm": "OP_SHA256 6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333 OP_EQUALVERIFY 4edfcf9dfe6c0b5c83d1ab3f78d1b39a46ebac6798e08e19761f5ed89ec83c10 OP_CHECKSIG", + "description": "Bob takes the money whenever he wishes to by revealing the preimage_hash", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9", + "632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42" + ], + "merkleRoot": "41646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b" + }, + "expected": { + "scriptPubKey": "522041646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b", + "bip350Address": "bc1zg9jxlrqlu25kmkkh74r3h387uldfs72wlrz95n60c6j4n4svna4s4lhfhe", + "scriptPathControlBlocks": [ + "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9", + "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42" + ] + } + }, + { + "id": "p2mr_two_leaf_same_version", + "objective": "Tests P2MR with two script leaves of same version", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "asm": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "leafVersion": 192 + }, + { + "id": 1, + "script": "07546170726f6f74", + "asm": "546170726f6f74", + "description": "pushes the string 'Taproot' onto the stack", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc" + }, + "expected": { + "scriptPubKey": "5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + "bip350Address": "bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y", + "scriptPathControlBlocks": [ + "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c164512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ] + } + }, + { + "id": "p2mr_three_leaf_complex", + "objective": "Tests P2MR with a complex three-leaf script tree structure, demonstrating nested script paths and multiple verification options", + "given": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "scriptTree": [ + { + "id": 0, + "script": "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac", + "asm": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac", + "asm": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "leafVersion": 192 + }, + { + "id": 2, + "script": "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "asm": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c", + "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6" + ], + "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2" + }, + "expected": { + "scriptPubKey": "5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "bip350Address": "bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq", + "scriptPathControlBlocks": [ + "c1ffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", + "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ] + } + }, + { + "id": "p2mr_three_leaf_alternative", + "objective": "Tests another variant of P2MR with three leaves arranged in a different tree structure, showing alternative script path spending options", + "given": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptTree": [ + { + "id": 0, + "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "asm": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "asm": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "leafVersion": 192 + }, + { + "id": 2, + "script": "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "asm": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7" + ], + "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def" + }, + "expected": { + "scriptPubKey": "52202f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "bip350Address": "bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv", + "scriptPathControlBlocks": [ + "c13cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", + "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + } + ] +} diff --git a/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json b/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json new file mode 100644 index 0000000000..86a7f2fca7 --- /dev/null +++ b/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json @@ -0,0 +1,252 @@ +{ + "version": 1, + "test_vectors": [ + { + "id": "p2mr_missing_leaf_script_tree_error", + "objective": "Tests P2MR with missing leaf script tree", + "given": { + "script_tree": "" + }, + "intermediary": { + }, + "expected": { + "error": "P2MR requires a script tree with at least one leaf" + } + }, + { + "id": "p2mr_single_leaf_script_tree", + "objective": "Tests P2MR with single leaf script tree", + "given": { + "scriptTree": { + "id": 0, + "script": "201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f", + "asm": "1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR", + "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc" + ], + "merkleRoot": "3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc" + }, + "expected": { + "scriptPubKey": "52203bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc", + "bip350Address": "bc1z8wcdhrr2mnv8xv9y4ry3hc87rv3a50q4rdhjld8jd9pfcsacmz7qg5szm8", + "scriptPathControlBlocks": [ + "c1" + ] + } + }, + { + "id": "p2mr_different_version_leaves", + "objective": "Tests P2MR with two script leaves of different versions. TO-DO: currently ignores given leaf version and over-rides. Probably better to throw error", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f", + "asm": "1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR", + "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24", + "leafVersion": 192 + }, + { + "id": 1, + "script": "06424950333431", + "asm": "424950333431", + "description": "just pushes the string 'BIP341' onto the stack", + "leafVersion": 250 + } + ] + }, + "intermediary": { + "leafHashes": [ + "3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc", + "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ], + "merkleRoot": "1619ce6d22a46dea045c4adf7f5f33d6810d00d0e9c8a4c7ba35db37b915c604" + }, + "expected": { + "scriptPubKey": "52201619ce6d22a46dea045c4adf7f5f33d6810d00d0e9c8a4c7ba35db37b915c604", + "bip350Address": "bc1zzcvuumfz53k75pzuft0h7hen66qs6qxsa8y2f3a6xhdn0wg4cczq0h84sj", + "scriptPathControlBlocks": [ + "c13bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc", + "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ] + } + }, + { + "id": "p2mr_simple_lightning_contract", + "objective": "Tests P2MR with two script leaves that simulate a simple lightning network contract. Reference: https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example-with-tap.md", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "029000b275201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f", + "asm": "144 OP_CHECKSEQUENCEVERIFY OP_DROP 1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR", + "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24", + "description": "Alice takes the money after waiting 1 day", + "leafVersion": 192 + }, + { + "id": 1, + "script": "a8206c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd533388204c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd", + "asm": "OP_SHA256 6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333 OP_EQUALVERIFY 4c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd OP_SUBSTR", + "priv_key": "49a4214f386240d97ea68efb4268043fd5a55208dcdac18ce5bd3332b8e488944c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd", + "description": "Bob takes the money whenever he wishes to by revealing the preimage_hash", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e", + "a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e" + ], + "merkleRoot": "2794771cd51f215ba3a19fbcdf08c771edb7de782a0c34457e0e9be5d0e4008f" + }, + "expected": { + "scriptPubKey": "52202794771cd51f215ba3a19fbcdf08c771edb7de782a0c34457e0e9be5d0e4008f", + "bip350Address": "bc1zy728w8x4rus4hgapn77d7zx8w8km0hnc9gxrg3t7p6d7t58yqz8sg0nccq", + "scriptPathControlBlocks": [ + "c1cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e", + "c1a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e" + ] + } + }, + { + "id": "p2mr_two_leaf_same_version", + "objective": "Tests P2MR with two script leaves of same version", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "20e6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f53447f", + "asm": "e6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f5344 OP_SUBSTR", + "priv_key": "a695d8a1351774d59ed1b980462c2ab8d58b3b7a48f55b114c6a39980dc1c13ee6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f5344", + "leafVersion": 192 + }, + { + "id": 1, + "script": "07546170726f6f74", + "asm": "546170726f6f74", + "description": "pushes the string 'Taproot' onto the stack", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "9de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + "merkleRoot": "5112b3edfd2c0b717491e9d4888ed2d5dfeaa25115143540e0a08516b68c008c" + }, + "expected": { + "scriptPubKey": "52205112b3edfd2c0b717491e9d4888ed2d5dfeaa25115143540e0a08516b68c008c", + "bip350Address": "bc1z2yft8m0a9s9hzay3a82g3rkj6h074gj3z52r2s8q5zz3dd5vqzxqngpk2w", + "scriptPathControlBlocks": [ + "c19de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d", + "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ] + } + }, + { + "id": "p2mr_three_leaf_complex", + "objective": "Tests P2MR with a complex three-leaf script tree structure, demonstrating nested script paths and multiple verification options", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "201d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc7f", + "asm": "1d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc OP_SUBSTR", + "priv_key": "2b339b5055fad695f0595d5581fa087455854f64c5443c03d5b4ca53549f12a41d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "20a3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad7f", + "asm": "a3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad OP_SUBSTR", + "priv_key": "ec29d60c1be9263602906499b5e3c1de9e36cc88cec31154210191a578b92e9da3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad", + "leafVersion": 192 + }, + { + "id": 2, + "script": "20e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f367f", + "asm": "e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f36 OP_SUBSTR", + "priv_key": "da04d13f706a6e0d22ac0db7c361f1aaa706a27043efca4edfecfed3125238e1e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f36", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "0840c39e59eda6c9deee687a480cb169130c2f053ed2eb3134511ec1cfd8a2c7", + "837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1", + "b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a" + ], + "merkleRoot": "eaf8f557fdb9673de7bb9bad7e7452da9f44a3e65133fdadf2849c55cfb3cf5b" + }, + "expected": { + "scriptPubKey": "5220eaf8f557fdb9673de7bb9bad7e7452da9f44a3e65133fdadf2849c55cfb3cf5b", + "bip350Address": "bc1zatu024lah9nnmeamnwkhuazjm205fglx2yelmt0jsjw9tnaneadszq7wg7", + "scriptPathControlBlocks": [ + "c1837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a", + "c10840c39e59eda6c9deee687a480cb169130c2f053ed2eb3134511ec1cfd8a2c7b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a", + "c118781f42f664d67acaf0ce7c6826437e5440eb1789f232af05e9a09fdf547903" + ] + } + }, + { + "id": "p2mr_three_leaf_alternative", + "objective": "Tests another variant of P2MR with three leaves arranged in a different tree structure, showing alternative script path spending options", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "20409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c7f", + "asm": "409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c OP_SUBSTR", + "priv_key": "c6ec3801b04afa40be6f77f3f7a7b3b7cb8cfe233b0263fb70e2f087b21397c1409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "209f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a7f", + "asm": "9f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a OP_SUBSTR", + "priv_key": "7524bca170d54231f1246f30fb00f9da2208b1363ba5a7bbaf11fc3b0309e9519f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a", + "leafVersion": 192 + }, + { + "id": 2, + "script": "20c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f7f", + "asm": "c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f OP_SUBSTR", + "priv_key": "d9d0f17d630b6a538e6a8a036373e7b9e5023a4c08f72dd8c3ef59288b98c079c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "52e9326c2bf04d926b7e9f6c7645dd853f3f007b870201de9b814952750c9310", + "dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5c", + "ddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361" + ], + "merkleRoot": "51e3c1151ba73d9efce801837773331bf9030977242f62dfeb6756795f482409" + }, + "expected": { + "scriptPubKey": "522051e3c1151ba73d9efce801837773331bf9030977242f62dfeb6756795f482409", + "bip350Address": "bc1z283uz9gm5u7eal8gqxphwuenr0usxzthyshk9hltvat8jh6gysys28twnc", + "scriptPathControlBlocks": [ + "c1dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5cddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361", + "c152e9326c2bf04d926b7e9f6c7645dd853f3f007b870201de9b814952750c9310ddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361", + "c1b45680a7821e4b9450096ab38adbc3c99225af8f6c7ec121a0a5f1ae02893ba3" + ] + } + } + ] +} diff --git a/bip-0360/ref-impl/common/utils/signet_miner_loop.sh b/bip-0360/ref-impl/common/utils/signet_miner_loop.sh new file mode 100755 index 0000000000..6ec65c4a40 --- /dev/null +++ b/bip-0360/ref-impl/common/utils/signet_miner_loop.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Invokes mining simulator a configurable number of times + +if [ -z "${P2MR_ADDR}" ]; then + echo "Error: Environment variable P2MR_ADDR needs to be set" + exit 1 +fi + + +BITCOIN_SOURCE_DIR=${BITCOIN_SOURCE_DIR:-$HOME/bitcoin} + +# path to bitcoin.conf for signet +BITCOIN_CONF_FILE_PATH=${BITCOIN_CONF_FILE_PATH:-$HOME/anduro-360/configs/bitcoin.conf.signet} + +# Set default LOOP_COUNT to 110 if not set +LOOP_COUNT=${LOOP_COUNT:-110} + +# Validate LOOP_COUNT is a positive integer +if ! [[ "$LOOP_COUNT" =~ ^[0-9]+$ ]] || [ "$LOOP_COUNT" -le 0 ]; then + echo "Error: LOOP_COUNT must be a positive integer" + exit 1 +fi + +# Determine name of pool by querying mempool.space backend +# curl -X GET "http://localhost:8999/api/v1/mining/pool/marapool" | jq -r .pool.regexes +export POOL_ID=${POOL_ID:-"MARA Pool"} + +echo -en "\nLoop_COUNT = $LOOP_COUNT\nBITCOIN_CONF_FILE_PATH=$BITCOIN_CONF_FILE_PATH\nBITCOIN_SOURCE_DIR=$BITCOIN_SOURCE_DIR\nPOOL_ID=$POOL_ID\n\n"; + + +for ((i=1; i<=LOOP_COUNT; i++)) +do + echo "Iteration $i of $LOOP_COUNT" + $BITCOIN_SOURCE_DIR/contrib/signet/miner --cli "bitcoin-cli -conf=$BITCOIN_CONF_FILE_PATH" generate \ + --address $P2MR_ADDR \ + --grind-cmd "$BITCOIN_SOURCE_DIR/build/bin/bitcoin-util grind" \ + --poolid "$POOL_ID" \ + --min-nbits --set-block-time $(date +%s) +done diff --git a/bip-0360/ref-impl/common/utils/workshop/Dockerfile.bcli b/bip-0360/ref-impl/common/utils/workshop/Dockerfile.bcli new file mode 100644 index 0000000000..2060accaa5 --- /dev/null +++ b/bip-0360/ref-impl/common/utils/workshop/Dockerfile.bcli @@ -0,0 +1,88 @@ +# podman build -f Dockerfile.bcli -t quay.io/jbride2000/p2mr_bcli:0.1 . +# podman run -it --entrypoint /bin/bash quay.io/jbride200/p2mr_bcli:0.1 + +FROM rust:1-slim-bookworm AS builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + git \ + autoconf \ + automake \ + libtool \ + pkg-config \ + zlib1g-dev \ + libevent-dev \ + libboost-dev \ + libzmq3-dev \ + bash \ + python3 \ + python3-pip \ + libclang-dev \ + clang \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /bitcoin + +# Copy Bitcoin Core source (or clone) +# COPY . /bitcoin +RUN git clone --branch p2mr-pqc --single-branch https://github.com/jbride/bitcoin.git + +# Environment variables for musl +ENV CC=gcc +ENV CXX=g++ + +# Build Bitcoin Core +WORKDIR bitcoin + +RUN apt-get update && apt-get install -y libsqlite3-dev && rm -rf /var/lib/apt/lists/* + +RUN mkdir build && cd build && \ + cmake .. \ + -DWITH_ZMQ=ON \ + -DBUILD_BENCH=ON \ + -DBUILD_DAEMON=ON && \ + make -j$(nproc) bitcoin-cli + +# Runtime stage with Debian Slim +FROM debian:bookworm-slim + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + libevent-core-2.1-7 \ + libevent-pthreads-2.1-7 \ + libevent-extra-2.1-7 \ + libboost-filesystem1.74.0 \ + libboost-thread1.74.0 \ + libzmq5 \ + libsqlite3-0 \ + telnet \ + && rm -rf /var/lib/apt/lists/* + +# Copy bitcoin-cli from builder +COPY --from=builder /bitcoin/bitcoin/build/bin/bitcoin-cli /usr/local/bin/bitcoin-cli + +# Create non-root user and set permissions +RUN groupadd --system bip360 && \ + useradd --system --gid bip360 --shell /bin/bash --create-home bip360 && \ + chmod +x /usr/local/bin/bitcoin-cli && \ + ln -s /usr/local/bin/bitcoin-cli /usr/local/bin/b-cli && \ + echo 'b-cli() { /usr/local/bin/bitcoin-cli -rpcconnect=${RPC_CONNECT:-192.168.122.1} -rpcport=${RPC_PORT:-18443} -rpcuser=${RPC_USER:-signet} -rpcpassword=${RPC_PASSWORD:-signet} "$@"; }' >> /home/bip360/.bashrc + +# Set default environment variables (can be overridden at runtime) +ENV RPC_CONNECT=192.168.122.1 \ + RPC_PORT=38332 \ + RPC_USER=signet \ + RPC_PASSWORD=signet + +# Switch to non-root user +USER bip360 + +WORKDIR /home/bip360 + +ENTRYPOINT ["/usr/local/bin/bitcoin-cli"] + diff --git a/bip-0360/ref-impl/common/utils/workshop/Dockerfile.full b/bip-0360/ref-impl/common/utils/workshop/Dockerfile.full new file mode 100644 index 0000000000..8ee222e714 --- /dev/null +++ b/bip-0360/ref-impl/common/utils/workshop/Dockerfile.full @@ -0,0 +1,115 @@ +# podman build -f Dockerfile.full -t quay.io/jbride2000/p2mr_demo:0.1 . +# podman run -it --entrypoint /bin/bash quay.io/jbride2000/p2mr_demo:0.1 + +FROM rust:1-slim-bookworm AS builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + git \ + autoconf \ + automake \ + libtool \ + pkg-config \ + zlib1g-dev \ + libevent-dev \ + libboost-dev \ + libzmq3-dev \ + bash \ + python3 \ + python3-pip \ + libclang-dev \ + clang \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /bitcoin + +# Copy Bitcoin Core source (or clone) +# COPY . /bitcoin +RUN git clone --branch p2mr-pqc --single-branch https://github.com/jbride/bitcoin.git + +# Environment variables for musl +ENV CC=gcc +ENV CXX=g++ + +# Build Bitcoin Core +WORKDIR bitcoin + +RUN apt-get update && apt-get install -y libsqlite3-dev && rm -rf /var/lib/apt/lists/* + +RUN mkdir build && cd build && \ + cmake .. \ + -DWITH_ZMQ=ON \ + -DBUILD_BENCH=ON \ + -DBUILD_DAEMON=ON && \ + make -j$(nproc) bitcoin-cli + +# Runtime stage with Debian Slim +FROM rust:1-slim-bookworm + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + libevent-core-2.1-7 \ + libevent-pthreads-2.1-7 \ + libevent-extra-2.1-7 \ + libboost-filesystem1.74.0 \ + libboost-thread1.74.0 \ + libzmq5 \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Copy bitcoin-cli from builder +COPY --from=builder /bitcoin/bitcoin/build/bin/bitcoin-cli /usr/local/bin/bitcoin-cli + +# Create non-root user and set permissions +RUN groupadd --system bip360 && \ + useradd --system --gid bip360 --shell /bin/bash --create-home bip360 && \ + chmod +x /usr/local/bin/bitcoin-cli && \ + ln -s /usr/local/bin/bitcoin-cli /usr/local/bin/b-cli && \ + echo 'b-cli() { /usr/local/bin/bitcoin-cli -rpcconnect=${RPC_CONNECT:-192.168.122.1} -rpcport=${RPC_PORT:-18443} -rpcuser=${RPC_USER:-signet} -rpcpassword=${RPC_PASSWORD:-signet} "$@"; }' >> /home/bip360/.bashrc + +# Install additional tools needed for building Rust code (before switching to non-root user) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libclang-dev \ + clang \ + git \ + curl \ + cmake \ + build-essential \ + jq \ + gawk \ + telnet \ + && rm -rf /var/lib/apt/lists/* + +# Set default environment variables (can be overridden at runtime) +ENV RPC_CONNECT=192.168.122.1 \ + RPC_PORT=38332 \ + RPC_USER=signet \ + RPC_PASSWORD=signet + +# Switch to non-root user +USER bip360 + +WORKDIR /home/bip360 + +RUN git clone --no-checkout --depth 1 --branch p2mr-pqc \ + --single-branch https://github.com/jbride/bips.git bips && \ + cd bips && \ + git sparse-checkout init --cone && \ + git sparse-checkout set bip-0360 && \ + git checkout && \ + cd bip-0360/ref-impl/rust && \ + cargo build && \ + rm -rf target + +WORKDIR /home/bip360/bips/bip-0360/ref-impl/rust + +ENV BITCOIN_NETWORK=signet \ + USE_PQC=true + +ENTRYPOINT ["/usr/local/bin/bitcoin-cli"] + diff --git a/bip-0360/ref-impl/common/utils/workshop/bip360-workshop.drawio b/bip-0360/ref-impl/common/utils/workshop/bip360-workshop.drawio new file mode 100644 index 0000000000..4f587663c1 --- /dev/null +++ b/bip-0360/ref-impl/common/utils/workshop/bip360-workshop.drawio @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/bip-0360/ref-impl/common/utils/workshop/signet_360.drawio.png b/bip-0360/ref-impl/common/utils/workshop/signet_360.drawio.png new file mode 100644 index 0000000000..4c80145f8d Binary files /dev/null and b/bip-0360/ref-impl/common/utils/workshop/signet_360.drawio.png differ diff --git a/bip-0360/ref-impl/js/.gitignore b/bip-0360/ref-impl/js/.gitignore new file mode 100644 index 0000000000..1d1baf2e55 --- /dev/null +++ b/bip-0360/ref-impl/js/.gitignore @@ -0,0 +1,76 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# TypeScript +*.js.map +*.d.ts.map + +# Environment variables +.env +.env.local +.env.*.local + +# IDE / Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# OS files +Thumbs.db +desktop.ini + +# Testing +coverage/ +.nyc_output/ +*.lcov + +# Logs +logs/ +*.log + +# Temporary files +*.tmp +*.temp +.cache/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache +.parcel-cache + +# Next.js (if ever used) +.next +out + +# Vercel (if ever used) +.vercel + +# Turbo (if ever used) +.turbo + diff --git a/bip-0360/ref-impl/js/README.adoc b/bip-0360/ref-impl/js/README.adoc new file mode 100644 index 0000000000..797ba5436b --- /dev/null +++ b/bip-0360/ref-impl/js/README.adoc @@ -0,0 +1,18 @@ + += BIP 360 Javascript Reference Implementation + +:numbered: + +== procedure + +----- +$ npm install + +# compile Typecript +$ npx tsc + + +# run tests +$ node src/p2mr-example.ts +$ node src/test-npm-pqc-package.js +----- diff --git a/bip-0360/ref-impl/js/package-lock.json b/bip-0360/ref-impl/js/package-lock.json new file mode 100644 index 0000000000..453951f285 --- /dev/null +++ b/bip-0360/ref-impl/js/package-lock.json @@ -0,0 +1,245 @@ +{ + "name": "js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "js", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@jbride/bitcoinjs-lib": "^7.0.0-rc.0-p2mr-0.0", + "@jbride/bitcoinpqc-wasm": "^0.1.1", + "ecpair": "^3.0.0", + "tiny-secp256k1": "^2.2.4" + }, + "devDependencies": { + "@types/node": "^24.10.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@jbride/bitcoinjs-lib": { + "version": "7.0.0-rc.0-p2mr-0.0", + "resolved": "https://registry.npmjs.org/@jbride/bitcoinjs-lib/-/bitcoinjs-lib-7.0.0-rc.0-p2mr-0.0.tgz", + "integrity": "sha512-JRL0LTBVFBhfHhEciJi8I7C5jGoL24b07rwQthe+3UAGbv2LLOqKRYAD3dc5XD4Maanh1awV+MUELgc7OFgjYw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0-rc.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^0.38.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jbride/bitcoinpqc-wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jbride/bitcoinpqc-wasm/-/bitcoinpqc-wasm-0.1.1.tgz", + "integrity": "sha512-aftRuCBYXPEYzvWBVG84Vgum/ba4zkpUQrNA3jsWK2ir/DzxY7WmReskFEuPprc8X/LQ0/2lxJZyzqGRxDuO8w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bip174": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz", + "integrity": "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, + "node_modules/ecpair": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-3.0.0.tgz", + "integrity": "sha512-kf4JxjsRQoD4EBzpYjGAcR0t9i/4oAeRPtyCpKvSwyotgkc6oA4E4M0/e+kep7cXe+mgxAvoeh/jdgH9h5+Wxw==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8", + "valibot": "^0.37.0", + "wif": "^5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/ecpair/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ecpair/node_modules/valibot": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.37.0.tgz", + "integrity": "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.38.0.tgz", + "integrity": "sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/wif": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", + "integrity": "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==", + "license": "MIT", + "dependencies": { + "bs58check": "^4.0.0" + } + } + } +} diff --git a/bip-0360/ref-impl/js/package.json b/bip-0360/ref-impl/js/package.json new file mode 100644 index 0000000000..4ec7be0f5b --- /dev/null +++ b/bip-0360/ref-impl/js/package.json @@ -0,0 +1,23 @@ +{ + "name": "js", + "version": "1.0.0", + "type": "module", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "^24.10.0", + "typescript": "^5.9.3" + }, + "dependencies": { + "@jbride/bitcoinjs-lib": "^7.0.0-rc.0-p2mr-0.0", + "@jbride/bitcoinpqc-wasm": "^0.1.1", + "ecpair": "^3.0.0", + "tiny-secp256k1": "^2.2.4" + } +} diff --git a/bip-0360/ref-impl/js/src/p2mr-example.ts b/bip-0360/ref-impl/js/src/p2mr-example.ts new file mode 100644 index 0000000000..6eb20fb406 --- /dev/null +++ b/bip-0360/ref-impl/js/src/p2mr-example.ts @@ -0,0 +1,200 @@ +// src/p2mr-example.ts +// Example demonstrating P2MR (Pay-to-Taproot-Script-Hash) address construction + +import { payments } from '@jbride/bitcoinjs-lib'; +import * as bitcoinCrypto from '@jbride/bitcoinjs-lib/src/crypto'; +import * as bscript from '@jbride/bitcoinjs-lib/src/script'; +import type { Taptree } from '@jbride/bitcoinjs-lib/src/types'; +import ECPairFactory, { type ECPairInterface } from 'ecpair'; +import * as ecc from 'tiny-secp256k1'; +import { randomBytes } from 'crypto'; + +const { p2mr } = payments; + +// Initialize ECPair with the ECC library +const ECPair = ECPairFactory(ecc); + +// Create a secure RNG function +const rng = (size: number) => randomBytes(size); + +function signAndVerify( + keyPair: ECPairInterface, + xOnlyPubkey: Uint8Array, + message: Buffer, +) { + const hash = Buffer.from(bitcoinCrypto.hash256(message)); + const schnorrSignature = Buffer.from(keyPair.signSchnorr(hash)); + const signatureWithSighashDefault = Buffer.concat([schnorrSignature, Buffer.from([0x00])]); + const verified = keyPair.verifySchnorr(hash, schnorrSignature); + + return { + message, + hash, + signature: schnorrSignature, + signatureWithSighashDefault, + verified, + }; +} + +/** + * Example 1: Construct a P2MR address from a script tree with a single leaf + * This is the simplest case - a script tree containing one script. + */ +function example1_simpleScriptTree() { + console.log('=== Example 1: P2MR from simple script tree ==='); + + // Generate a key pair + const keyPair = ECPair.makeRandom({ rng }); + const pubkey = keyPair.publicKey; + const xOnlyPubkey = ecc.xOnlyPointFromPoint(pubkey); + + // Compile the script: x-only pubkey OP_CHECKSIG (BIP342 Schnorr signature) + const script = bscript.compile([Buffer.from(xOnlyPubkey), bscript.OPS.OP_CHECKSIG]); + + // Create a script tree with one leaf + const scriptTree = { + output: script, + }; + + // Construct the P2MR payment + const payment = p2mr({ + scriptTree: scriptTree, + }); + + console.log('Generated compressed pubkey:', pubkey.toString('hex')); + console.log('X-only pubkey:', Buffer.from(xOnlyPubkey).toString('hex')); + console.log('Script tree:', { output: bscript.toASM(script) }); + console.log('P2MR Address:', payment.address); + console.log('Output script:', bscript.toASM(payment.output!)); + console.log('Merkle root hash:', payment.hash ? Buffer.from(payment.hash).toString('hex') : undefined); + const message = Buffer.from('P2MR demo - example 1', 'utf8'); + const result = signAndVerify(keyPair, xOnlyPubkey, message); + + console.log('Message:', result.message.toString('utf8')); + console.log('Hash256(message):', result.hash.toString('hex')); + console.log('Schnorr signature (64-byte):', result.signature.toString('hex')); + console.log('Signature + default sighash (65-byte witness element):', result.signatureWithSighashDefault.toString('hex')); + console.log('Signature valid:', result.verified); + console.log('Witness stack for spend:', [result.signatureWithSighashDefault.toString('hex'), bscript.toASM(script)]); + console.log(); +} + +/** + * Example 2: Construct a P2MR address from a script tree with multiple leaves + * This demonstrates a more complex script tree structure. + */ +function example2_multiLeafScriptTree() { + console.log('=== Example 2: P2MR from multi-leaf script tree ==='); + + // Generate two different key pairs for the leaves + const keyPair1 = ECPair.makeRandom({ rng }); + const keyPair2 = ECPair.makeRandom({ rng }); + const pubkey1 = keyPair1.publicKey; + const pubkey2 = keyPair2.publicKey; + const xOnlyPubkey1 = ecc.xOnlyPointFromPoint(pubkey1); + const xOnlyPubkey2 = ecc.xOnlyPointFromPoint(pubkey2); + + const script1 = bscript.compile([Buffer.from(xOnlyPubkey1), bscript.OPS.OP_CHECKSIG]); + const script2 = bscript.compile([Buffer.from(xOnlyPubkey2), bscript.OPS.OP_CHECKSIG]); + + // Create a script tree with two leaves (array of two leaf objects) + const scriptTree: Taptree = [ + { output: script1 }, + { output: script2 }, + ]; + + // Construct the P2MR payment + const payment = p2mr({ + scriptTree: scriptTree, + }); + + console.log('Generated compressed public keys:'); + console.log(' Pubkey 1:', pubkey1.toString('hex')); + console.log(' Pubkey 2:', pubkey2.toString('hex')); + console.log('X-only pubkeys:'); + console.log(' X-only 1:', Buffer.from(xOnlyPubkey1).toString('hex')); + console.log(' X-only 2:', Buffer.from(xOnlyPubkey2).toString('hex')); + console.log('Script tree leaves:'); + console.log(' Leaf 1:', bscript.toASM(script1)); + console.log(' Leaf 2:', bscript.toASM(script2)); + console.log('P2MR Address:', payment.address); + console.log('Output script:', bscript.toASM(payment.output!)); + console.log('Merkle root hash:', payment.hash ? Buffer.from(payment.hash).toString('hex') : undefined); + const message1 = Buffer.from('P2MR demo - example 2 leaf 1', 'utf8'); + const message2 = Buffer.from('P2MR demo - example 2 leaf 2', 'utf8'); + const result1 = signAndVerify(keyPair1, xOnlyPubkey1, message1); + const result2 = signAndVerify(keyPair2, xOnlyPubkey2, message2); + + console.log('Leaf 1 signature info:'); + console.log(' Message:', result1.message.toString('utf8')); + console.log(' Hash256(message):', result1.hash.toString('hex')); + console.log(' Schnorr signature (64-byte):', result1.signature.toString('hex')); + console.log(' Signature + default sighash (65-byte):', result1.signatureWithSighashDefault.toString('hex')); + console.log(' Signature valid:', result1.verified); + console.log(' Witness stack:', [result1.signatureWithSighashDefault.toString('hex'), bscript.toASM(script1)]); + + console.log('Leaf 2 signature info:'); + console.log(' Message:', result2.message.toString('utf8')); + console.log(' Hash256(message):', result2.hash.toString('hex')); + console.log(' Schnorr signature (64-byte):', result2.signature.toString('hex')); + console.log(' Signature + default sighash (65-byte):', result2.signatureWithSighashDefault.toString('hex')); + console.log(' Signature valid:', result2.verified); + console.log(' Witness stack:', [result2.signatureWithSighashDefault.toString('hex'), bscript.toASM(script2)]); + console.log(); +} + +/** + * Example 4: Construct a P2MR address from a hash and redeem script + * This demonstrates creating a P2MR when you have the hash directly. + */ +function example3_fromHashAndRedeem() { + console.log('=== Example 3: P2MR from hash and redeem script ==='); + + // Generate a key pair + const keyPair = ECPair.makeRandom({ rng }); + const pubkey = keyPair.publicKey; + const xOnlyPubkey = ecc.xOnlyPointFromPoint(pubkey); + const redeemScript = bscript.compile([Buffer.from(xOnlyPubkey), bscript.OPS.OP_CHECKSIG]); + + // Use a known hash (from test fixtures) + const hash = Buffer.from( + 'b424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26', + 'hex', + ); + + // Construct the P2MR payment + const payment = p2mr({ + hash: hash, + redeem: { + output: redeemScript, + }, + }); + + console.log('Generated compressed pubkey:', pubkey.toString('hex')); + console.log('X-only pubkey:', Buffer.from(xOnlyPubkey).toString('hex')); + console.log('Redeem script:', bscript.toASM(redeemScript)); + console.log('Hash:', hash.toString('hex')); + console.log('P2MR Address:', payment.address); + console.log('Output script:', bscript.toASM(payment.output!)); + const message = Buffer.from('P2MR demo - example 3', 'utf8'); + const result = signAndVerify(keyPair, xOnlyPubkey, message); + + console.log('Message:', result.message.toString('utf8')); + console.log('Hash256(message):', result.hash.toString('hex')); + console.log('Schnorr signature (64-byte):', result.signature.toString('hex')); + console.log('Signature + default sighash (65-byte):', result.signatureWithSighashDefault.toString('hex')); + console.log('Signature valid:', result.verified); + console.log('Witness stack:', [result.signatureWithSighashDefault.toString('hex'), bscript.toASM(redeemScript)]); + console.log(); +} + +// Run all examples +console.log('P2MR Address Construction Examples\n'); +console.log('=====================================\n'); + +example1_simpleScriptTree(); +example2_multiLeafScriptTree(); +example3_fromHashAndRedeem(); + +console.log('====================================='); +console.log('All examples completed!'); diff --git a/bip-0360/ref-impl/js/src/test-npm-pqc-package.js b/bip-0360/ref-impl/js/src/test-npm-pqc-package.js new file mode 100644 index 0000000000..a16e50dff3 --- /dev/null +++ b/bip-0360/ref-impl/js/src/test-npm-pqc-package.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +/** + * Node.js test script for Bitcoin PQC WASM module (High-Level API) + * + * Usage: node test-npm-package.js + * + * This script tests the high-level TypeScript wrapper API (index.js) from the + * command line, which provides a cleaner interface than the low-level API. + */ + +import { randomBytes } from 'node:crypto'; + +// Load the high-level WASM module +let bitcoinpqc; +let Algorithm; + +try { + const module = await import('@jbride/bitcoinpqc-wasm'); + bitcoinpqc = module.bitcoinpqc || module.default; + Algorithm = module.Algorithm; + + if (!bitcoinpqc || !Algorithm) { + throw new Error('Failed to import bitcoinpqc or Algorithm from @jbride/bitcoinpqc-wasm'); + } +} catch (error) { + console.error('Failed to load WASM module:', error); + console.error('Make sure you have installed the @jbride/bitcoinpqc-wasm package before running this test.'); + process.exit(1); +} + +// Helper function to generate random bytes +function generateRandomBytes(length) { + const array = new Uint8Array(length); + const bytes = randomBytes(length); + array.set(bytes); + return array; +} + +// Test function +async function testAlgorithm(algorithm, name) { + console.log(`\nTesting ${name} algorithm:`); + console.log('------------------------'); + + try { + // Get key and signature sizes + const pkSize = bitcoinpqc.publicKeySize(algorithm); + const skSize = bitcoinpqc.secretKeySize(algorithm); + const sigSize = bitcoinpqc.signatureSize(algorithm); + + console.log(`Public key size: ${pkSize} bytes`); + console.log(`Secret key size: ${skSize} bytes`); + console.log(`Signature size: ${sigSize} bytes`); + + // Generate random data for key generation + const randomData = generateRandomBytes(128); + + // Generate a key pair + const keygenStart = Date.now(); + const keypair = bitcoinpqc.generateKeypair(algorithm, randomData); + const keygenDuration = Date.now() - keygenStart; + console.log(`Key generation time: ${keygenDuration} ms`); + + // Create a message to sign + const messageText = 'This is a test message for PQC signature verification'; + const message = Buffer.from(messageText, 'utf8'); + const messageUint8 = new Uint8Array(message); + console.log(`Message to sign: "${messageText}"`); + console.log(`Message length: ${message.length} bytes`); + + // Sign the message + const signStart = Date.now(); + let signature; + try { + signature = bitcoinpqc.sign(keypair.secretKey, messageUint8, algorithm); + const signDuration = Date.now() - signStart; + console.log(`Signing time: ${signDuration} ms`); + console.log(`Actual signature size: ${signature.size} bytes`); + } catch (error) { + const signDuration = Date.now() - signStart; + console.log(`Signing failed after ${signDuration} ms`); + console.log(`Error: ${error.message}`); + if (algorithm === Algorithm.SLH_DSA_SHAKE_128S) { + console.log(''); + console.log('⚠️ NOTE: SLH-DSA-SHAKE-128s signing is currently experiencing'); + console.log(' issues when compiled to WebAssembly. This appears to be a'); + console.log(' bug in the SPHINCS+ reference implementation when compiled'); + console.log(' to WASM. ML-DSA-44 (Dilithium) works correctly.'); + console.log(''); + console.log(' Key generation succeeded, but signing failed.'); + console.log(' This is a known limitation of the browser/WASM build.'); + } + throw error; + } + + // Verify the signature + const verifyStart = Date.now(); + const verifyResult = bitcoinpqc.verify( + keypair.publicKey, + messageUint8, + signature, + algorithm + ); + const verifyDuration = Date.now() - verifyStart; + + if (verifyResult) { + console.log('Signature verified successfully!'); + } else { + console.log('ERROR: Signature verification failed!'); + } + console.log(`Verification time: ${verifyDuration} ms`); + + // Try to verify with a modified message + const modifiedMessageText = 'This is a MODIFIED message for PQC signature verification'; + const modifiedMessage = Buffer.from(modifiedMessageText, 'utf8'); + const modifiedMessageUint8 = new Uint8Array(modifiedMessage); + console.log(`Modified message: "${modifiedMessageText}"`); + const modifiedVerifyResult = bitcoinpqc.verify( + keypair.publicKey, + modifiedMessageUint8, + signature, + algorithm + ); + + if (modifiedVerifyResult) { + console.log('ERROR: Signature verified for modified message!'); + } else { + console.log('Correctly rejected signature for modified message'); + } + + console.log('✓ Test passed!\n'); + return true; + } catch (error) { + console.error(`❌ Error: ${error.message}`); + if (error.stack) { + console.error(error.stack); + } + return false; + } +} + +async function runTests() { + console.log('Bitcoin PQC Library Example (Node.js - High-Level API)'); + console.log('======================================================\n'); + console.log('This example tests the post-quantum signature algorithms designed for BIP-360 and the Bitcoin QuBit soft fork.'); + console.log('Using the high-level TypeScript wrapper API (index.js).\n'); + + // Initialize the module + try { + console.log('Initializing WASM module...'); + await bitcoinpqc.init({ + onRuntimeInitialized: () => { + console.log('✓ WASM module initialized successfully!\n'); + }, + print: (text) => { + // Enable WASM print output for debugging + console.log('WASM:', text); + }, + printErr: (text) => { + console.error('WASM Error:', text); + }, + // Node.js-specific: provide crypto.getRandomValues + getRandomValues: (arr) => { + const bytes = randomBytes(arr.length); + arr.set(bytes); + return arr; + } + }); + } catch (error) { + console.error('Failed to initialize module:', error); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } + + const results = []; + + // Test ML-DSA-44 + results.push(await testAlgorithm(Algorithm.ML_DSA_44, 'ML-DSA-44')); + + // Test SLH-DSA-Shake-128s + results.push(await testAlgorithm(Algorithm.SLH_DSA_SHAKE_128S, 'SLH-DSA-Shake-128s')); + + // Summary + console.log('\n======================================================'); + console.log('Test Summary:'); + console.log(` ML-DSA-44: ${results[0] ? '✓ PASSED' : '✗ FAILED'}`); + console.log(` SLH-DSA-Shake-128s: ${results[1] ? '✓ PASSED' : '✗ FAILED'}`); + console.log('======================================================\n'); + + const exitCode = results.every(r => r) ? 0 : 1; + process.exit(exitCode); +} + +// Start +runTests(); diff --git a/bip-0360/ref-impl/js/tsconfig.json b/bip-0360/ref-impl/js/tsconfig.json new file mode 100644 index 0000000000..b181afe130 --- /dev/null +++ b/bip-0360/ref-impl/js/tsconfig.json @@ -0,0 +1,45 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "include": [ + "src/**/*" + ], + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": ["node"], + // For nodejs: + // "lib": ["esnext"], + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} diff --git a/bip-0360/ref-impl/python/.gitignore b/bip-0360/ref-impl/python/.gitignore new file mode 100644 index 0000000000..b6e47617de --- /dev/null +++ b/bip-0360/ref-impl/python/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/bip-0360/ref-impl/rust/.cargo/config.toml b/bip-0360/ref-impl/rust/.cargo/config.toml new file mode 100644 index 0000000000..04bd2901e0 --- /dev/null +++ b/bip-0360/ref-impl/rust/.cargo/config.toml @@ -0,0 +1,2 @@ +[registries.kellnr-denver-space] +index = "sparse+https://crates.denver.space/api/v1/crates/" diff --git a/bip-0360/ref-impl/rust/Cargo.lock b/bip-0360/ref-impl/rust/Cargo.lock new file mode 100644 index 0000000000..e635f4bd5a --- /dev/null +++ b/bip-0360/ref-impl/rust/Cargo.lock @@ -0,0 +1,905 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin-p2mr-pqc" +version = "0.32.6-p2mr-pqc.1" +source = "sparse+https://crates.denver.space/api/v1/crates/" +checksum = "ce8a80e619111bf8d228f3f1b169a2487e6e28974f39c8935657640e0214f9ed" +dependencies = [ + "base58ck", + "base64", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1 0.29.1", + "serde", +] + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitcoinpqc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf74aafaea8106c29daed19657c3952a4f297d44fbd437d5dd697772bb463fc2" +dependencies = [ + "bindgen", + "bitmask-enum", + "cmake", + "hex", + "libc", + "secp256k1 0.31.1", + "serde", +] + +[[package]] +name = "bitcoinpqc" +version = "0.3.0" +source = "sparse+https://crates.denver.space/api/v1/crates/" +checksum = "5fbdb2a3ebd6701c141909f4b7f16165b848481c5c3f37a8bd7b273304ead5a6" +dependencies = [ + "bindgen", + "bitmask-enum", + "cmake", + "hex", + "libc", + "secp256k1 0.31.1", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bitmask-enum" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6cbbb8f56245b5a479b30a62cdc86d26e2f35c2b9f594bc4671654b03851380" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniscript" +version = "13.0.0-p2mr-pqc-1.0" +source = "sparse+https://crates.denver.space/api/v1/crates/" +checksum = "aae6f58a3f729d916bf610da66391345706d36115a346efd0041be21c4fa058f" +dependencies = [ + "bech32", + "bitcoin-p2mr-pqc", + "bitcoinpqc 0.2.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "p2mr-ref" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitcoin-p2mr-pqc", + "bitcoinpqc 0.3.0", + "env_logger", + "hex", + "log", + "miniscript", + "once_cell", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "regex" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.2", + "secp256k1-sys 0.11.0", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/bip-0360/ref-impl/rust/Cargo.toml b/bip-0360/ref-impl/rust/Cargo.toml new file mode 100644 index 0000000000..1c94d5f46b --- /dev/null +++ b/bip-0360/ref-impl/rust/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "p2mr-ref" +version = "0.1.0" +edition = "2024" + +[dependencies] + +# Dev version of miniscript crate re-exports bitcoin 0.32.6 +# view configuration for "kellnr-denver-space": +# cat .cargo/config.toml +miniscript = { version="=13.0.0-p2mr-pqc-1.0", registry="kellnr-denver-space" } +bitcoin = { package = "bitcoin-p2mr-pqc", version="0.32.6-p2mr-pqc.1", features = ["rand-std", "serde", "base64"], registry = "kellnr-denver-space" } +bitcoinpqc = { version="0.3.0", features = ["serde"], registry="kellnr-denver-space" } + +# BDK Wallet with P2MR support +#bdk_wallet = { version = "=3.0.0-alpha.0-pqc-0.1", registry = "kellnr-denver-space" } + +env_logger = "0.11.5" +log = "0.4.22" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +once_cell = "1.19" +hex = "0.4.3" +anyhow = "1.0.98" +thiserror = "2.0.12" +rand = "0.9" + +[patch.crates-io] + +#bitcoin = { git = "https://github.com/jbride/rust-bitcoin.git", branch = "p2mr" } + +# Verify: +# cargo update +# cargo tree -p bitcoin | more +# bitcoin = { path = "./rust-bitcoin/bitcoin" } + +# cargo tree -p miniscript | more +#miniscript = { path = "./rust-miniscript" } + +# bitcoinpqc = { path = "./libbitcoinpqc" } diff --git a/bip-0360/ref-impl/rust/README.md b/bip-0360/ref-impl/rust/README.md new file mode 100644 index 0000000000..52bc7327ad --- /dev/null +++ b/bip-0360/ref-impl/rust/README.md @@ -0,0 +1,45 @@ + +# p2mr test vectors + +This rust project contains the test vectors for BIP-360 + + +## Run Test Vectors + +These test vectors are being developed in conjunction with forks of [rust-bitcoin](https://github.com/jbride/rust-bitcoin/tree/p2mr) and [rust-miniscript](https://github.com/jbride/rust-miniscript/tree/p2mr-pqc) customized with p2mr functionality. + + +1. environment variables + ``` + // Specify Bitcoin network used when generating bip350 (bech32m) address + // Options: regtest, testnet, signet + // Default: mainnet + $ export BITCOIN_NETWORK= + ``` + +1. run a specific test: + ``` + $ cargo test test_p2mr_single_leaf_script_tree -- --nocapture + ``` + +## Local Development + + +All P2MR/PQC enabled bitcoin crates are temporarily available in a custom crate registry at: `https://crates.denver.space`. +These crates will be made available in `crates.io` in the near future. + +Subsequently, you will need to execute the following at the root of your rust workspace: + +```bash +mkdir .cargo \ + && echo '[registries.kellnr-denver-space] +index = "sparse+https://crates.denver.space/api/v1/crates/"' > .cargo/config +``` + +Afterwards, for all P2MR/PQC enabled dependencies used in your project, include a "registry" similar to the following: + +```bash +bitcoin = { version="0.32.6", registry = "kellnr-denver-space" } +``` + + diff --git a/bip-0360/ref-impl/rust/docs/development_notes.adoc b/bip-0360/ref-impl/rust/docs/development_notes.adoc new file mode 100644 index 0000000000..270b05ac63 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/development_notes.adoc @@ -0,0 +1,174 @@ + +== bitcoin core + +=== Two Different Size Limits: + +* *MAX_SCRIPT_ELEMENT_SIZE* (in interpreter.cpp line 1882) - This is a consensus rule that limits individual stack elements to 520 bytes. This is what's currently blocking your SLH-DSA signature. +* *MAX_STANDARD_P2MR_STACK_ITEM_SIZE* (in policy.h) - This is a policy rule that limits P2MR stack items to 80 bytes (or 8000 bytes with your change) for standardness. + +== P2MR changes to rust-bitcoin + +# 1. p2mr module + +The p2mr branch of rust-bitcoin includes a new module: `p2mr`. + +Source code for this new module can be found [here](https://github.com/jbride/rust-bitcoin/blob/p2mr/bitcoin/src/p2mr/mod.rs). + +Highlights of this _p2mr_ module as follows: + +## 1.1. p2mrBuilder + +This is struct inherits from the rust-bitcoin _TaprootBuilder_. +It has an important modification in that it disables keypath spend. + +Similar to its Taproot parent, p2mrBuilder provides functionality to add leaves to a TapTree. +One its TapTree has been fully populated with all leaves, an instance of _p2mrSpendInfo_ can be retrieved from p2mrBuilder. + + +``` +pub struct p2mrBuilder { + inner: TaprootBuilder +} + +impl p2mrBuilder { + + /// Creates a new p2mr builder. + pub fn new() -> Self { + Self { + inner: TaprootBuilder::new() + } + } + + /// Adds a leaf to the p2mr builder. + pub fn add_leaf_with_ver( + self, + depth: u8, + script: ScriptBuf, + leaf_version: LeafVersion, + ) -> Result { + match self.inner.add_leaf_with_ver(depth, script, leaf_version) { + Ok(builder) => Ok(Self { inner: builder }), + Err(_) => Err(p2mrError::LeafAdditionError) + } + } + + /// Finalizes the p2mr builder. + pub fn finalize(self) -> Result { + let node_info: NodeInfo = self.inner.try_into_node_info().unwrap(); + Ok(p2mrSpendInfo { + merkle_root: Some(node_info.node_hash()), + //script_map: self.inner.script_map().clone(), + }) + } + + /// Converts the p2mr builder into a Taproot builder. + pub fn into_inner(self) -> TaprootBuilder { + self.inner + } +} +``` + +## 1.2. p2mrSpendInfo + +Provides merkle_root of a completed p2mr TapTree + +``` +/// A struct for p2mr spend information. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct p2mrSpendInfo { + + /// The merkle root of the script path. + pub merkle_root: Option + +} +``` + +## 1.3. p2mrScriptBuf + +Allows for creation of a p2mr scriptPubKey UTXO using only the merkle root of a script tree only. + +``` +/// A wrapper around ScriptBuf for p2mr (Pay to Quantum Resistant Hash) scripts. +pub struct p2mrScriptBuf { + inner: ScriptBuf +} + +impl p2mrScriptBuf { + /// Creates a new p2mr script from a ScriptBuf. + pub fn new(inner: ScriptBuf) -> Self { + Self { inner } + } + + /// Generates p2mr scriptPubKey output + /// Only accepts the merkle_root (of type TapNodeHash) + /// since keypath spend is disabled in p2mr + pub fn new_p2mr(merkle_root: TapNodeHash) -> Self { + // https://github.com/cryptoquick/bips/blob/p2mr/bip-0360.mediawiki#scriptpubkey + let merkle_root_hash_bytes: [u8; 32] = merkle_root.to_byte_array(); + let script = Builder::new() + .push_opcode(OP_PUSHNUM_3) + + // automatically pre-fixes with OP_PUSHBYTES_32 (as per size of hash) + .push_slice(&merkle_root_hash_bytes) + + .into_script(); + p2mrScriptBuf::new(script) + } + + /// Returns the script as a reference. + pub fn as_script(&self) -> &Script { + self.inner.as_script() + } +} +``` + +## 1.4. p2mr Control Block + +Closely related to P2TR control block. +Difference being that _internal public key_ is not included. + + +``` +/// A control block for p2mr (Pay to Quantum Resistant Hash) script path spending. +/// This is a simplified version of Taproot's control block that excludes key-related fields. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct p2mrControlBlock { + /// The version of the leaf. + pub leaf_version: LeafVersion, + /// The merkle branch of the leaf. + pub merkle_branch: TaprootMerkleBranch, +} +``` + +# 2. Witness Program + +New p2mr related functions that allow for creation of a new V3 _witness program_ given a merkle_root only. + +Found in bitcoin/src/blockdata/script/witness_program.rs + +``` +/// Creates a [`WitnessProgram`] from a 32 byte merkle root. +fn new_p2mr(program: [u8; 32]) -> Self { + WitnessProgram { version: WitnessVersion::V3, program: ArrayVec::from_slice(&program) } +} + +/// Creates a pay to quantum resistant hash address from a merkle root. +pub fn p2mr(merkle_root: Option) -> Self { + let merkle_root = merkle_root.unwrap(); + WitnessProgram::new_p2mr(merkle_root.to_byte_array()) +} +``` + +# 3. Address + +New _p2mr_ function that allows for creation of a new _p2mr_ Address given a merkle_root only. + +Found in bitcoin/src/address/mod.rs + +``` +/// Creates a pay to quantum resistant hash address from a merkle root. +pub fn p2mr(merkle_root: Option, hrp: impl Into) -> Address { + let program = WitnessProgram::p2mr(merkle_root); + Address::from_witness_program(program, hrp) +} +``` diff --git a/bip-0360/ref-impl/rust/docs/images/crypto_key_characteristics.png b/bip-0360/ref-impl/rust/docs/images/crypto_key_characteristics.png new file mode 100644 index 0000000000..901d57a859 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/crypto_key_characteristics.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/faucet_1.png b/bip-0360/ref-impl/rust/docs/images/faucet_1.png new file mode 100644 index 0000000000..826057475b Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/faucet_1.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/faucet_2.png b/bip-0360/ref-impl/rust/docs/images/faucet_2.png new file mode 100644 index 0000000000..4ac08e24c4 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/faucet_2.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/funding_utxo_id.png b/bip-0360/ref-impl/rust/docs/images/funding_utxo_id.png new file mode 100644 index 0000000000..e409d695bb Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/funding_utxo_id.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/mempool_next_block.png b/bip-0360/ref-impl/rust/docs/images/mempool_next_block.png new file mode 100644 index 0000000000..89754f1411 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/mempool_next_block.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_1.png b/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_1.png new file mode 100644 index 0000000000..14aa99bc94 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_1.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_2.png b/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_2.png new file mode 100644 index 0000000000..e92fada18c Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/mempool_spending_tx_2.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/merkletree.png b/bip-0360/ref-impl/rust/docs/images/merkletree.png new file mode 100644 index 0000000000..a4ed868819 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/merkletree.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/p2mr_construction.png b/bip-0360/ref-impl/rust/docs/images/p2mr_construction.png new file mode 100644 index 0000000000..9ac23a5f93 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/p2mr_construction.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/p2mr_witness.png b/bip-0360/ref-impl/rust/docs/images/p2mr_witness.png new file mode 100644 index 0000000000..b1e7c72c85 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/p2mr_witness.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/tap_tree_concatenated.png b/bip-0360/ref-impl/rust/docs/images/tap_tree_concatenated.png new file mode 100644 index 0000000000..7a392eecc9 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/tap_tree_concatenated.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/tap_tree_mixed.png b/bip-0360/ref-impl/rust/docs/images/tap_tree_mixed.png new file mode 100644 index 0000000000..d4c0a7d6b1 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/tap_tree_mixed.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/tap_tree_schnorr_only.png b/bip-0360/ref-impl/rust/docs/images/tap_tree_schnorr_only.png new file mode 100644 index 0000000000..9445608596 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/tap_tree_schnorr_only.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/tap_tree_slh_dsa_only.png b/bip-0360/ref-impl/rust/docs/images/tap_tree_slh_dsa_only.png new file mode 100644 index 0000000000..eb51b26fc9 Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/tap_tree_slh_dsa_only.png differ diff --git a/bip-0360/ref-impl/rust/docs/images/workshop_deployment_arch.png b/bip-0360/ref-impl/rust/docs/images/workshop_deployment_arch.png new file mode 100644 index 0000000000..8435e2181a Binary files /dev/null and b/bip-0360/ref-impl/rust/docs/images/workshop_deployment_arch.png differ diff --git a/bip-0360/ref-impl/rust/docs/p2mr-end-to-end.adoc b/bip-0360/ref-impl/rust/docs/p2mr-end-to-end.adoc new file mode 100644 index 0000000000..f09c5bc117 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/p2mr-end-to-end.adoc @@ -0,0 +1,527 @@ +:scrollbar: +:data-uri: +:toc2: +:linkattrs: + += P2MR End-to-End Tutorial + +:numbered: + +This tutorial is inspired by the link:https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature[script-path-spend-signature] example of the _learnmeabitcoin_ tutorial. + +It is customized to create, fund and spend from a P2MR UTXO to a P2WPKH address. + +In addition, this tutorial allows for the (un)locking mechanism of the script to optionally use _Post Quantum Cryptography_ (PQC). + +The purpose of this tutorial is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] `p2mr` UTXO (optionally using _Post-Quantum Cryptography_). + +The steps outlined in this tutorial are executed using a custom Bitcoin Core instance running either in `regtest` or `signet`. + +== Pre-reqs + +=== Bitcoin Core + +If participating in a workshop, your instructor will provide a bitcoin environment. +Related: your instructor should also provide you with a wallet. + +Otherwise, if running this tutorial on your own, follow the instructions in the appendix of this doc: <>. + + +=== Shell Environment + +. *docker / podman* ++ +NOTE: If you have built the custom `p2mr` enabled Bitcoin Core, you do not need docker (nor podman) installed. Skip this section. ++ +This tutorial makes use of a `p2mr` enabled _bitcoin-cli_ utility. +This utility is made available as a docker (or podman) container. +Ensure your host machine has either docker or podman installed. + +. *bitcoin-cli* command line utility: ++ +NOTE: If you have built the custom `p2mr` enabled Bitcoin Core, you can simply use the `bitcoin-cli` utility found in the `build/bin/` directory. No need to use the _dockerized_ utility described below. + +.. You will need a `bitcoin-cli` binary that is `p2mr` enabled. +For this purpose, a docker container with this `bitcoin-cli` utility is provided: ++ +----- +docker pull quay.io/jbride2000/bitcoin-cli:p2mr-pqc-0.0.1 +----- + +.. Configure an alias to the `bitcoin-cli` command that connects to your customized bitcoin-core node. ++ +----- +alias b-cli='docker run --rm --network host bitcoin-cli:p2mr-pqc-0.0.1 -rpcconnect=192.168.122.1 -rpcport=18443 -rpcuser=regtest -rpcpassword=regtest' +----- + +. *jq*: ensure json parsing utility is link:https://jqlang.org/download/[installed] and available via your $PATH. +. *awk* : standard utility for all Linux distros (often packaged as `gawk`). +. *Rust* development environment with _cargo_ utility. Use link:https://rustup[Rustup] to install. + +== Create & Fund P2MR UTXO + +The purpose of this workshop is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] _P2MR_ address (optionally using _Post-Quantum Cryptography_). + +In this section of the workshop, you create and fund a P2MR address. + +The following depicts the construction of a P2MR _TapTree_ and computation its _scriptPubKey_. + +image::images/p2mr_construction.png[] + +A P2MR address is created by adding locking scripts to leaves of a _TapTree_. +The locking scripts can use either _Schnorr_ (as per BIP-360) or _SLH-DSA_ (defined in a future BIP) cryptography. + +. Set an environment variable specific to your Bitcoin network environment (regtest, signet, etc) ++ +[source,bash] +----- +export BITCOIN_NETWORK=regtest +----- ++ +Doing so influences the P2MR address that you'll create later in this tutorial. + + +. Define number of total leaves in tap tree : ++ +[source,bash] +----- +export TOTAL_LEAF_COUNT=5 +----- + +. OPTIONAL: Indicate what type of cryptography to use in the locking scripts of your TapTree leaves. +Valid options are: `MIXED`, `SCHNORR_ONLY`, `SLH_DSA_ONLY`, and `CONCATENATED_SCHNORR_AND_SLH_DSA`. +Default is `MIXED`. ++ +[source,bash] +----- +export TAP_TREE_LOCK_TYPE=MIXED +----- + +.. If you set _TAP_TREE_LOCK_TYPE=SCHNORR_ONLY_, then the locking script of your TapTree leaves will utilize _Schnorr_ cryptography. ++ +Schnorr is not quantum-resistant. However, its signature size is relatively small: 64 bytes. +A _SCHNORR_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_schnorr_only.png[s,300] + +.. If you set _TAP_TREE_LOCK_TYPE=SLH_DSA_ONLY_, then the locking script of your TapTree leaves will utilize _SLH-DSA_ cryptography. +A _SLH_DSA_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_slh_dsa_only.png[l,300] ++ +SLH_DSA is quantum-resistant. However, the trade-off is the much larger signature size 7,856 bytes when spending. ++ +image::images/crypto_key_characteristics.png[] ++ +NOTE: PQC cryptography is made available to this BIP-360 reference implementation via the link:https://crates.io/crates/bitcoinpqc[libbitcoinpqc Rust bindings]. + +.. If you set _MIXED_, then each leaf of the taptree will consist of *either* a Schnorr based locking script or a SLH-DSA based locking script. +A _MIXED_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_mixed.png[t,300] ++ +NOTE: The benefit of constructing a taptree with a mixed set of cryptography used as locking scripts in the leaves is articulated nicely in link:https://www.bitmex.com/blog/Taproot%20Quantum%20Spend%20Paths[this article from BitMex]. + +.. If you set _TAP_TREE_LOCK_TYPE=CONCATENATED_SCHNORR_AND_SLH_DSA_, then the locking script of your TapTree leaves will be secured using both SCHNORR and SLH-DSA cryptography in a concatenated / serial manner. +Private keys for both SCHNORR and SLH-DSA will be needed when unlocking. +A _CONCATENATED_SCHNORR_AND_SLH_DSA_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_concatenated.png[p,300] + +. Set the tap leaf index to later use as the unlocking script (when spending) For example, to later spend from the 5th leaf of the tap tree: ++ +[source,bash] +----- +export LEAF_TO_SPEND_FROM=4 +----- + +. Generate a P2MR scripPubKey with multi-leaf taptree: ++ +[source,bash] +----- +export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \ + && echo $BITCOIN_ADDRESS_INFO | jq -r . +----- ++ +NOTE: In `regtest`, you can expect a P2MR address that starts with: `bcrt1z` . ++ +[subs=+quotes] +++++ +
+What just happened? +The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a `p2mr` UTXO as follows: + +
    +
  • A configurable number of leaves are generated each with their own locking script.
  • +
  • Each of these leaves are added to a Huffman tree that sorts the leaves by weight.
  • +
  • The merkle root of the tree is calculated and subsequently used to generate the p2mr witness program and BIP0350 address.
  • +
+

+The source code for the above logic is found in this project: src/lib.rs + +
+++++ + +. Only if you previously set `TAP_TREE_LOCK_TYPE=MIXED`, set the environment variable `SPENDING_LEAF_TYPE`. +Valid values are `SHNORR_ONLY` or `SLH_DSA_ONLY` based on the type of locking script that was used in the leaf you will spend from later in this lab. The logs from the previous step tell you the appropriate value to set. For instance: ++ +----- +NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows: + export SPENDING_LEAF_TYPE=SCHNORR_ONLY +----- + +. Set some env vars (for use in later steps in this tutorial) based on previous result: ++ +[source,bash] +----- +export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \ + && export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \ + && export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \ + && export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \ + && export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \ + && export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' ) +----- + +. View tapscript used in target leaf of taptree: ++ +[source,bash] +----- +b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm' +----- ++ +NOTE: If not using PQC, notice that this script commits to a Schnorr 32-byte x-only public key. +If using PQC, this script commits to a Schnorr 32-byte SLH-DSA pub key and a OP_SUCCESS127 (represented as `OP_SUBSTR`) opcode. + +. Fund this P2MR address with the coinbase reward of a newly generated block: ++ +Choose from one of the following networks: + +.. Regtest ++ +If on `regtest` network, execute the following: ++ +[source,bash] +----- +export COINBASE_REWARD_TX_ID=$( b-cli -named generatetoaddress nblocks=1 address="$P2MR_ADDR" maxtries=5 | jq -r '.[]' ) \ + && echo $COINBASE_REWARD_TX_ID +----- ++ +NOTE: Sometimes Bitcoin Core may not hit a block (even on regtest). If so, just try the above command again. + +.. Signet ++ +If on `signet` network, then execute the following: ++ +[source,bash] +----- +$BITCOIN_SOURCE_DIR/contrib/signet/miner --cli "bitcoin-cli -conf=$HOME/anduro-360/configs/bitcoin.conf.signet" generate \ + --address $P2MR_ADDR \ + --grind-cmd "$BITCOIN_SOURCE_DIR/build/bin/bitcoin-util grind" \ + --min-nbits --set-block-time $(date +%s) \ + --poolid "MARA Pool" +----- + +. view summary of all txs that have funded P2MR address ++ +[source,bash] +----- +export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \ + && echo $P2MR_DESC \ + && b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' +----- + +. grab txid of first tx with unspent funds: ++ +[source,bash] +----- +export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \ + && echo $FUNDING_TX_ID +----- + +. Set FUNDING_UTXO_INDEX env var (used later to correctly identify funding UTXO when generating the spending tx) ++ +[source,bash] +----- +export FUNDING_UTXO_INDEX=0 +----- + +. view details of funding UTXO to the P2MR address: ++ +[source,bash] +----- +export FUNDING_UTXO=$( b-cli getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \ + && echo $FUNDING_UTXO | jq -r . +----- ++ +NOTE: the above only works when Bitcoin Core is started with the following arg: -txindex + + +== Spend P2MR UTXO + +In the previous section, you created and funded a P2MR UTXO. +That UTXO includes a leaf script locked with a key-pair (optionally based on PQC) known to you. + +In this section, you spend from that P2MR UTXO. +Specifically, you will generate an appropriate _SigHash_ and sign it (to create a signature) using the known private key that unlocks the known leaf script of the P2MR UTXO. + +For the purpose of this tutorial, you will spend funds to a new P2WPKH utxo. (there is nothing novel about this P2WPKH utxo). + + +. Determine value (in sats) of the funding P2MR utxo: ++ +[source,bash] +----- +export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \ + && echo $FUNDING_UTXO_AMOUNT_SATS +----- + +. Generate additional blocks. ++ +This is necessary if you have only previously generated less than 100 blocks. ++ +Otherwise, you may see an error from bitcoin core such as the following when attempting to spend: ++ +_bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 1_ + +.. regtest ++ +[source,bash] +----- +b-cli -generate 110 +----- + +.. signet ++ +This will involve having the signet miner generate about 110 blocks .... which can take about 10 minutes. ++ +The `common/utils` directory of this project provides a script called: link:../../common/utils/signet_miner_loop.sh[signet_miner_loop.sh]. + + +. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx: ++ +[source,bash] +----- +export SPEND_DETAILS=$( cargo run --example p2mr_spend ) +----- ++ +[subs=+quotes] +++++ +
+What just happened? +The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the `p2mr` UTXO as follows: + +
    +
  • Create a transaction template (aka: SigHash) that serves as the message to be signed.
  • +
  • Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2MR tree.
  • +
  • Add this signature to the witness section of the transaction.
  • +
+

+The source code for the above logic is found in this project: src/lib.rs + +
+++++ + +. Set environment variables passed to _bitcoin-cli_ when spending: ++ +[source,bash] +----- +export RAW_P2MR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \ + && echo "RAW_P2MR_SPEND_TX = $RAW_P2MR_SPEND_TX" \ + && export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \ + && echo "SIG_HASH = $SIG_HASH" \ + && export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \ + && echo "SIG_BYTES = $SIG_BYTES" +----- + +. Inspect the spending tx: ++ +[source,bash] +----- +b-cli decoderawtransaction $RAW_P2MR_SPEND_TX +----- ++ +Pay particular attention to the `vin.txinwitness` field. +Do the three elements (script input, script and control block) of the witness stack for this script path spend make sense ? +What do you observe as the first byte of the `control block` element ? + +. Test standardness of the spending tx by sending to local mempool of p2mr enabled Bitcoin Core: ++ +[source,bash] +----- +b-cli testmempoolaccept '["'''$RAW_P2MR_SPEND_TX'''"]' +----- + +. Submit tx: ++ +[source,bash] +----- +export P2MR_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2MR_SPEND_TX ) \ + && echo $P2MR_SPENDING_TX_ID +----- ++ +NOTE: Should return same tx id as was included in $RAW_P2MR_SPEND_TX + +== Mine P2MR Spend TX + +. View tx in mempool: ++ +[source,bash] +----- +b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 +----- ++ +NOTE: There will not yet be a field `blockhash` in the response. + +. Mine 1 block: + +.. regtest: ++ +[source,bash] +----- +b-cli -generate 1 +----- + +.. signet: ++ +If on `signet` network, then execute the following: ++ +[source,bash] +----- +$BITCOIN_SOURCE_DIR/contrib/signet/miner --cli "bitcoin-cli -conf=$HOME/anduro-360/configs/bitcoin.conf.signet" generate \ + --address $P2MR_ADDR \ + --grind-cmd "$BITCOIN_SOURCE_DIR/build/bin/bitcoin-util grind" \ + --min-nbits --set-block-time $(date +%s) \ + --poolid "MARA Pool" +----- + +. Obtain `blockhash` field of mined tx: ++ +[source,bash] +----- +export BLOCK_HASH=$( b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \ + && echo $BLOCK_HASH +----- + +. View tx in block: ++ +[source,bash] +----- +b-cli getblock $BLOCK_HASH | jq -r .tx +----- + +== Appendix + +[[build_p2mr]] +=== Build P2MR / PQC Enabled Bitcoin Core + +The link:https://github.com/jbride/bitcoin/tree/p2mr[p2mr branch] of bitcoin core is needed. + +Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here]. + +As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core: + +. Set BITCOIN_SOURCE_DIR ++ +----- +export BITCOIN_SOURCE_DIR=/path/to/root/dir/of/cloned/bitcoin/source +----- + +. build ++ +----- +cmake -B build \ + -DWITH_ZMQ=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DBUILD_BENCH=ON \ + -DBUILD_DAEMON=ON \ + -DSANITIZERS=address,undefined + +cmake --build build -j$(nproc) +----- + +. run in either `regtest` or `signet` mode: + +.. regtest: ++ +----- +./build/bin/bitcoind -daemon=0 -regtest=1 -txindex -prune=0 +----- + +.. signet: ++ +----- +./build/bin/bitcoind -daemon=0 -signet=1 -txindex -prune=0 +----- ++ +NOTE: If running in `signet`, your bitcoin core will need to be configured with the `signetchallenge` property. +link:https://edil.com.br/blog/creating-a-custom-bitcoin-signet[This tutorial] provides a nice overview of the topic. + +=== libbitcoinpqc build + +The `p2mr-pqc` branch of this project includes a dependency on the link:https://crates.io/crates/libbitcoinpqc[libbitcoinpqc crate]. +libbitcoinpqc contains native code (C/C++/ASM) and is made available to Rust projects via Rust bindings. +This C/C++/ASM code is provided in the libbitcoinpqc crate as source code (not prebuilt binaries). + +Subsequently, the `Cargo` utility needs to build this libbitcoinpqc C native code on your local machine. +You will need to have C development related libraries installed on your local machine. + +Every developer or CI machine building `p2mr-ref` must have cmake and a C toolchain installed locally. + +==== Linux + +. Debian / Ubuntu ++ +----- +sudo apt update +sudo apt install cmake build-essential clang libclang-dev +----- + +. Fedora / RHEL ++ +----- +sudo dnf5 update +sudo dnf5 install cmake make gcc gcc-c++ clang clang-libs llvm-devel +----- + +==== OSX + +[[bitcoin_core_wallet]] +=== Bitcoin Core Wallet + +This tutorial assumes that a bitcoin core wallet is available. + +. For example, the following would be sufficient: ++ +----- + +export W_NAME=anduro + +b-cli -named createwallet \ + wallet_name=$W_NAME \ + descriptors=true \ + load_on_startup=true +----- + +=== Schnorr + SLH-DSA + +----- + OP_CHECKSIG OP_SUBSTR OP_BOOLAND OP_VERIFY +----- + + +The logic flow is: + +. OP_CHECKSIG: Verify Schnorr signature against Schnorr pubkey +. OP_SUBSTR: Verify SLH-DSA signature against SLH-DSA pubkey (using OP_SUBSTR for the SLH-DSA verification) +. OP_BOOLAND: Ensure both signature verifications succeeded +. OP_VERIFY: Final verification that the script execution succeeded +. This creates a "both signatures required" locking condition, which is exactly what you want for SCHNORR_AND_SLH_DSA scripts. + + +===== Sighash bytes + +Sighash bytes are appended to each signature, instead of being separate witness elements: + +. SlhDsaOnly: SLH-DSA signature + sighash byte appended +. SchnorrOnly: Schnorr signature + sighash byte appended +. SchnorrAndSlhDsa: Schnorr signature (no sighash) + SLH-DSA signature + sighash byte appended to the last signature diff --git a/bip-0360/ref-impl/rust/docs/p2mr-signet-workshop.adoc b/bip-0360/ref-impl/rust/docs/p2mr-signet-workshop.adoc new file mode 100644 index 0000000000..0afaaac8a4 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/p2mr-signet-workshop.adoc @@ -0,0 +1,524 @@ +:scrollbar: +:data-uri: +:toc2: +:linkattrs: + += P2MR End-to-End workshop + +:numbered: + +Welcome to the BIP-360 / _Pay-To-Tap-Script-Hash_ (P2MR) workshop ! + +In this workshop, you will interact with a custom Signet environment to create, fund and spend from a _P2MR_ address. + +_P2MR_ is a new Bitcoin address type defined in link:https://bip360.org/bip360.html[bip-360]. + + +In addition, this workshop allows for the (un)locking mechanism of the leaf scripts of your P2MR address to optionally use _Post Quantum Cryptography_ (PQC). +The use of PQC is alluded to in BIP-360 and will be further defined in future BIPs. + +The steps outlined in this workshop are executed using a P2MR/PQC enabled Bitcoin Core instance running on a signet environment. +*The target audience of the workshop is Bitcoin developers and ops personnel. +As such, the workshop makes heavy use of the _bitcoin-cli_ at the command line.* + +== Pre-reqs + +=== *docker / podman* + +This workshop environment is provided as a _docker_ container. +Subsequently, ensure your host machine has either link:https://docs.docker.com/desktop/[docker] or link:https://podman.io/docs/installation[podman] installed. + +=== *p2mr_demo* docker container + +==== Obtain container + +Once docker or podman is installed on your machine, you can obtain the workshop docker container via any of the following: + +. Pull from _quay.io_ : ++ +[source,bash] +----- +sudo docker pull quay.io/jbride2000/p2mr_demo:0.1 +----- ++ +NOTE: The container image is 1.76GB in size. This approach may be slow depending on network bandwidth. + +. Download container image archive file from local HTTP server: ++ +*TO_DO* + +. Obtain container image archive file from instructor: + +.. Workshop instructors have the container image available via USB thumb drives. +If you feel comfortable with this approach, ask an instructor for a thumb drive. + +... Mount the USB thumb drive and copy for the file called: _p2mr_demo-image.tar_. +... Load the container image into your docker environment as per the following: ++ +[source,bash] +----- +docker load -i /path/to/p2mr_demo-image.tar +----- + +==== Start container + +You will need to start the workshop container using the docker infrastructure on your machine. + +. If working at the command line, the following is an example to run the container and obtain a command prompt: ++ +[source,bash] +----- + +sudo docker run -it --rm --entrypoint /bin/bash --network host \ + -e RPC_CONNECT=10.21.3.194 \ + quay.io/jbride2000/p2mr_demo:0.1 +----- + +. You should see a _bash_ shell command prompt similar to the following: ++ +----- +bip360@0aa9edf3d201:~/bips/bip-0360/ref-impl/rust$ ls + + +Cargo.lock Cargo.toml README.md docs examples src tests +----- ++ +As per the `ls` command seen above, your command prompt path defaults to the link:https://github.com/jbride/bips/tree/p2mr/bip-0360/ref-impl/rust[Rust based reference implementation] for BIP-360. + +==== Container contents +Your docker environment already includes a P2MR/PQC enabled `bitcoin-cli` utility. +In addition, an alias to this custom bitcoin-cli utility configured for the signet workshop environment has also been provided. + +. You can view this alias as follows (execute all commands within workshop container image): ++ +[source,bash] +----- +declare -f b-cli +----- ++ +You should see a response similar to the following: ++ +----- +b-cli () +{ + /usr/local/bin/bitcoin-cli -rpcconnect=${RPC_CONNECT:-192.168.122.1} -rpcport=${RPC_PORT:-18443} -rpcuser=${RPC_USER:-signet} -rpcpassword=${RPC_PASSWORD:-signet} "$@" +} +----- + +. Test interaction between your _b-cli_ utility and the workshop's signet node via the following: ++ +[source,bash] +----- +b-cli getnetworkinfo +----- ++ +[source,bash] +----- +b-cli getblockcount +----- + +. In addition, your docker environment also comes pre-installed with the following utilities needed for this workshop: + +. *jq*: json parsing utility +. *awk* +. *Rust* development environment with _cargo_ utility + +== Bitcoin Environment + +Your workshop instructors have provided you with a _P2MR/PQC_ enabled Bitcoin environment running in _signet_. + +You will send RPC commands to this custom Bitcoin node via the _b-cli_of your docker container. + +image::images/workshop_deployment_arch.png[] + +Via your browser, you will interact with the P2MR enabled _mempool.space_ for the workshop at: link:http://signet.bip360.org[signet.bip360.org]. + + +== Create & Fund P2MR Address + +The purpose of this workshop is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] _P2MR_ address (optionally using _Post-Quantum Cryptography_). + +In this section of the workshop, you create and fund a P2MR address. + +The following depicts the construction of a P2MR _TapTree_ and computation its _scriptPubKey_. + +image::images/p2mr_construction.png[] + +A P2MR address is created by adding locking scripts to leaves of a _TapTree_. +The locking scripts can use either _Schnorr_ (as per BIP-360) or _SLH-DSA_ (defined in a future BIP) cryptography. + +. Define number of total leaves in tap tree : ++ +[source,bash] +----- +export TOTAL_LEAF_COUNT=5 +----- + +. In your container image, indicate what type of cryptography to use in the locking scripts of your TapTree leaves. +Valid options are: `MIXED`, `SLH_DSA_ONLY`, `SCHNORR_ONLY`, `CONCATENATED_SCHNORR_AND_SLH_DSA`. +Default is `MIXED`. ++ +[source,bash] +----- +export TAP_TREE_LOCK_TYPE=MIXED +----- + +.. If you set _TAP_TREE_LOCK_TYPE=SCHNORR_ONLY_, then the locking script of your TapTree leaves will utilize _Schnorr_ cryptography. ++ +Schnorr is not quantum-resistant. However, its signature size is relatively small: 64 bytes. +A _SCHNORR_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_schnorr_only.png[s,300] + +.. If you set _TAP_TREE_LOCK_TYPE=SLH_DSA_ONLY_, then the locking script of your TapTree leaves will utilize _SLH-DSA_ cryptography. +A _SLH_DSA_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_slh_dsa_only.png[l,300] ++ +SLH_DSA is quantum-resistant. However, the trade-off is the much larger signature size 7,856 bytes when spending. ++ +image::images/crypto_key_characteristics.png[] ++ +NOTE: PQC cryptography is made available to this BIP-360 reference implementation via the link:https://crates.io/crates/bitcoinpqc[libbitcoinpqc Rust bindings]. + +.. If you set _MIXED_, then each leaf of the taptree will consist of *either* a Schnorr based locking script or a SLH-DSA based locking script. +A _MIXED_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_mixed.png[t,300] ++ +NOTE: The benefit of constructing a taptree with a mixed set of cryptography used as locking scripts in the leaves is articulated nicely in link:https://www.bitmex.com/blog/Taproot%20Quantum%20Spend%20Paths[this article from BitMex]. + +.. If you set _TAP_TREE_LOCK_TYPE=CONCATENATED_SCHNORR_AND_SLH_DSA_, then the locking script of your TapTree leaves will be secured using both SCHNORR and SLH-DSA cryptography in a concatenated / serial manner. +Private keys for both SCHNORR and SLH-DSA will be needed when unlocking. +A _CONCATENATED_SCHNORR_AND_SLH_DSA_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows: ++ +image::images/tap_tree_concatenated.png[p,300] + +. Set the tap leaf index to later use as the unlocking script (when spending) For example, to later spend from the 5th leaf of the tap tree: ++ +[source,bash] +----- +export LEAF_TO_SPEND_FROM=4 +----- + +. Generate a P2MR scripPubKey with multi-leaf taptree: ++ +[source,bash] +----- +export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \ + && echo $BITCOIN_ADDRESS_INFO | jq -r . +----- ++ +NOTE: In signet, you can expect a P2MR address that starts with the following prefix: `tb1z` . ++ +[subs=+quotes] +++++ +
+What just happened? +The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a P2MR UTXO as follows: + +
    +
  • A configurable number of leaves are generated each with their own locking script.
  • +
  • Each of these leaves are added to a Huffman tree that sorts the leaves by weight.
  • +
  • The merkle root of the tree is calculated and subsequently used to generate the P2MR witness program and BIP0350 address.
  • +
+

+ +
+++++ ++ +The source code for the above logic is found in this project's source file: link:../src/lib.rs[src/lib.rs] + +. Only if you previously set `TAP_TREE_LOCK_TYPE=MIXED`, set the environment variable `SPENDING_LEAF_TYPE`. Valid values are `SHNORR_ONLY` or `SLH_DSA_ONLY` based on the type of locking script that was used in the leaf you will spend from later in this lab. The logs from the previous step tell you the appropriate value to set. For instance: ++ +----- +NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows: + export SPENDING_LEAF_TYPE=SCHNORR_ONLY +----- + +. Fund this P2MR address using workshop's signet faucet + +.. In a browser tab, navigate to: link:http://faucet.bip360.org/[faucet.bip360.org]. +.. Copy-n-paste the value of `bech32m_address` (found in the json response from the previous step) ++ +image::images/faucet_1.png[] ++ +Press the `Request` button. +.. The faucet should allocate bitcoin to your address. ++ +image::images/faucet_2.png[] + +.. Click on the link of your transaction id. +This will take you a detailed view of the transaction. +Scroll down to the the _Inputs & Outputs_ section of the transaction and identify the _vout_ index of funds sent to your _P2MR_ address. ++ +image::images/funding_utxo_id.png[] + +.. Return back to your terminal and set a _FUNDING_UTXO_INDEX_ environment variable (used later to correctly identify funding UTXO when generating the spending tx) ++ +[source,bash] +----- +export FUNDING_UTXO_INDEX= +----- + +.. Return back to your browser tab at navigate to: link:https://signet.bip360.org[signet.bip360.org] and wait until a new block mines the transaction from the faucet that funded your P2MR address. ++ +image::images/mempool_next_block.png[] + +. Set some env vars (for use in later steps in this workshop) based on previous json result: ++ +[source,bash] +----- +export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \ + && export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \ + && export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \ + && export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \ + && export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \ + && export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' ) +----- + +. View tapscript used in target leaf of taptree: ++ +[source,bash] +----- +b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm' +----- ++ +NOTE: If not using Schnorr crypto, this script commits to a Schnorr 32-byte x-only public key. +If using SLH_DSA, this script commits to a 32-byte SLH-DSA pub key and a OP_SUCCESS127 (represented as `OP_SUBSTR`) opcode., +If using SCHNORR + SLH_DSA, then you should see a locking script in the leaf similar to the following: ++ +----- +886fc1edb7a8a364da65aef57343de451c1449d8a6c5b766fe150667d50d3e80 OP_CHECKSIG 479f93fbd251863c3e3e72da6e26ea82f87313da13090de10e57eca1f8b5e0f3 OP_SUBSTR OP_BOOLAND OP_VERIFY +----- + +. view summary of all txs that have funded P2MR address ++ +[source,bash] +----- +export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \ + && echo $P2MR_DESC \ + && b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' +----- ++ +NOTE: You will likely have to wait a few minutes until a new block (containing the tx that funds your P2MR address) is mined. + +. grab txid of first tx with unspent funds: ++ +[source,bash] +----- +export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \ + && echo $FUNDING_TX_ID +----- + +. view details of funding UTXO to the P2MR address: ++ +[source,bash] +----- +export FUNDING_UTXO=$( b-cli getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \ + && echo $FUNDING_UTXO | jq -r . +----- + + +== Spend P2MR UTXO + +In the previous section, you created and funded a P2MR UTXO. +That UTXO includes a leaf script locked with a key-pair (optionally based on PQC) known to you. + +In this section, you spend from that P2MR UTXO. +Specifically, you will generate an appropriate _SigHash_ and sign it (to create a signature) using the known private key that unlocks the known leaf script of the P2MR UTXO. + +The target address type that you send funds to is: P2WPKH. + + +. Determine value (in sats) of the funding P2MR utxo: ++ +[source,bash] +----- +export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \ + && echo $FUNDING_UTXO_AMOUNT_SATS +----- + +. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx: ++ +[source,bash] +----- +export SPEND_DETAILS=$( cargo run --example p2mr_spend ) +----- ++ +[subs=+quotes] +++++ +
+What just happened? +The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the P2MR UTXO as follows: + +
    +
  • Create a transaction template (aka: SigHash) that serves as the message to be signed.
  • +
  • Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2MR tree.
  • +
  • Add this signature to the witness section of the transaction.
  • +
+

+ +
+++++ ++ +The source code for the above logic is found in this project's source file: link:../src/lib.rs[src/lib.rs] + +. Set environment variables passed to _bitcoin-cli_ when spending: ++ +[source,bash] +----- +export RAW_P2MR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \ + && echo "RAW_P2MR_SPEND_TX = $RAW_P2MR_SPEND_TX" \ + && export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \ + && echo "SIG_HASH = $SIG_HASH" \ + && export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \ + && echo "SIG_BYTES = $SIG_BYTES" +----- + +. Inspect the spending tx: ++ +[source,bash] +----- +b-cli decoderawtransaction $RAW_P2MR_SPEND_TX +----- ++ +Pay particular attention to the `vin.txinwitness` field. +The following depicts the elements of a P2MR witness stack. ++ +image::images/p2mr_witness.png[] ++ +Do the three elements (script input, script and control block) of the witness stack for this _script path spend_ correspond ? +What do you observe as the first byte of the `control block` element ? + + +. Test standardness of the spending tx by sending to local mempool of P2MR enabled Bitcoin Core: ++ +[source,bash] +----- +b-cli testmempoolaccept '["'''$RAW_P2MR_SPEND_TX'''"]' +----- + +. Submit tx: ++ +[source,bash] +----- +export P2MR_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2MR_SPEND_TX ) \ + && echo $P2MR_SPENDING_TX_ID +----- ++ +NOTE: Should return same tx id as was included in $RAW_P2MR_SPEND_TX + +== Mine P2MR Spend TX + +. View tx in mempool: ++ +[source,bash] +----- +b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 +----- ++ +NOTE: There will not yet be a field `blockhash` in the response. + +. Monitor the mempool.space instance at link:http://signet.bip360.org[signet.bip360.org] until a new block is mined. + +. While still in the mempool.space instance at link:http://signet.bip360.org[signet.bip360.org], lookup your tx (denoted by: $P2MR_SPENDING_TX_ID ) in the top-right search bar: ++ +image::images/mempool_spending_tx_1.png[] ++ +Click on the `Details` button at the top-right of the `Inputs & Outputs` section. + +.. Study the elements of the `Witness. Approximately how large is each element of the witness stack? + +.. View the values of the `Previous output script` and `Previous output type` fields: ++ +image::images/mempool_spending_tx_2.png[] + +. Obtain `blockhash` field of mined tx: ++ +[source,bash] +----- +export BLOCK_HASH=$( b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \ + && echo $BLOCK_HASH +----- + +. View txs in block: ++ +[source,bash] +----- +b-cli getblock $BLOCK_HASH | jq -r .tx +----- ++ +You should see your tx (as per $P2MR_SPENDING_TX_ID) in the list. ++ +Congratulations!! You have created, funded and spent from a P2MR address. + +== Appendix + +[[build_p2mr]] +=== Build P2MR / PQC Enabled Bitcoin Core + +*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION* + +The link:https://github.com/jbride/bitcoin/tree/p2mr[p2mr branch] of bitcoin core is needed. + +Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here]. + +As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core: + +. Set BITCOIN_SOURCE_DIR ++ +----- +export BITCOIN_SOURCE_DIR=/path/to/root/dir/of/cloned/bitcoin/source +----- + +. build ++ +----- +cmake -B build \ + -DWITH_ZMQ=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DBUILD_BENCH=ON \ + -DBUILD_DAEMON=ON \ + -DSANITIZERS=address,undefined + +cmake --build build -j$(nproc) +----- + +. run bitcoind in signet mode: ++ +----- +./build/bin/bitcoind -daemon=0 -signet=1 -txindex -prune=0 +----- ++ +NOTE: If running in `signet`, your bitcoin core will need to be configured with the `signetchallenge` property. +link:https://edil.com.br/blog/creating-a-custom-bitcoin-signet[This workshop] provides a nice overview of the topic. + +=== libbitcoinpqc Rust bindings + +*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION* + +The `p2mr-pqc` branch of this project includes a dependency on the link:https://crates.io/crates/libbitcoinpqc[libbitcoinpqc crate]. +libbitcoinpqc contains native code (C/C++/ASM) and is made available to Rust projects via Rust bindings. +This C/C++/ASM code is provided in the libbitcoinpqc crate as source code (not prebuilt binaries). + +Subsequently, the `Cargo` utility needs to build this libbitcoinpqc C native code on your local machine. +You will need to have C development related libraries installed on your local machine. + +Every developer or CI machine building `p2mr-ref` must have cmake and a C toolchain installed locally. + +==== Linux + +. Debian / Ubuntu ++ +----- +sudo apt update +sudo apt install cmake build-essential clang libclang-dev +----- + +. Fedora / RHEL ++ +----- +sudo dnf5 update +sudo dnf5 install cmake make gcc gcc-c++ clang clang-libs llvm-devel +----- diff --git a/bip-0360/ref-impl/rust/docs/p2tr-end-to-end.adoc b/bip-0360/ref-impl/rust/docs/p2tr-end-to-end.adoc new file mode 100644 index 0000000000..51e0a2fdb1 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/p2tr-end-to-end.adoc @@ -0,0 +1,236 @@ +:scrollbar: +:data-uri: +:toc2: +:linkattrs: + += P2TR End-to-End Tutorial + +:numbered: + +This tutorial is inspired from the link:https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature[script-path-spend-signature] example of the _learnmeabitcoin_ tutorial. + +It is customized to create, fund and spend from a P2TR UTXO to a P2WPKH address. + +Execute in Bitcoin Core `regtest` mode. + +== Pre-reqs + +=== Bitcoin Core + +The link:https://github.com/jbride/bitcoin/tree/p2mr-pqc[p2mr branch] of bitcoin core is needed. + +Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here]. + +As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core: + +. build ++ +----- +$ cmake -B build \ + -DWITH_ZMQ=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DBUILD_BENCH=ON \ + -DSANITIZERS=address,undefined + +$ cmake --build build -j$(nproc) +----- + +. run in `regtest` mode ++ +----- +$ ./build/bin/bitcoind -daemon=0 -regtest=1 -txindex +----- + +=== Shell Environment + +. *b-reg* command line alias: ++ +Configure an alias to the `bitcoin-cli` command that connects to your customized bitcoin-core node running in `regtest` mode. +. *jq*: ensure json parsing utility is installed and available via your $PATH. +. *awk* : standard utility for all Linux distros (often packaged as `gawk`). + +=== Bitcoin Core Wallet + +This tutorial assumes that a bitcoin core wallet is available. +For example, the following would be sufficient: + +----- +$ export W_NAME=regtest \ + && export WPASS=regtest + +$ b-reg -named createwallet \ + wallet_name=$W_NAME \ + descriptors=true \ + passphrase="$WPASS" \ + load_on_startup=true +----- + +== Fund P2TR UTXO + +. OPTIONAL: Define number of leaves in tap tree as well as the tap leaf to later use as the unlocking script: ++ +----- +$ export TOTAL_LEAF_COUNT=5 \ + && export LEAF_TO_SPEND_FROM=4 +----- ++ +NOTE: Defaults are 4 leaves with the first leaf (leaf 0 ) as the script to later use as the unlocking script. + + +. Generate a P2TR scripPubKey with multi-leaf taptree: ++ +----- +$ export BITCOIN_NETWORK=regtest \ + && export BITCOIN_ADDRESS_INFO=$( cargo run --example p2tr_construction ) \ + && echo $BITCOIN_ADDRESS_INFO | jq -r . +----- ++ +NOTE: In `regtest`, you can expect a P2TR address that starts with: `bcrt1q` . ++ +NOTE: In the context of P2TR, the _tree_root_hex_ from the response is in reference to the _merkle_root_ used in this tutorial. + +. Set some env vars (for use in later steps in this tutorial) based on previous result: ++ +----- +$ export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \ + && export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \ + && export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \ + && export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \ + && export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \ + && export P2TR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' ) +----- + +. View tapscript used in target leaf of taptree: ++ +----- +$ b-reg decodescript $LEAF_SCRIPT_HEX | jq -r '.asm' +----- ++ +NOTE: Notice that this script commits to a Schnorr 32-byte x-only public key. + +. fund this P2TR address with the coinbase reward of a newly generated block: ++ +----- +$ export COINBASE_REWARD_TX_ID=$( b-reg -named generatetoaddress 1 $P2TR_ADDR 5 | jq -r '.[]' ) \ + && echo $COINBASE_REWARD_TX_ID +----- ++ +NOTE: Sometimes Bitcoin Core may not hit a block (even on regtest). If so, just try the above command again. + +. view summary of all txs that have funded P2TR address ++ +----- +$ export P2TR_DESC=$( b-reg getdescriptorinfo "addr($P2TR_ADDR)" | jq -r '.descriptor' ) \ + && echo $P2TR_DESC \ + && b-reg scantxoutset start '[{"desc": "'''$P2TR_DESC'''"}]' +----- + +. grab txid of first tx with unspent funds: ++ +----- +$ export FUNDING_TX_ID=$( b-reg scantxoutset start '[{"desc": "'''$P2TR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \ + && echo $FUNDING_TX_ID +----- + +. Set FUNDING_UTXO_INDEX env var (used later to correctly identify funding UTXO when generating the spending tx) ++ +----- +$ export FUNDING_UTXO_INDEX=0 +----- + +. view details of funding UTXO to the P2TR address: ++ +----- +$ export FUNDING_UTXO=$( b-reg getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \ + && echo $FUNDING_UTXO | jq -r . +----- ++ +NOTE: the above only works when Bitcoin Core is started with the following arg: -txindex + +== Spend P2TR UTXO + + +. Determine value (in sats) of funding utxo: ++ +----- +$ export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \ + && echo $FUNDING_UTXO_AMOUNT_SATS +----- + +. Generate additional blocks. ++ +This is necessary if you have only previously generated less than 100 blocks. ++ +----- +$ b-reg -generate 110 +----- ++ +Otherwise, you may see an error from bitcoin core such as the following when attempting to spend: ++ +_bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 1_ + +. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx: ++ +----- +$ export SPEND_DETAILS=$( cargo run --example p2tr_spend ) + +$ export RAW_P2TR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \ + && echo "RAW_P2TR_SPEND_TX = $RAW_P2TR_SPEND_TX" \ + && export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \ + && echo "SIG_HASH = $SIG_HASH" \ + && export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \ + && echo "SIG_BYTES = $SIG_BYTES" +----- + +. Inspect the spending tx: ++ +----- +$ b-reg decoderawtransaction $RAW_P2TR_SPEND_TX +----- + +. Test standardness of the spending tx by sending to local mempool of p2tr enabled Bitcoin Core: + + +----- +$ b-reg testmempoolaccept '["'''$RAW_P2TR_SPEND_TX'''"]' +----- + +. Submit tx: ++ +----- +$ export P2TR_SPENDING_TX_ID=$( b-reg sendrawtransaction $RAW_P2TR_SPEND_TX ) \ + && echo $P2TR_SPENDING_TX_ID +----- ++ +NOTE: Should return same tx id as was included in $RAW_P2TR_SPEND_TX + +== Mine P2TR Spend TX + +. View tx in mempool: ++ +----- +$ b-reg getrawtransaction $P2TR_SPENDING_TX_ID 1 +----- ++ +NOTE: There will not yet be a field `blockhash` in the response. + +. Mine 1 block: ++ +----- +$ b-reg -generate 1 +----- + +. Obtain `blockhash` field of mined tx: ++ +----- +$ export BLOCK_HASH=$( b-reg getrawtransaction $P2TR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \ + && echo $BLOCK_HASH +----- + +. View tx in block: ++ +----- +$ b-reg getblock $BLOCK_HASH | jq -r .tx +----- + +== TO-DO diff --git a/bip-0360/ref-impl/rust/docs/quantum_root_tap_tree.txt b/bip-0360/ref-impl/rust/docs/quantum_root_tap_tree.txt new file mode 100644 index 0000000000..f032d13111 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/quantum_root_tap_tree.txt @@ -0,0 +1,36 @@ + ┌───────────────────────┐ + │ tapleaf Merkle root │ + │ │ + └───────────────────────┘ + | + ┌───────────────────────┐ + │ 5 tagged_hash │ + │ QuantumRoot │ + └───────────|───────────┘ + ┌───────────|───────────┐ + ┌───────────────────────────────►│ 4 tagged_hash ◄─────────────────────┐ + │ │ TapBranch │ │ + │ └───────────────────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌─────────────┼───────────┐ + │ │ 3 tagged_hash │ + │ ┌──►│ TapBranch ◄───────┐ + │ │ └─────────────────────────┘ │ + │ │ │ + │ │ │ + ┌───────────┼────────────┐ ┌───────┼────────┐ │ + ┌─────►│ 2 tagged_hash ◄────┐ ┌─────►│ 2 tagged_hash ◄────┐ │ + │ │ TapBranch │ │ │ │ TapBranch │ │ │ + │ └────────────────────────┘ │ │ └────────────────┘ │ │ + │ │ │ │ │ + │ │ ┌─────┼────────┐ ┌───────|───-─┐ ┌──────┴──────┐ + ┌───┼──────────┐ ┌──────────┼──┐ │ 1 tagged_hash│ │1 tagged_hash│ │1 tagged_hash│ + │1 tagged_hash │ │1 tagged_hash│ │ Tapleaf │ │ Tapleaf │ │ Tapleaf │ + │ Tapleaf │ │ Tapleaf │ └──────────────┘ └─────────────┘ └─────────────┘ + └──▲───────────┘ └──────▲──────┘ ▲ ▲ ▲ + │ │ │ │ │ + version | A script version | B script version | C script version | D script version|E script diff --git a/bip-0360/ref-impl/rust/docs/stack_element_size_performance_tests.adoc b/bip-0360/ref-impl/rust/docs/stack_element_size_performance_tests.adoc new file mode 100644 index 0000000000..0addb22bc3 --- /dev/null +++ b/bip-0360/ref-impl/rust/docs/stack_element_size_performance_tests.adoc @@ -0,0 +1,358 @@ +:scrollbar: +:data-uri: +:toc2: +:linkattrs: + += Stack Element Size Performance Tests + +:numbered: + +== Overview + +BIP-0360 proposes an increase in stack element size from current 520 bytes (v0.29) to 8kb. + +Subsequently, there is a need to determine the performance and stability related consequences of doing so. + +== Regression Tests + +The following regression tests failed with `MAX_SCRIPT_ELEMENT_SIZE` set to 8000 . + +[cols="1,1,2"] +|=== +|feature_taproot.py | line 1338 | Missing error message 'Push value size limit exceeded' from block response 'mandatory-script-verify-flag-failed (Stack size must be exactly one after execution)' +|p2p_filter.py | lines 130-132 | Check that too large data element to add to the filter is rejected +|p2p_segwit.py | lines 1047-1049 | mandatory-script-verify-flag-failed (Push value size limit exceeded) +|rpc_createmultisig.py | lines 75-75 | No exception raised: redeemScript exceeds size limit: 684 > 520" +|=== + +**Analysis** + +These 4 tests explicitly test for a stack element size of 520 and are expected to fail with a stack element size of 8Kb. +Subsequently, no further action needed. + + +== Performance Tests + +=== OP_SHA256 + +The following Bitcoin script is used to conduct this performance test: + +----- + OP_SHA256 OP_DROP OP_1 +----- + +When executed, this script adds the pre-image array of arbitrary data to the stack. +Immediately after, a SHA256 hash function pops the pre-image array off the stack, executes a hash and adds the result to the top of the stack. +The `OP_DROP` operation removes the hash result from the stack. + + +==== Results Summary + +[cols="3,1,1,1,1,1,1,1,1,1", options="header"] +|=== +| Stack Element Size (Bytes) | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op |miss% | total +| 1 | 637.28 | 1,569,165.30 | 0.3% | 8,736.00 | 1,338.55 | 6.526 | 832.00 | 0.0% | 5.53 +| 64 | 794.85 | 1,258,098.46 | 0.4% | 11,107.00 | 1,666.92 | 6.663 | 827.00 | 0.0% | 5.61 +| 65 | 831.95 | 1,201,996.30 | 0.5% | 11,144.00 | 1,698.26 | 6.562 | 841.00 | 0.0% | 5.53 +| 100 | 794.82 | 1,258,139.86 | 0.2% | 11,139.00 | 1,673.89 | 6.655 | 837.00 | 0.0% | 5.50 +| 520 | 1,946.67 | 513,697.88 | 0.2% | 27,681.00 | 4,095.57 | 6.759 | 885.00 | 0.0% | 5.50 +| 8000 | 20,958.63 | 47,713.05 | 2.7% | 304,137.02 | 43,789.86 | 6.945 | 1,689.02 | 0.4% | 5.63 +|=== + +**Analysis** + +The following observations are made from the performance test: + +. **Performance Scaling**: The increase from 520 bytes to 8000 bytes (15.4x size increase) results in approximately 9.8x performance degradation (19,173 ns/op vs 1,947 ns/op). +This represents sub-linear scaling, which suggests the implementation handles large data efficiently. + +. **Instruction Count Scaling**: Instructions per operation increase from 27,681 to 285,220 (10.3x increase), closely matching the performance degradation, indicating the bottleneck is primarily computational rather than memory bandwidth. + +. **Throughput Impact**: Operations per second decrease from 513,698 op/s to 52,158 op/s, representing a 9.8x reduction in throughput. + +. **Cache Efficiency**: The IPC (Instructions Per Cycle) remains relatively stable (6.759 to 7.094), suggesting good CPU pipeline utilization despite the increased data size. + +. **Memory Access Patterns**: The branch mis-prediction rate increases slightly (0.0% to 0.4%), indicating minimal impact on branch prediction accuracy. + + +**key** + +[cols="1,6", options="header"] +|=== +| Metric | Description +| ns/op | Nanoseconds per operation - average time it takes to complete one benchmark iteration +| op/s | Operations per second - throughput rate showing how many benchmark iterations can be completed per second +| err% | Error percentage - statistical margin of error in the measurement, indicating the reliability of the benchmark results +| ins/op | Instructions per operation - the number of CPU instructions executed for each benchmark iteration +| cyc/op | CPU cycles per operation - the number of CPU clock cycles consumed for each benchmark iteration +| IPC | Instructions per cycle - the ratio of instructions executed per CPU cycle, indicating CPU efficiency and pipeline utilization +| bra/op | Branches per operation - the number of conditional branch instructions executed for each benchmark iteration +| miss% | Branch misprediction percentage - the rate at which the CPU incorrectly predicts branch outcomes, causing pipeline stalls +| total | Total benchmark time - the total wall-clock time spent running the entire benchmark in seconds +|=== + +==== Detailed Results + +===== Stack Element Size = 1 Byte + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op |miss% |total +|637.28 |1,569,165.30 |0.3% |8,736.00 |1,338.55 |6.526 |832.00 |0.0% |5.53 +|=== + +===== Stack Element Size = 64 Bytes + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total +| 794.85 | 1,258,098.46 | 0.4% | 11,107.00 | 1,666.92 | 6.663 | 827.00 | 0.0% | 5.61 +|=== + +====== Explanation + +Even though 64 bytes doesn't require padding (it's exactly one SHA256 block), the ins/op still increases from 8,736 to 11,107 instructions. Here's why: + +. Data Movement Overhead + + * 1 byte: Minimal data to copy into the SHA256 processing buffer + * 64 bytes: 64x more data to move from the witness stack into the SHA256 input buffer + * Memory copying operations add instructions + +. SHA256 State Initialization + + * 1 byte: The 1-byte input gets padded to 64 bytes internally, but the padding is mostly zeros + * 64 bytes: All 64 bytes are actual data that needs to be processed + * The SHA256 algorithm may have different code paths for handling "real" data vs padded data + +. Memory Access Patterns + + * 1 byte: Single byte access, likely cache-friendly + * 64 bytes: Sequential access to 64 bytes, potentially different memory access patterns + * May trigger different CPU optimizations or cache behavior + +. Bit Length Processing + + * 1 byte: The SHA256 algorithm needs to set the bit length field (8 bits) + * 64 bytes: The bit length field is 512 bits + * Different bit length values may cause different code paths in the SHA256 implementation + +. Loop Unrolling and Optimization + + * 1 byte: Compiler might optimize the single-block case differently + * 64 bytes: May use different loop structures or optimization strategies + * The SHA256 implementation might have specialized code paths for different input sizes + +. Witness Stack Operations + + * 1 byte: Small witness element, minimal stack manipulation + * 64 bytes: Larger witness element, more complex stack operations + * The Bitcoin script interpreter has to handle larger data on the stack + +The increase from 8,736 to 11,107 instructions (~27% increase) suggests that even without padding overhead, the additional data movement and processing of "real" data vs padded data adds significant instruction count. +This is a good example of how seemingly small changes in input size can affect the underlying implementation's code paths and optimization strategies. + +===== Stack Element Size = 65 Bytes + +1 byte more than the SHA256 _block_ size + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total +| 831.95 | 1,201,996.30 |0.5% |11,144.00 |1,698.26 | 6.562 |841.00 | 0.0% | 5.53 +|=== + +===== Stack Element Size = 100 Bytes + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total +| 794.82 | 1,258,139.86 | 0.2% | 11,139.00 | 1,673.89 | 6.655 | 837.00 | 0.0% | 5.50 +|=== + +===== Stack Element Size = 520 Bytes + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total +| 1,946.67 | 513,697.88 | 0.2% | 27,681.00 | 4,095.57 | 6.759 | 885.00 | 0.0% | 5.50 +|=== + +===== Stack Element Size = 8000 Bytes + +[cols="2,1,1,1,1,1,1,1,1", options="header"] +|=== +|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total +| 20,958.63 | 47,713.05 | 2.7% | 304,137.02 | 43,789.86 | 6.945 | 1,689.02 | 0.4% | 5.63 +|=== + +=== OP_DUP OP_SHA256 + +NOTE: This test is likely irrelevant as per latest BIP-0360: _To prevent OP_DUP from creating an 8 MB stack by duplicating stack elements larger than 520 bytes we define OP_DUP to fail on stack elements larger than 520 bytes_. + +This test builds off the previous (involving the hashing of large stack element data) by duplicating that stack element data. + +The following Bitcoin script is used to conduct this performance test: + +----- + OP_DUP OP_SHA256 OP_DROP OP_1 +----- + +When executed, this script adds the pre-image array of arbitrary data to the stack. +Immediately after, a `OP_DUP` operation duplicates the pre-image array on the stack. +Then, a SHA256 hash function pops the pre-image array off the stack, executes a hash and adds the result to the top of the stack. +The `OP_DROP` operation removes the hash result from the stack. + +==== Results Summary + +[cols="3,1,1,1,1,1,1,1,1,1", options="header"] +|=== +| Stack Element Size (Bytes) | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op |miss% | total +| 1 | 714.83 | 1,398,937.33 | 0.7% | 9,548.00 | 1,488.22 | 6.416 | 1,012.00 | 0.0% | 5.57 +| 64 | 858.44 | 1,164,905.19 | 0.4% | 11,911.00 | 1,800.87 | 6.614 | 999.00 | 0.0% | 5.11 +| 65 | 868.40 | 1,151,539.31 | 0.8% | 11,968.00 | 1,814.31 | 6.596 | 1,019.00 | 0.0% | 5.56 +| 100 | 864.33 | 1,156,966.91 | 0.4% | 11,963.00 | 1,809.16 | 6.612 | 1,015.00 | 0.0% | 5.49 +| 520 | 2,036.64 | 491,005.94 | 0.7% | 28,615.00 | 4,266.27 | 6.707 | 1,073.00 | 0.0% | 5.52 +| 8000 | 20,883.10 | 47,885.61 | 0.2% | 306,887.04 | 43,782.35 | 7.009 | 2,089.02 | 0.3% | 5.53 +|=== + +==== Analysis + +The following observations are made from the performance test (in comparison to the `OP_SHA256` test): + +. OP_DUP Overhead: The OP_DUP operation adds overhead by duplicating the stack element, which requires: + * Memory allocation for the duplicate + * Data copying from the original to the duplicate + * Additional stack manipulation + +. Size-Dependent Impact on ns/op: + * For small elements (1-100 bytes): Significant overhead (4.4% to 12.2%) + * For medium elements (520 bytes): Moderate overhead (4.6%) + * For large elements (8000 bytes): Negligible difference (-0.4%) + +. Instruction Count Impact: + * 8000 bytes: 304,137 → 306,887 instructions (+2,750 instructions) + * The additional instructions for OP_DUP are relatively small compared to the SHA256 computation + +. Memory Operations: ++ +The OP_DUP operation primarily affects memory operations rather than computational complexity. +This explains why the impact diminishes with larger data sizes where SHA256 computation dominates the performance. + +This analysis shows that the OP_DUP operation has a measurable but manageable performance impact, especially for larger stack elements where the computational overhead of SHA256 dominates the overall execution time. + +=== Procedure + +* Testing is done using functionality found in the link:https://github.com/jbride/bitcoin/tree/p2mr-pqc[p2mr branch] of Bitcoin Core. + +* Compilation of Bitcoin Core is done using the following `cmake` flags: ++ +----- +$ cmake \ + -B build \ + -DWITH_ZMQ=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DBUILD_BENCH=ON + +$ cmake --build build -j$(nproc) +----- + +* Bench tests are conducted similar to the following : ++ +----- +$ export PREIMAGE_SIZE_BYTES=8000 +$ ./build/bin/bench_bitcoin --filter=VerifySHA256Bench -min-time=5000 +----- + +== Failure Analysis + +Goals: + +* Measure stack memory usage to detect overflows or excessive stack growth. +* Monitor heap memory usage to identify increased allocations or leaks caused by larger elements. +* Detect memory errors (e.g., invalid reads/writes, use-after-free) that might arise from modified stack handling. +* Assess performance impacts (e.g., memory allocation overhead) in critical paths like transaction validation. + +=== Memory Errors + +AddressSanitizer is a fast, compiler-based tool (available in GCC/Clang) for detecting memory errors with lower overhead than Valgrind. + +==== Results + +No memory errors or leaks were revealed by AddressSanitizer when running the `OP_SHA256` bench test for 30 minutes. + +==== Procedure + +AddressSanitizer is included with Clang/LLVM + +. Compilation of Bitcoin Core is done using the following `cmake` flags: ++ +----- +$ cmake -B build \ + -DWITH_ZMQ=ON \ + -DBUILD_BENCH=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DSANITIZERS=address,undefined + +$ cmake --build build -j$(nproc) +----- + +. Check that ASan is statically linked to the _bench_bitcoin_ executable: ++ +----- +$ nm build/bin/bench_bitcoin | grep asan | more +0000000000148240 T __asan_address_is_poisoned +00000000000a2fe6 t __asan_check_load_add_16_R13 + +... + +000000000316c828 b _ZZN6__asanL18GlobalsByIndicatorEmE20globals_by_indicator +0000000003170ccc b _ZZN6__asanL7AsanDieEvE9num_calls +----- + +. Set the following environment variable: ++ +----- +$ export ASAN_OPTIONS="halt_on_error=0:detect_leaks=1:log_path=/tmp/asan_logs/asan" +----- ++ +Doing so ensures that _address sanitizer_ : + +.. avoids halting on the first error +.. is enable memory leak detection +.. writes ASAN related logs to a specified directory + +== Test Environment + +* Fedora 42 +* 8 cores (Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz) +* 32 GB RAM + +* OS settings: ++ +----- +$ ulimit -a +real-time non-blocking time (microseconds, -R) unlimited +core file size (blocks, -c) unlimited +data seg size (kbytes, -d) unlimited +scheduling priority (-e) 0 +file size (blocks, -f) unlimited +pending signals (-i) 126896 +max locked memory (kbytes, -l) 8192 +max memory size (kbytes, -m) unlimited +open files (-n) 1024 +pipe size (512 bytes, -p) 8 +POSIX message queues (bytes, -q) 819200 +real-time priority (-r) 0 +stack size (kbytes, -s) 8192 +cpu time (seconds, -t) unlimited +max user processes (-u) 126896 +virtual memory (kbytes, -v) unlimited +file locks (-x) unlimited +----- + +== Notes + +. test with different thread stack sizes (ie: ulimit -s xxxx ) diff --git a/bip-0360/ref-impl/rust/examples/p2mr-end-to-end.sh b/bip-0360/ref-impl/rust/examples/p2mr-end-to-end.sh new file mode 100644 index 0000000000..2029420dbd --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/p2mr-end-to-end.sh @@ -0,0 +1,30 @@ + +export BITCOIN_SOURCE_DIR=$HOME/bitcoin +export W_NAME=anduro +export USE_PQC=false +export TOTAL_LEAF_COUNT=5 +export LEAF_TO_SPEND_FROM=4 + +b-cli -named createwallet \ + wallet_name=$W_NAME \ + descriptors=true \ + load_on_startup=true + +export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \ + && echo $BITCOIN_ADDRESS_INFO | jq -r . + +export QUANTUM_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \ + && export LEAF_SCRIPT_PRIV_KEY_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_key_hex' ) \ + && export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \ + && export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \ + && export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \ + && export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' ) + +b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm' + +export COINBASE_REWARD_TX_ID=$( b-cli -named generatetoaddress 1 $P2MR_ADDR 5 | jq -r '.[]' ) \ + && echo $COINBASE_REWARD_TX_ID + +export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \ + && echo $P2MR_DESC \ + && b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' diff --git a/bip-0360/ref-impl/rust/examples/p2mr_construction.rs b/bip-0360/ref-impl/rust/examples/p2mr_construction.rs new file mode 100644 index 0000000000..517ea619b1 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/p2mr_construction.rs @@ -0,0 +1,27 @@ +use p2mr_ref::{create_p2mr_utxo, create_p2mr_multi_leaf_taptree, tap_tree_lock_type}; +use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn, LeafScriptType}; +use std::env; +use log::{info, error}; + +// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature +fn main() -> ConstructionReturn { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let tap_tree_lock_type = tap_tree_lock_type(); + info!("tap_tree_lock_type: {:?}", tap_tree_lock_type); + + let taptree_return: TaptreeReturn = create_p2mr_multi_leaf_taptree(); + let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(taptree_return.clone().tree_root_hex); + + // Alert user about SPENDING_LEAF_TYPE requirement when using MIXED mode + if tap_tree_lock_type == LeafScriptType::Mixed { + info!("NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows:"); + info!(" export SPENDING_LEAF_TYPE={}", taptree_return.leaf_script_type); + } + + return ConstructionReturn { + taptree_return: taptree_return, + utxo_return: p2mr_utxo_return, + }; +} diff --git a/bip-0360/ref-impl/rust/examples/p2mr_spend.rs b/bip-0360/ref-impl/rust/examples/p2mr_spend.rs new file mode 100644 index 0000000000..7dcbc15a63 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/p2mr_spend.rs @@ -0,0 +1,284 @@ +use p2mr_ref::{ pay_to_p2wpkh_tx, verify_schnorr_signature_via_bytes, verify_slh_dsa_via_bytes, tap_tree_lock_type }; + +use p2mr_ref::data_structures::{SpendDetails, LeafScriptType}; +use std::env; +use log::{info, error}; + +// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature +fn main() -> SpendDetails { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + // FUNDING_TX_ID environment variable is required + let funding_tx_id: String = env::var("FUNDING_TX_ID") + .unwrap_or_else(|_| { + error!("FUNDING_TX_ID environment variable is required but not set"); + std::process::exit(1); + }); + let funding_tx_id_bytes: Vec = hex::decode(funding_tx_id.clone()).unwrap(); + + // FUNDING_UTXO_AMOUNT_SATS environment variable is required + let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS") + .unwrap_or_else(|_| { + error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set"); + std::process::exit(1); + }) + .parse::() + .unwrap_or_else(|_| { + error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer"); + std::process::exit(1); + }); + + // The input index of the funding tx + // Allow override via FUNDING_UTXO_INDEX environment variable + let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index); + + // FUNDING_SCRIPT_PUBKEY environment variable is required + let funding_script_pubkey_bytes: Vec = env::var("FUNDING_SCRIPT_PUBKEY") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set"); + std::process::exit(1); + }); + + let control_block_bytes: Vec = env::var("CONTROL_BLOCK_HEX") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("CONTROL_BLOCK_HEX environment variable is required but not set"); + std::process::exit(1); + }); + info!("P2MR control block size: {}", control_block_bytes.len()); + + // TAP_TREE_LOCK_TYPE environment variable is required to determine key structure + let leaf_script_type: LeafScriptType = tap_tree_lock_type(); + info!("leaf_script_type: {:?}", leaf_script_type); + + // For Mixed trees, we need to determine the actual leaf type via SPENDING_LEAF_TYPE + let effective_leaf_type: LeafScriptType = if leaf_script_type == LeafScriptType::Mixed { + match env::var("SPENDING_LEAF_TYPE") { + Ok(value) => match value.as_str() { + "SCHNORR_ONLY" => { + info!("SPENDING_LEAF_TYPE: SCHNORR_ONLY"); + LeafScriptType::SchnorrOnly + }, + "SLH_DSA_ONLY" => { + info!("SPENDING_LEAF_TYPE: SLH_DSA_ONLY"); + LeafScriptType::SlhDsaOnly + }, + _ => { + error!("Invalid SPENDING_LEAF_TYPE '{}'. Must be SCHNORR_ONLY or SLH_DSA_ONLY", value); + std::process::exit(1); + } + }, + Err(_) => { + error!("SPENDING_LEAF_TYPE environment variable is required when TAP_TREE_LOCK_TYPE=MIXED"); + error!("Set SPENDING_LEAF_TYPE to the actual type of the leaf being spent (SCHNORR_ONLY or SLH_DSA_ONLY)."); + error!("The leaf type is returned in the 'leaf_script_type' field of the tree construction output."); + std::process::exit(1); + } + } + } else { + leaf_script_type + }; + + // Parse private keys based on effective script type + let leaf_script_priv_keys_bytes: Vec> = match effective_leaf_type { + LeafScriptType::SlhDsaOnly => { + let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX") + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SLH_DSA_ONLY"); + std::process::exit(1); + }); + // Parse JSON array and extract the first (and only) hex string + let priv_keys_hex: String = serde_json::from_str::>(&priv_keys_hex_array) + .unwrap_or_else(|_| { + error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array"); + std::process::exit(1); + }) + .into_iter() + .next() + .unwrap_or_else(|| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty"); + std::process::exit(1); + }); + let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap(); + if priv_keys_bytes.len() != 64 { + error!("SLH-DSA private key must be 64 bytes, got {}", priv_keys_bytes.len()); + std::process::exit(1); + } + vec![priv_keys_bytes] + }, + LeafScriptType::SchnorrOnly => { + let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX") + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_ONLY"); + std::process::exit(1); + }); + // Parse JSON array and extract the first (and only) hex string + let priv_keys_hex: String = serde_json::from_str::>(&priv_keys_hex_array) + .unwrap_or_else(|_| { + error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array"); + std::process::exit(1); + }) + .into_iter() + .next() + .unwrap_or_else(|| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty"); + std::process::exit(1); + }); + let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap(); + if priv_keys_bytes.len() != 32 { + error!("Schnorr private key must be 32 bytes, got {}", priv_keys_bytes.len()); + std::process::exit(1); + } + vec![priv_keys_bytes] + }, + LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => { + let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX") + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_AND_SLH_DSA"); + std::process::exit(1); + }); + // Parse JSON array and extract the hex strings + let priv_keys_hex_vec: Vec = serde_json::from_str(&priv_keys_hex_array) + .unwrap_or_else(|_| { + error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array"); + std::process::exit(1); + }); + + if priv_keys_hex_vec.len() != 2 { + error!("For SCHNORR_AND_SLH_DSA, LEAF_SCRIPT_PRIV_KEYS_HEX must contain exactly 2 hex strings, got {}", priv_keys_hex_vec.len()); + std::process::exit(1); + } + + let schnorr_priv_key_hex = &priv_keys_hex_vec[0]; + let slh_dsa_priv_key_hex = &priv_keys_hex_vec[1]; + + let schnorr_priv_key_bytes = hex::decode(schnorr_priv_key_hex).unwrap(); + let slh_dsa_priv_key_bytes = hex::decode(slh_dsa_priv_key_hex).unwrap(); + + if schnorr_priv_key_bytes.len() != 32 { + error!("Schnorr private key must be 32 bytes, got {}", schnorr_priv_key_bytes.len()); + std::process::exit(1); + } + if slh_dsa_priv_key_bytes.len() != 64 { + error!("SLH-DSA private key must be 64 bytes, got {}", slh_dsa_priv_key_bytes.len()); + std::process::exit(1); + } + + vec![schnorr_priv_key_bytes, slh_dsa_priv_key_bytes] + }, + LeafScriptType::Mixed => { + // This case should never be reached because Mixed is resolved to effective_leaf_type above + unreachable!("Mixed should have been resolved to effective_leaf_type"); + }, + LeafScriptType::NotApplicable => { + panic!("LeafScriptType::NotApplicable is not applicable"); + } + }; + + + // ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG + let leaf_script_bytes: Vec = env::var("LEAF_SCRIPT_HEX") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_HEX environment variable is required but not set"); + std::process::exit(1); + }); + + // https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs + let spend_output_pubkey_hash_bytes: Vec = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap(); + + // OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats + let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(funding_utxo_amount_sats.saturating_sub(5000)); + + + let result: SpendDetails = pay_to_p2wpkh_tx( + funding_tx_id_bytes, + funding_utxo_index, + funding_utxo_amount_sats, + funding_script_pubkey_bytes, + control_block_bytes, + leaf_script_bytes.clone(), + leaf_script_priv_keys_bytes, // Now passing Vec> instead of Vec + spend_output_pubkey_hash_bytes, + spend_output_amount_sats, + effective_leaf_type // Use effective type (resolved from SPENDING_LEAF_TYPE if Mixed) + ); + + // Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes + let tapleaf_pubkey_bytes: Vec = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec(); + + match effective_leaf_type { + LeafScriptType::SlhDsaOnly => { + let is_valid: bool = verify_slh_dsa_via_bytes(&result.sig_bytes, &result.sighash, &tapleaf_pubkey_bytes); + info!("is_valid: {}", is_valid); + }, + LeafScriptType::SchnorrOnly => { + let is_valid: bool = verify_schnorr_signature_via_bytes( + &result.sig_bytes, + &result.sighash, + &tapleaf_pubkey_bytes); + info!("is_valid: {}", is_valid); + }, + LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => { + // For combined scripts, we need to separate the signatures + // The sig_bytes contains: [schnorr_sig (64 bytes), slh_dsa_sig (7856 bytes)] (raw signatures without sighash) + let schnorr_sig_len = 64; // Schnorr signature is 64 bytes + let slh_dsa_sig_len = 7856; // SLH-DSA signature is 7856 bytes + + let expected_min_len = schnorr_sig_len + slh_dsa_sig_len; + + if result.sig_bytes.len() < expected_min_len { + error!("Combined signature length is too short: expected at least {}, got {}", + expected_min_len, result.sig_bytes.len()); + return result; + } + + // Extract Schnorr signature (first 64 bytes) + let schnorr_sig = &result.sig_bytes[..schnorr_sig_len]; + // Extract SLH-DSA signature (next 7856 bytes) + let slh_dsa_sig = &result.sig_bytes[schnorr_sig_len..schnorr_sig_len + slh_dsa_sig_len]; + + // For SCHNORR_AND_SLH_DSA scripts, we need to extract the individual public keys + // The script structure is: OP_PUSHBYTES_32 OP_CHECKSIG OP_PUSHBYTES_32 OP_SUBSTR OP_BOOLAND OP_VERIFY + // So we need to extract the Schnorr pubkey (first 32 bytes after OP_PUSHBYTES_32) + let schnorr_pubkey_bytes = &leaf_script_bytes[1..33]; // Skip OP_PUSHBYTES_32 (0x20), get next 32 bytes + let slh_dsa_pubkey_bytes = &leaf_script_bytes[35..67]; // Skip OP_CHECKSIG (0xac), OP_PUSHBYTES_32 (0x20), get next 32 bytes + + // Verify Schnorr signature + let schnorr_is_valid: bool = verify_schnorr_signature_via_bytes( + schnorr_sig, + &result.sighash, + schnorr_pubkey_bytes); + info!("Schnorr signature is_valid: {}", schnorr_is_valid); + + // Verify SLH-DSA signature + let slh_dsa_is_valid: bool = verify_slh_dsa_via_bytes( + slh_dsa_sig, + &result.sighash, + slh_dsa_pubkey_bytes); + info!("SLH-DSA signature is_valid: {}", slh_dsa_is_valid); + + let both_valid = schnorr_is_valid && slh_dsa_is_valid; + info!("Both signatures valid: {}", both_valid); + } + LeafScriptType::Mixed => { + // This case should never be reached because Mixed is resolved to effective_leaf_type above + unreachable!("Mixed should have been resolved to effective_leaf_type"); + } + LeafScriptType::NotApplicable => { + panic!("LeafScriptType::NotApplicable is not applicable"); + } + } + + return result; +} diff --git a/bip-0360/ref-impl/rust/examples/p2tr_construction.rs b/bip-0360/ref-impl/rust/examples/p2tr_construction.rs new file mode 100644 index 0000000000..328c16fd71 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/p2tr_construction.rs @@ -0,0 +1,17 @@ +use p2mr_ref::{create_p2tr_utxo, create_p2tr_multi_leaf_taptree}; +use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn}; + +// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature +fn main() -> ConstructionReturn { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let internal_pubkey_hex = "924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329".to_string(); + + let taptree_return: TaptreeReturn = create_p2tr_multi_leaf_taptree(internal_pubkey_hex.clone()); + let utxo_return: UtxoReturn = create_p2tr_utxo(taptree_return.clone().tree_root_hex, internal_pubkey_hex); + return ConstructionReturn { + taptree_return: taptree_return, + utxo_return: utxo_return, + }; +} diff --git a/bip-0360/ref-impl/rust/examples/p2tr_spend.rs b/bip-0360/ref-impl/rust/examples/p2tr_spend.rs new file mode 100644 index 0000000000..636468c168 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/p2tr_spend.rs @@ -0,0 +1,129 @@ +use p2mr_ref::{ pay_to_p2wpkh_tx , verify_schnorr_signature_via_bytes}; + +use p2mr_ref::data_structures::{SpendDetails, LeafScriptType}; +use std::env; +use log::{info, error}; + +// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature +fn main() -> SpendDetails { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + // FUNDING_TX_ID environment variable is required + let funding_tx_id: String = env::var("FUNDING_TX_ID") + .unwrap_or_else(|_| { + error!("FUNDING_TX_ID environment variable is required but not set"); + std::process::exit(1); + }); + let funding_tx_id_bytes: Vec = hex::decode(funding_tx_id.clone()).unwrap(); + + // FUNDING_UTXO_AMOUNT_SATS environment variable is required + let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS") + .unwrap_or_else(|_| { + error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set"); + std::process::exit(1); + }) + .parse::() + .unwrap_or_else(|_| { + error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer"); + std::process::exit(1); + }); + + // The input index of the funding tx + // Allow override via FUNDING_UTXO_INDEX environment variable + let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index); + + // FUNDING_SCRIPT_PUBKEY environment variable is required + let funding_script_pubkey_bytes: Vec = env::var("FUNDING_SCRIPT_PUBKEY") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set"); + std::process::exit(1); + }); + + let control_block_bytes: Vec = env::var("CONTROL_BLOCK_HEX") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("CONTROL_BLOCK_HEX environment variable is required but not set"); + std::process::exit(1); + }); + info!("P2TR control block size: {}", control_block_bytes.len()); + + // P2TR only supports Schnorr signatures, so we only need one private key + let leaf_script_priv_key_bytes: Vec = { + let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX") + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required but not set"); + std::process::exit(1); + }); + // Parse JSON array and extract the first (and only) hex string + let priv_keys_hex: String = serde_json::from_str::>(&priv_keys_hex_array) + .unwrap_or_else(|_| { + error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array"); + std::process::exit(1); + }) + .into_iter() + .next() + .unwrap_or_else(|| { + error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty"); + std::process::exit(1); + }); + hex::decode(priv_keys_hex).unwrap() + }; + + // Validate that the private key is 32 bytes (Schnorr key size) + if leaf_script_priv_key_bytes.len() != 32 { + error!("P2TR private key must be 32 bytes (Schnorr), got {}", leaf_script_priv_key_bytes.len()); + std::process::exit(1); + } + + // Convert to Vec> format expected by the function + let leaf_script_priv_keys_bytes: Vec> = vec![leaf_script_priv_key_bytes]; + + // ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG + let leaf_script_bytes: Vec = env::var("LEAF_SCRIPT_HEX") + .map(|s| hex::decode(s).unwrap()) + .unwrap_or_else(|_| { + error!("LEAF_SCRIPT_HEX environment variable is required but not set"); + std::process::exit(1); + }); + + // https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs + let spend_output_pubkey_hash_bytes: Vec = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap(); + + // OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats + let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(funding_utxo_amount_sats.saturating_sub(5000)); + + + let result: SpendDetails = pay_to_p2wpkh_tx( + funding_tx_id_bytes, + funding_utxo_index, + funding_utxo_amount_sats, + funding_script_pubkey_bytes, + control_block_bytes, + leaf_script_bytes.clone(), + leaf_script_priv_keys_bytes, // Now passing Vec> format + spend_output_pubkey_hash_bytes.clone(), + spend_output_amount_sats, + LeafScriptType::SchnorrOnly + ); + + // Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes + let tapleaf_pubkey_bytes: Vec = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec(); + + let is_valid: bool = verify_schnorr_signature_via_bytes( + &result.sig_bytes, + &result.sighash, + &tapleaf_pubkey_bytes); + info!("is_valid: {}", is_valid); + + return result; +} diff --git a/bip-0360/ref-impl/rust/examples/schnorr_example.rs b/bip-0360/ref-impl/rust/examples/schnorr_example.rs new file mode 100644 index 0000000000..4723d99bd5 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/schnorr_example.rs @@ -0,0 +1,69 @@ +use std::env; +use log::info; +use once_cell::sync::Lazy; +use bitcoin::key::{Secp256k1}; +use bitcoin::hashes::{sha256::Hash, Hash as HashTrait}; +use bitcoin::secp256k1::{Message}; + +use p2mr_ref::{ acquire_schnorr_keypair, verify_schnorr_signature }; + +/* Secp256k1 implements the Signing trait when it's initialized in signing mode. + It's important to note that Secp256k1 has different capabilities depending on how it's constructed: + * Secp256k1::new() creates a context capable of both signing and verification + * Secp256k1::signing_only() creates a context that can only sign + * Secp256k1::verification_only() creates a context that can only verify +*/ +static SECP: Lazy> = Lazy::new(Secp256k1::new); + +fn main() { + let _ = env_logger::try_init(); + + // acquire a schnorr keypair (leveraging OS provided random number generator) + let keypair = acquire_schnorr_keypair(); + let (secret_key, public_key) = keypair.as_schnorr().unwrap(); + let message_bytes = b"hello"; + + // secp256k1 operates on a 256-bit (32-byte) field, so inputs must be exactly this size + // subsequently, Schnorr signatures on secp256k1 require exactly a 32-byte input (the curve's scalar field size) + let message_hash: Hash = Hash::hash(message_bytes); + + let message: Message = Message::from_digest_slice(&message_hash.to_byte_array()).unwrap(); + + + /* The secp256k1 library internally generates a random scalar value (aka: nonce or k-value) for each signature + * Every signature is unique - even if you sign the same message with the same private key multiple times + * The randomness is handled automatically by the secp256k1 implementation + * You get different signatures each time for the same inputs + * The nonce is only needed during signing, not during verification + + Schnorr signatures require randomness for security reasons: + * Prevents private key recovery - If the same nonce is used twice, an attacker could potentially derive your private key + * Ensures signature uniqueness - Each signature should be cryptographically distinct + * Protects against replay attacks - Different signatures for the same data + */ + let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr(&message, &secret_key.keypair(&SECP)); + info!("Signature created successfully, size: {}", signature.serialize().len()); + + //let pubkey = public_key; + + + /* + * The nonce provides security during signing (prevents private key recovery) + * The nonce is mathematically eliminated during verification + * The verifier only needs public information (signature, message, public key) + */ + let schnorr_valid = verify_schnorr_signature(signature, message, *public_key); + info!("schnorr_valid: {}", schnorr_valid); + + + let aux_rand = [0u8; 32]; // 32 zero bytes; fine for testing + let signature_aux_rand: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand( + &message, + &secret_key.keypair(&SECP), + &aux_rand + ); + info!("aux_rand signature created successfully, size: {}", signature_aux_rand.serialize().len()); + + let schnorr_valid_aux_rand = verify_schnorr_signature(signature_aux_rand, message, *public_key); + info!("schnorr_valid_aux_rand: {}", schnorr_valid_aux_rand); +} diff --git a/bip-0360/ref-impl/rust/examples/slh_dsa_verification_example.rs b/bip-0360/ref-impl/rust/examples/slh_dsa_verification_example.rs new file mode 100644 index 0000000000..c6c965e7a0 --- /dev/null +++ b/bip-0360/ref-impl/rust/examples/slh_dsa_verification_example.rs @@ -0,0 +1,77 @@ +use std::env; +use log::info; +use once_cell::sync::Lazy; +use bitcoin::hashes::{sha256::Hash, Hash as HashTrait}; +use rand::{rng, RngCore}; + +use bitcoinpqc::{ + generate_keypair, public_key_size, secret_key_size, sign, signature_size, verify, Algorithm, KeyPair, +}; + +fn main() { + let _ = env_logger::try_init(); + + /* + In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to: + * Initialize hash function parameters within the key generation + * Seed the Merkle tree construction that forms the public key + * Generate the secret key components that enable signing + */ + let random_data = get_random_bytes(128); + println!("Generated random data of size {}", random_data.len()); + + let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data) + .expect("Failed to generate SLH-DSA-128S keypair"); + + let message_bytes = b"SLH-DSA-128S Test Message"; + + println!("Message to sign: {message_bytes:?}"); + + /* No need to hash the message + 1. Variable Input Size: SPHINCS+ can handle messages of arbitrary length directly + 2. Internal Hashing: The SPHINCS+ algorithm internally handles message processing and hashing as part of its design + 3. Hash-Based Design: SPHINCS+ is built on hash functions and Merkle trees, so it's designed to work with variable-length inputs + 4. No Curve Constraints: Unlike elliptic curve schemes, SPHINCS+ doesn't have fixed field size requirements + + SLH-DSA doesn't use nonces like Schnorr does. + With SLH-DSA, randomness is built into the key generation process only ( and not the signing process; ie: SECP256K1) + Thus, no need for aux_rand data fed to the signature function. + The signing algorithm is deterministic and doesn't require random input during signing. + */ + + let signature = sign(&keypair.secret_key, message_bytes).expect("Failed to sign with SLH-DSA-128S"); + + println!( + "Signature created successfully, size: {}", + signature.bytes.len() + ); + println!( + "Signature prefix: {:02x?}", + &signature.bytes[..8.min(signature.bytes.len())] + ); + + // Verify the signature + println!("Verifying signature..."); + let result = verify(&keypair.public_key, message_bytes, &signature); + println!("Verification result: {result:?}"); + + assert!(result.is_ok(), "SLH-DSA-128S signature verification failed"); + + // Try to verify with a modified message - should fail + let modified_message = b"SLH-DSA-128S Modified Message"; + println!("Modified message: {modified_message:?}"); + + let result = verify(&keypair.public_key, modified_message, &signature); + println!("Verification with modified message result: {result:?}"); + + assert!( + result.is_err(), + "SLH-DSA-128S verification should fail with modified message" + ); +} + +fn get_random_bytes(size: usize) -> Vec { + let mut bytes = vec![0u8; size]; + rng().fill_bytes(&mut bytes); + bytes +} diff --git a/bip-0360/ref-impl/rust/src/bin/slh_dsa_key_gen.rs b/bip-0360/ref-impl/rust/src/bin/slh_dsa_key_gen.rs new file mode 100644 index 0000000000..999d487d3d --- /dev/null +++ b/bip-0360/ref-impl/rust/src/bin/slh_dsa_key_gen.rs @@ -0,0 +1,33 @@ +use std::env; +use log::info; +use rand::{rng, RngCore}; + +use bitcoinpqc::{ + generate_keypair, public_key_size, secret_key_size, Algorithm, KeyPair, +}; + +fn main() { + let _ = env_logger::try_init(); + + /* + In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to: + * Initialize hash function parameters within the key generation + * Seed the Merkle tree construction that forms the public key + * Generate the secret key components that enable signing + */ + let random_data = get_random_bytes(128); + println!("Generated random data of size {}", random_data.len()); + + let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data) + .expect("Failed to generate SLH-DSA-128S keypair"); + + info!("public key size / value = {}, {}", public_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.public_key.bytes)); + info!("private key size / value = {}, {}", secret_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.secret_key.bytes)); + +} + +fn get_random_bytes(size: usize) -> Vec { + let mut bytes = vec![0u8; size]; + rng().fill_bytes(&mut bytes); + bytes +} diff --git a/bip-0360/ref-impl/rust/src/data_structures.rs b/bip-0360/ref-impl/rust/src/data_structures.rs new file mode 100644 index 0000000000..e21b402e4d --- /dev/null +++ b/bip-0360/ref-impl/rust/src/data_structures.rs @@ -0,0 +1,596 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use log::debug; + +// Add imports for the unified keypair +use bitcoin::secp256k1::{SecretKey, XOnlyPublicKey}; +use bitcoinpqc::{KeyPair, Algorithm}; + +/// Enum representing the type of leaf script to create +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LeafScriptType { + /// Script requires only SLH-DSA signature + SlhDsaOnly, + /// Script requires only Schnorr signature + SchnorrOnly, + /// Script requires both Schnorr and SLH-DSA signatures (in that order) + ConcatenatedSchnorrAndSlhDsaSameLeaf, + /// Leaves of TapTree are mixed. Some leaves are locked using Schnorr and others are locked using SLH-DSA + Mixed, + /// Script type is not applicable + NotApplicable, +} + +impl LeafScriptType { + /// Check if this script type uses SLH-DSA + pub fn uses_slh_dsa(&self) -> bool { + matches!(self, LeafScriptType::SlhDsaOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf) + } + + /// Check if this script type uses Schnorr + pub fn uses_schnorr(&self) -> bool { + matches!(self, LeafScriptType::SchnorrOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf) + } + + /// Check if this script type requires both signature types + pub fn requires_both(&self) -> bool { + matches!(self, LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf) + } + + /// Check if TapTree uses Schnorr for some leaves and SLH-DSA for others + pub fn uses_mixed(&self) -> bool { + matches!(self, LeafScriptType::Mixed) + } + + /// Check if this script type is not applicable + pub fn is_not_applicable(&self) -> bool { + matches!(self, LeafScriptType::NotApplicable) + } + + /// Convert to string representation for serialization + pub fn to_string(&self) -> String { + match self { + LeafScriptType::SlhDsaOnly => "SLH_DSA_ONLY".to_string(), + LeafScriptType::SchnorrOnly => "SCHNORR_ONLY".to_string(), + LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => "CONCATENATED_SCHNORR_AND_SLH_DSA".to_string(), + LeafScriptType::Mixed => "MIXED".to_string(), + LeafScriptType::NotApplicable => "NOT_APPLICABLE".to_string(), + } + } + + /// Parse from string representation + pub fn from_string(s: &str) -> Self { + match s { + "SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly, + "SCHNORR_ONLY" => LeafScriptType::SchnorrOnly, + "CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf, + "MIXED" => LeafScriptType::Mixed, + _ => LeafScriptType::NotApplicable, + } + } +} + +#[derive(Debug, Serialize)] +pub struct TestVectors { + pub version: u32, + #[serde(rename = "test_vectors")] + pub test_vectors: Vec, + #[serde(skip, default = "HashMap::new")] + pub test_vector_map: HashMap, +} + +impl<'de> Deserialize<'de> for TestVectors { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + version: u32, + #[serde(rename = "test_vectors")] + test_vectors: Vec, + } + + let helper = Helper::deserialize(deserializer)?; + + let mut test_vector_map = HashMap::new(); + for test in helper.test_vectors.iter() { + test_vector_map.insert(test.id.clone(), test.clone()); + } + + Ok(TestVectors { + version: helper.version, + test_vectors: helper.test_vectors, + test_vector_map, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TestVector { + pub id: String, + pub objective: String, + pub given: TestVectorGiven, + #[serde(default)] + pub intermediary: TestVectorIntermediary, + pub expected: TestVectorExpected, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TestVectorGiven { + + #[serde(rename = "internalPubkey")] + pub internal_pubkey: Option, + + #[serde(rename = "scriptTree")] + pub script_tree: Option, + + #[serde(rename = "scriptInputs")] + pub script_inputs: Option>, + #[serde(rename = "scriptHex")] + pub script_hex: Option, + #[serde(rename = "controlBlock")] + pub control_block: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct TestVectorIntermediary { + + #[serde(default)] + #[serde(rename = "leafHashes")] + pub leaf_hashes: Vec, + #[serde(rename = "merkleRoot")] + pub merkle_root: Option +} + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TestVectorExpected { + #[serde(rename = "scriptPubKey")] + pub script_pubkey: Option, + #[serde(rename = "bip350Address")] + pub bip350_address: Option, + #[serde(default)] + #[serde(rename = "scriptPathControlBlocks")] + pub script_path_control_blocks: Option>, + #[serde(rename = "error")] + pub error: Option, + #[serde(rename = "address")] + pub address: Option, + #[serde(default)] + pub witness: Option +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TVScriptLeaf { + pub id: u8, + pub script: String, + #[serde(rename = "leafVersion")] + pub leaf_version: u8, +} + +// Taproot script trees are binary trees, so each branch should have exactly two children. +#[derive(Debug, Serialize, Clone)] +pub enum TVScriptTree { + Leaf(TVScriptLeaf), + Branch { + + // Box is used because Rust needs to know the exact size of types at compile time. + // Without it, we'd have an infinitely size recursive type. + // The enum itself is on the stack, but the Box fields within the Branch variant store pointers to heap-allocated ScriptTree values. + left: Box, + right: Box, + }, +} + +// Add custom deserialize implementation +impl<'de> Deserialize<'de> for TVScriptTree { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Helper { + Leaf(TVScriptLeaf), + Branch(Vec), + } + + match Helper::deserialize(deserializer)? { + Helper::Leaf(leaf) => Ok(TVScriptTree::Leaf(leaf)), + Helper::Branch(v) => { + assert!(v.len() == 2, "Branch must have exactly two children"); + let mut iter = v.into_iter(); + Ok(TVScriptTree::Branch { + left: Box::new(iter.next().unwrap()), + right: Box::new(iter.next().unwrap()), + }) + } + } + } +} + +// Add this enum before the TVScriptTree implementation +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Direction { + Left, + Right, + Root, +} + +impl TVScriptTree { + /// Implements a "post-order" traversal as follows: left, right, branch + pub fn traverse_with_depth(&self, depth: u8, direction: Direction, f: &mut F) { + match self { + TVScriptTree::Branch { left, right } => { + + right.traverse_with_depth(depth, Direction::Right, f); // Pass Right for right subtree + left.traverse_with_depth(depth, Direction::Left, f); // Pass Left for left subtree + f(self, depth, direction); // Pass the current node's direction + } + TVScriptTree::Leaf { .. } => { + f(self, depth, direction); + } + } + } + + /// Traverses the tree visiting right subtree leaves first, then left subtree leaves. + /// Depth increases by 1 at each branch level. + /* + root (depth 0) + / \ + L0 (depth 1) (subtree) (depth 1) + / \ + L1 (depth 2) L2 (depth 2) + + The new traversal will visit: + L1 at depth 2 -> L2 at depth 2 -> L0 at depth 1 + */ + pub fn traverse_with_right_subtree_first(&self, depth: u8, direction: Direction, f: &mut F) { + match self { + TVScriptTree::Branch { left, right } => { + let next_depth = depth + 1; + // Visit right subtree first + right.traverse_with_right_subtree_first(next_depth, Direction::Right, f); + // Then visit left subtree + left.traverse_with_right_subtree_first(next_depth, Direction::Left, f); + } + TVScriptTree::Leaf { .. } => { + f(self, depth, direction); + } + } + } +} + +impl std::fmt::Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Direction::Left => write!(f, "L"), + Direction::Right => write!(f, "R"), + Direction::Root => write!(f, "Root"), + } + } +} + +pub struct ScriptTreeHashCache { + pub leaf_hashes: HashMap, + pub branch_hashes: HashMap, +} + +impl ScriptTreeHashCache { + pub fn new() -> Self { + Self { + leaf_hashes: HashMap::new(), + branch_hashes: HashMap::new(), + } + } + + pub fn set_leaf_hash(&mut self, branch_id: u8, direction: Direction, hash: String) { + let key = format!("{branch_id}_{direction}"); + debug!("set_leaf_hash: key: {}, hash: {}", key, hash); + self.leaf_hashes.insert(key, hash); + } + + pub fn set_branch_hash(&mut self, branch_id: u8, hash: String) { + self.branch_hashes.insert(branch_id, hash); + } +} + +fn serialize_hex(bytes: &Vec, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&hex::encode(bytes)) +} + +fn deserialize_hex<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(d)?; + hex::decode(s).map_err(serde::de::Error::custom) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpendDetails { + pub tx_hex: String, + #[serde(serialize_with = "serialize_hex")] + #[serde(deserialize_with = "deserialize_hex")] + pub sighash: Vec, + #[serde(serialize_with = "serialize_hex")] + #[serde(deserialize_with = "deserialize_hex")] + pub sig_bytes: Vec, + #[serde(serialize_with = "serialize_hex")] + #[serde(deserialize_with = "deserialize_hex")] + pub derived_witness_vec: Vec, +} + +impl std::process::Termination for SpendDetails { + fn report(self) -> std::process::ExitCode { + if let Ok(json) = serde_json::to_string_pretty(&self) { + println!("{}", json); + } else { + println!("{:?}", self); + } + std::process::ExitCode::SUCCESS + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UtxoReturn { + + pub script_pubkey_hex: String, + pub bech32m_address: String, + pub bitcoin_network: bitcoin::Network, +} + +impl std::process::Termination for UtxoReturn { + fn report(self) -> std::process::ExitCode { + if let Ok(json) = serde_json::to_string_pretty(&self) { + println!("{}", json); + } else { + println!("{:?}", self); + } + std::process::ExitCode::SUCCESS + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaptreeReturn { + pub leaf_script_priv_keys_hex: Vec, // Changed to support multiple private keys + pub leaf_script_hex: String, + pub tree_root_hex: String, + pub control_block_hex: String, + /// The script type of the leaf being returned (needed for spending) + pub leaf_script_type: String, +} + +impl std::process::Termination for TaptreeReturn { + fn report(self) -> std::process::ExitCode { + if let Ok(json) = serde_json::to_string_pretty(&self) { + println!("{}", json); + } else { + println!("{:?}", self); + } + std::process::ExitCode::SUCCESS + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConstructionReturn { + pub taptree_return: TaptreeReturn, + pub utxo_return: UtxoReturn, +} + +impl std::process::Termination for ConstructionReturn { + fn report(self) -> std::process::ExitCode { + if let Ok(json) = serde_json::to_string_pretty(&self) { + println!("{}", json); + } else { + println!("{:?}", self); + } + std::process::ExitCode::SUCCESS + } +} + +/// A unified keypair that can contain either a Schnorr keypair or an SLH-DSA keypair +#[derive(Debug, Clone)] +pub enum UnifiedKeypair { + Schnorr(SecretKey, XOnlyPublicKey), + SlhDsa(KeyPair), +} + +/// A container for multiple keypairs that can be used in a single leaf script +#[derive(Debug, Clone)] +pub struct MultiKeypair { + pub schnorr_keypair: Option, + pub slh_dsa_keypair: Option, +} + +impl MultiKeypair { + /// Create a new MultiKeypair with only a Schnorr keypair + pub fn new_schnorr_only(schnorr_keypair: UnifiedKeypair) -> Self { + Self { + schnorr_keypair: Some(schnorr_keypair), + slh_dsa_keypair: None, + } + } + + /// Create a new MultiKeypair with only an SLH-DSA keypair + pub fn new_slh_dsa_only(slh_dsa_keypair: UnifiedKeypair) -> Self { + Self { + schnorr_keypair: None, + slh_dsa_keypair: Some(slh_dsa_keypair), + } + } + + /// Create a new MultiKeypair with both keypairs + pub fn new_combined(schnorr_keypair: UnifiedKeypair, slh_dsa_keypair: UnifiedKeypair) -> Self { + Self { + schnorr_keypair: Some(schnorr_keypair), + slh_dsa_keypair: Some(slh_dsa_keypair), + } + } + + /// Get all secret key bytes for serialization (in order: schnorr, then slh_dsa if present) + pub fn secret_key_bytes(&self) -> Vec> { + let mut result = Vec::new(); + if let Some(ref schnorr) = self.schnorr_keypair { + result.push(schnorr.secret_key_bytes()); + } + if let Some(ref slh_dsa) = self.slh_dsa_keypair { + result.push(slh_dsa.secret_key_bytes()); + } + result + } + + /// Get all public key bytes for script construction (in order: schnorr, then slh_dsa if present) + pub fn public_key_bytes(&self) -> Vec> { + let mut result = Vec::new(); + if let Some(ref schnorr) = self.schnorr_keypair { + result.push(schnorr.public_key_bytes()); + } + if let Some(ref slh_dsa) = self.slh_dsa_keypair { + result.push(slh_dsa.public_key_bytes()); + } + result + } + + /// Check if this contains a Schnorr keypair + pub fn has_schnorr(&self) -> bool { + self.schnorr_keypair.is_some() + } + + /// Check if this contains an SLH-DSA keypair + pub fn has_slh_dsa(&self) -> bool { + self.slh_dsa_keypair.is_some() + } + + /// Get the Schnorr keypair if present + pub fn schnorr_keypair(&self) -> Option<&UnifiedKeypair> { + self.schnorr_keypair.as_ref() + } + + /// Get the SLH-DSA keypair if present + pub fn slh_dsa_keypair(&self) -> Option<&UnifiedKeypair> { + self.slh_dsa_keypair.as_ref() + } +} + +/// Information about a single leaf in a mixed-type tree +/// Used when different leaves in the same tree use different algorithms +#[derive(Debug, Clone)] +pub struct MixedLeafInfo { + /// The leaf index in the tree + pub leaf_index: u32, + /// The script type for this specific leaf + pub leaf_script_type: LeafScriptType, + /// The keypairs for this leaf + pub keypairs: MultiKeypair, + /// The script for this leaf + pub script: Vec, +} + +impl MixedLeafInfo { + /// Create a new MixedLeafInfo for a Schnorr-only leaf + pub fn new_schnorr(leaf_index: u32, keypairs: MultiKeypair, script: Vec) -> Self { + Self { + leaf_index, + leaf_script_type: LeafScriptType::SchnorrOnly, + keypairs, + script, + } + } + + /// Create a new MixedLeafInfo for an SLH-DSA-only leaf + pub fn new_slh_dsa(leaf_index: u32, keypairs: MultiKeypair, script: Vec) -> Self { + Self { + leaf_index, + leaf_script_type: LeafScriptType::SlhDsaOnly, + keypairs, + script, + } + } + + /// Create a new MixedLeafInfo for a combined Schnorr+SLH-DSA leaf + pub fn new_combined(leaf_index: u32, keypairs: MultiKeypair, script: Vec) -> Self { + Self { + leaf_index, + leaf_script_type: LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf, + keypairs, + script, + } + } + + /// Get the secret key bytes for this leaf + pub fn secret_key_bytes(&self) -> Vec> { + self.keypairs.secret_key_bytes() + } + + /// Get the public key bytes for this leaf + pub fn public_key_bytes(&self) -> Vec> { + self.keypairs.public_key_bytes() + } +} + +impl UnifiedKeypair { + /// Create a new Schnorr keypair + pub fn new_schnorr(secret_key: SecretKey, public_key: XOnlyPublicKey) -> Self { + UnifiedKeypair::Schnorr(secret_key, public_key) + } + + /// Create a new SLH-DSA keypair + pub fn new_slh_dsa(keypair: KeyPair) -> Self { + UnifiedKeypair::SlhDsa(keypair) + } + + /// Get the secret key bytes for serialization + pub fn secret_key_bytes(&self) -> Vec { + match self { + UnifiedKeypair::Schnorr(secret_key, _) => secret_key.secret_bytes().to_vec(), + UnifiedKeypair::SlhDsa(keypair) => keypair.secret_key.bytes.clone(), + } + } + + /// Get the public key bytes for script construction + pub fn public_key_bytes(&self) -> Vec { + match self { + UnifiedKeypair::Schnorr(_, public_key) => public_key.serialize().to_vec(), + UnifiedKeypair::SlhDsa(keypair) => keypair.public_key.bytes.clone(), + } + } + + /// Get the algorithm type + pub fn algorithm(&self) -> &'static str { + match self { + UnifiedKeypair::Schnorr(_, _) => "Schnorr", + UnifiedKeypair::SlhDsa(_) => "SLH-DSA", + } + } + + /// Check if this is a Schnorr keypair + pub fn is_schnorr(&self) -> bool { + matches!(self, UnifiedKeypair::Schnorr(_, _)) + } + + /// Check if this is an SLH-DSA keypair + pub fn is_slh_dsa(&self) -> bool { + matches!(self, UnifiedKeypair::SlhDsa(_)) + } + + /// Get the underlying Schnorr keypair if this is a Schnorr keypair + pub fn as_schnorr(&self) -> Option<(&SecretKey, &XOnlyPublicKey)> { + match self { + UnifiedKeypair::Schnorr(secret_key, public_key) => Some((secret_key, public_key)), + _ => None, + } + } + + /// Get the underlying SLH-DSA keypair if this is an SLH-DSA keypair + pub fn as_slh_dsa(&self) -> Option<&KeyPair> { + match self { + UnifiedKeypair::SlhDsa(keypair) => Some(keypair), + _ => None, + } + } +} diff --git a/bip-0360/ref-impl/rust/src/error.rs b/bip-0360/ref-impl/rust/src/error.rs new file mode 100644 index 0000000000..ceebf1a9c5 --- /dev/null +++ b/bip-0360/ref-impl/rust/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum P2MRError { + + #[error("P2TR requires witness version of 1")] + P2trRequiresWitnessVersion1, + + + #[error("P2MR requires a script tree with at least one leaf")] + MissingScriptTreeLeaf, + + // We can add more specific error variants here as needed + #[error("Invalid script tree structure: {0}")] + InvalidScriptTree(String), + + #[error("BIP-360 requires leaf version 0xc0; leaf {0} has version {1}")] + InvalidLeafVersion(u8, u8), +} \ No newline at end of file diff --git a/bip-0360/ref-impl/rust/src/lib.rs b/bip-0360/ref-impl/rust/src/lib.rs new file mode 100644 index 0000000000..e9c9816d14 --- /dev/null +++ b/bip-0360/ref-impl/rust/src/lib.rs @@ -0,0 +1,721 @@ +pub mod data_structures; +pub mod error; + +use log::{debug, info, error}; +use std::env; +use std::io::Write; +use rand::{rng, RngCore}; +use once_cell::sync::Lazy; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::key::{Secp256k1, Parity}; +use bitcoin::secp256k1::{Message, SecretKey, Keypair, rand::rngs::OsRng, rand::thread_rng, rand::Rng, schnorr::Signature}; +use bitcoin::{ Amount, TxOut, WPubkeyHash, + Address, Network, OutPoint, + blockdata::witness::Witness, + Script, ScriptBuf, XOnlyPublicKey, PublicKey, + sighash::{SighashCache, TapSighashType, Prevouts, TapSighash}, + taproot::{LeafVersion, NodeInfo, TapLeafHash, TapNodeHash, TapTree, ScriptLeaves, TaprootMerkleBranch, TaprootBuilder, TaprootSpendInfo, ControlBlock}, + transaction::{Transaction, Sequence} +}; + +use bitcoin::p2mr::{P2mrScriptBuf, P2mrBuilder, P2mrSpendInfo, P2mrControlBlock, P2MR_LEAF_VERSION}; + +use bitcoinpqc::{ + generate_keypair, public_key_size, secret_key_size, Algorithm, KeyPair, sign, verify, +}; + +use data_structures::{SpendDetails, UtxoReturn, TaptreeReturn, UnifiedKeypair, MultiKeypair, LeafScriptType, MixedLeafInfo}; + +/* Secp256k1 implements the Signing trait when it's initialized in signing mode. + It's important to note that Secp256k1 has different capabilities depending on how it's constructed: + * Secp256k1::new() creates a context capable of both signing and verification + * Secp256k1::signing_only() creates a context that can only sign + * Secp256k1::verification_only() creates a context that can only verify +*/ +static SECP: Lazy> = Lazy::new(Secp256k1::new); + +/// Creates a Huffman tree with leaves of the specified script type. +/// +/// For Mixed type, leaves alternate between Schnorr (even indices) and SLH-DSA (odd indices). +/// The LEAF_TO_SPEND_FROM_TYPE env var can override the type for the leaf of interest. +/// +/// Returns: (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) +fn create_huffman_tree(leaf_script_type: LeafScriptType) -> (Vec<(u32, ScriptBuf)>, MultiKeypair, ScriptBuf, LeafScriptType) { + + let mut total_leaf_count: u32 = 1; + if let Ok(env_value) = env::var("TOTAL_LEAF_COUNT") { + if let Ok(parsed_value) = env_value.parse::() { + total_leaf_count = parsed_value; + } + } + + let mut leaf_to_spend_from: u32 = 0; + if let Ok(env_value) = env::var("LEAF_TO_SPEND_FROM") { + if let Ok(parsed_value) = env_value.parse::() { + leaf_to_spend_from = parsed_value; + } + } + + // For Mixed mode, allow overriding the type of the leaf of interest + let leaf_to_spend_from_type: Option = if leaf_script_type == LeafScriptType::Mixed { + env::var("LEAF_TO_SPEND_FROM_TYPE").ok().map(|s| LeafScriptType::from_string(&s)) + } else { + None + }; + + if total_leaf_count < 1 { + panic!("total_leaf_count must be greater than 0"); + } + if leaf_to_spend_from >= total_leaf_count { + panic!("leaf_to_spend_from must be less than total_leaf_count and greater than 0"); + } + + debug!("Creating multi-leaf taptree with total_leaf_count: {}, leaf_to_spend_from: {}", total_leaf_count, leaf_to_spend_from); + let mut huffman_entries: Vec<(u32, ScriptBuf)> = vec![]; + let mut keypairs_of_interest: Option = None; + let mut script_buf_of_interest: Option = None; + let mut actual_leaf_type_of_interest: LeafScriptType = leaf_script_type; + + for leaf_index in 0..total_leaf_count { + let keypairs: MultiKeypair; + let script_buf: ScriptBuf; + + // Determine the effective script type for this leaf + let effective_script_type = if leaf_script_type == LeafScriptType::Mixed { + // For Mixed mode, check if this is the leaf of interest with an override + if leaf_index == leaf_to_spend_from && leaf_to_spend_from_type.is_some() { + leaf_to_spend_from_type.unwrap() + } else { + // Default pattern: even indices use Schnorr, odd indices use SLH-DSA + if leaf_index % 2 == 0 { + LeafScriptType::SchnorrOnly + } else { + LeafScriptType::SlhDsaOnly + } + } + } else { + leaf_script_type + }; + + match effective_script_type { + LeafScriptType::SchnorrOnly => { + let schnorr_keypair = acquire_schnorr_keypair(); + keypairs = MultiKeypair::new_schnorr_only(schnorr_keypair); + let pubkey_bytes = keypairs.schnorr_keypair().unwrap().public_key_bytes(); + // OP_PUSHBYTES_32 <32-byte xonly pubkey> OP_CHECKSIG + let mut script_buf_bytes = vec![0x20]; + script_buf_bytes.extend_from_slice(&pubkey_bytes); + script_buf_bytes.push(0xac); // OP_CHECKSIG + script_buf = ScriptBuf::from_bytes(script_buf_bytes); + }, + LeafScriptType::SlhDsaOnly => { + let slh_dsa_keypair = acquire_slh_dsa_keypair(); + keypairs = MultiKeypair::new_slh_dsa_only(slh_dsa_keypair); + let pubkey_bytes = keypairs.slh_dsa_keypair().unwrap().public_key_bytes(); + // OP_PUSHBYTES_32 <32-byte pubkey> OP_SUBSTR + let mut script_buf_bytes = vec![0x20]; + script_buf_bytes.extend_from_slice(&pubkey_bytes); + script_buf_bytes.push(0x7f); // OP_SUBSTR + script_buf = ScriptBuf::from_bytes(script_buf_bytes); + }, + LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => { + // For combined scripts, we need both keypairs + let schnorr_keypair = acquire_schnorr_keypair(); + let slh_dsa_keypair = acquire_slh_dsa_keypair(); + keypairs = MultiKeypair::new_combined(schnorr_keypair, slh_dsa_keypair); + + let schnorr_pubkey = keypairs.schnorr_keypair().unwrap().public_key_bytes(); + let slh_dsa_pubkey = keypairs.slh_dsa_keypair().unwrap().public_key_bytes(); + + // Debug: Print the private key used for script construction + info!("SLH-DSA DEBUG: Script construction using private key: {}", hex::encode(keypairs.slh_dsa_keypair().unwrap().secret_key_bytes())); + info!("SLH-DSA DEBUG: Script construction using public key: {}", hex::encode(&slh_dsa_pubkey)); + + // Combined script: OP_CHECKSIG OP_SUBSTR OP_BOOLAND OP_VERIFY + let mut script_buf_bytes = vec![0x20]; // OP_PUSHBYTES_32 + script_buf_bytes.extend_from_slice(&schnorr_pubkey); + script_buf_bytes.push(0xac); // OP_CHECKSIG + script_buf_bytes.push(0x20); // OP_PUSHBYTES_32 + script_buf_bytes.extend_from_slice(&slh_dsa_pubkey); + script_buf_bytes.push(0x7f); // OP_SUBSTR + script_buf_bytes.push(0x9a); // OP_BOOLAND + script_buf_bytes.push(0x69); // OP_VERIFY + script_buf = ScriptBuf::from_bytes(script_buf_bytes); + } + LeafScriptType::Mixed => { + // This shouldn't happen as Mixed is resolved to a specific type above + panic!("LeafScriptType::Mixed should have been resolved to a specific type"); + } + LeafScriptType::NotApplicable => { + panic!("LeafScriptType::NotApplicable is not applicable"); + } + } + + let random_weight = thread_rng().gen_range(0..total_leaf_count); + + let huffman_entry = (random_weight, script_buf.clone()); + huffman_entries.push(huffman_entry); + if leaf_index == leaf_to_spend_from { + keypairs_of_interest = Some(keypairs); + script_buf_of_interest = Some(script_buf.clone()); + actual_leaf_type_of_interest = effective_script_type; + debug!("Selected leaf {}: type: {:?}, weight: {}, script: {:?}", + leaf_index, effective_script_type, random_weight, script_buf); + } + } + return (huffman_entries, keypairs_of_interest.unwrap(), script_buf_of_interest.unwrap(), actual_leaf_type_of_interest); +} + +/// Parses the TAP_TREE_LOCK_TYPE environment variable and returns the corresponding LeafScriptType. +/// Defaults to LeafScriptType::SchnorrOnly if the environment variable is not set. +/// Exits with error code 1 if an invalid value is provided. +/// +/// Supported values: +/// - SLH_DSA_ONLY: All leaves use SLH-DSA signatures +/// - SCHNORR_ONLY: All leaves use Schnorr signatures +/// - CONCATENATED_SCHNORR_AND_SLH_DSA: All leaves require both Schnorr and SLH-DSA signatures +/// - MIXED: Different leaves use different algorithms (Schnorr or SLH-DSA) (default) +pub fn tap_tree_lock_type() -> LeafScriptType { + match env::var("TAP_TREE_LOCK_TYPE") { + Ok(value) => match value.as_str() { + "SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly, + "SCHNORR_ONLY" => LeafScriptType::SchnorrOnly, + "CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf, + "MIXED" => LeafScriptType::Mixed, + _ => { + error!("Invalid TAP_TREE_LOCK_TYPE '{}'. Must be one of: SLH_DSA_ONLY, SCHNORR_ONLY, CONCATENATED_SCHNORR_AND_SLH_DSA, MIXED", value); + std::process::exit(1); + } + }, + Err(_) => { + // Default to Mixed if not set + LeafScriptType::Mixed + } + } +} + +pub fn create_p2mr_multi_leaf_taptree() -> TaptreeReturn { + let leaf_script_type = tap_tree_lock_type(); + + let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(leaf_script_type); + let p2mr_builder: P2mrBuilder = P2mrBuilder::with_huffman_tree(huffman_entries).unwrap(); + + + let p2mr_spend_info: P2mrSpendInfo = p2mr_builder.clone().finalize().unwrap(); + let merkle_root:TapNodeHash = p2mr_spend_info.merkle_root.unwrap(); + + + let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap(); + let mut script_leaves: ScriptLeaves = tap_tree.script_leaves(); + let script_leaf = script_leaves + .find(|leaf| leaf.script() == script_buf_of_interest.as_script()) + .expect("Script leaf not found"); + + let merkle_root_node_info: NodeInfo = p2mr_builder.clone().into_inner().try_into_node_info().unwrap(); + let merkle_root: TapNodeHash = merkle_root_node_info.node_hash(); + + let leaf_hash: TapLeafHash = TapLeafHash::from_script(script_leaf.script(), LeafVersion::from_consensus(P2MR_LEAF_VERSION).unwrap()); + + // Convert leaf hash to big-endian for display (like Bitcoin Core) + let mut leaf_hash_bytes = leaf_hash.as_raw_hash().to_byte_array().to_vec(); + leaf_hash_bytes.reverse(); + + info!("leaf_hash: {}, merkle_root: {}, merkle_root: {}", + hex::encode(leaf_hash_bytes), + merkle_root, + merkle_root); + + let leaf_script = script_leaf.script(); + let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch(); + + info!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch); + + let control_block: P2mrControlBlock = P2mrControlBlock{ + merkle_branch: merkle_branch.clone(), + }; + + // Not a requirement here but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2mr UTXO + control_block.verify_script_in_merkle_root_path(leaf_script, merkle_root); + + let control_block_hex: String = hex::encode(control_block.serialize()); + + return TaptreeReturn { + leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes() + .into_iter() + .map(|bytes| hex::encode(bytes)) + .collect(), + leaf_script_hex: leaf_script.to_hex_string(), + tree_root_hex: hex::encode(merkle_root.to_byte_array()), + control_block_hex: control_block_hex, + leaf_script_type: actual_leaf_type.to_string(), + }; +} + +pub fn create_p2tr_multi_leaf_taptree(p2tr_internal_pubkey_hex: String) -> TaptreeReturn { + + let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(LeafScriptType::SchnorrOnly); + + let pub_key_string = format!("02{}", p2tr_internal_pubkey_hex); + let internal_pubkey: PublicKey = pub_key_string.parse::().unwrap(); + let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into(); + + let p2tr_builder: TaprootBuilder = TaprootBuilder::with_huffman_tree(huffman_entries).unwrap(); + let p2tr_spend_info: TaprootSpendInfo = p2tr_builder.clone().finalize(&SECP, internal_xonly_pubkey).unwrap(); + let merkle_root: TapNodeHash = p2tr_spend_info.merkle_root().unwrap(); + + // During taproot construction, the internal key is "tweaked" by adding a scalar (the tap tweak hash) to it. + // If this tweaking operation results in a public key w/ an odd Y-coordinate, the parity bit is set to 1. + // When spending via script path, the verifier needs to know whether the output key has an even or odd Y-coordinate to properly reconstruct & verify the internal key. + // The internal key can be recovered from the output key using the parity bit and the merkle root. + let output_key_parity: Parity = p2tr_spend_info.output_key_parity(); + let output_key: XOnlyPublicKey = p2tr_spend_info.output_key().into(); + + info!("keypairs_of_interest: \n\tsecret_bytes: {:?} \n\tpubkeys: {:?} \n\tmerkle_root: {}", + keypairs_of_interest.secret_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::>(), // secret_bytes returns big endian + keypairs_of_interest.public_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::>(), // serialize returns little endian + merkle_root); + + let tap_tree: TapTree = p2tr_builder.clone().try_into_taptree().unwrap(); + let mut script_leaves: ScriptLeaves = tap_tree.script_leaves(); + let script_leaf = script_leaves + .find(|leaf| leaf.script() == script_buf_of_interest.as_script()) + .expect("Script leaf not found"); + let leaf_script = script_leaf.script().to_hex_string(); + let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch(); + debug!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch); + + let control_block: ControlBlock = ControlBlock{ + leaf_version: LeafVersion::TapScript, + output_key_parity: output_key_parity, + internal_key: internal_xonly_pubkey, + merkle_branch: merkle_branch.clone(), + }; + let control_block_hex: String = hex::encode(control_block.serialize()); + + // Not a requirement but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2tr UTXO + let verify: bool = verify_taproot_commitment(control_block_hex.clone(), output_key, script_leaf.script()); + info!("verify_taproot_commitment: {}", verify); + + return TaptreeReturn { + leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes() + .into_iter() + .map(|bytes| hex::encode(bytes)) + .collect(), + leaf_script_hex: leaf_script, + tree_root_hex: hex::encode(merkle_root.to_byte_array()), + control_block_hex: control_block_hex, + leaf_script_type: actual_leaf_type.to_string(), + }; +} + +/// Parses the BITCOIN_NETWORK environment variable and returns the corresponding Network. +/// Defaults to Network::Regtest if the environment variable is not set or has an invalid value. +pub fn get_bitcoin_network() -> Network { + let mut bitcoin_network: Network = Network::Regtest; + + // Check for BITCOIN_NETWORK environment variable and override if set + if let Ok(network_str) = std::env::var("BITCOIN_NETWORK") { + bitcoin_network = match network_str.to_lowercase().as_str() { + "regtest" => Network::Regtest, + "testnet" => Network::Testnet, + "signet" => Network::Signet, + _ => { + debug!("Invalid BITCOIN_NETWORK value '{}', using default Regtest network", network_str); + Network::Regtest + } + }; + } + + bitcoin_network +} + +pub fn create_p2mr_utxo(merkle_root_hex: String) -> UtxoReturn { + + let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap(); + let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap()); + + /* commit (in scriptPubKey) to the merkle root of all the script path leaves. ie: + This output key is what gets committed to in the final P2MR address (ie: scriptPubKey) + */ + let script_buf: P2mrScriptBuf = P2mrScriptBuf::new_p2mr(merkle_root); + let script: &Script = script_buf.as_script(); + let script_pubkey = script.to_hex_string(); + + let bitcoin_network = get_bitcoin_network(); + + // derive bech32m address and verify against test vector + // p2mr address is comprised of network HRP + WitnessProgram (version + program) + let bech32m_address = Address::p2mr(Some(merkle_root), bitcoin_network); + + return UtxoReturn { + script_pubkey_hex: script_pubkey, + bech32m_address: bech32m_address.to_string(), + bitcoin_network, + }; + +} + +// Given script path p2tr or p2mr UTXO details, spend to p2wpkh +pub fn pay_to_p2wpkh_tx( + funding_tx_id_bytes: Vec, + funding_utxo_index: u32, + funding_utxo_amount_sats: u64, + funding_script_pubkey_bytes: Vec, + control_block_bytes: Vec, + leaf_script_bytes: Vec, + leaf_script_priv_keys_bytes: Vec>, // Changed to support multiple private keys + spend_output_pubkey_hash_bytes: Vec, + spend_output_amount_sats: u64, + leaf_script_type: LeafScriptType +) -> SpendDetails { + + let mut txid_little_endian = funding_tx_id_bytes.clone(); // initially in big endian format + txid_little_endian.reverse(); // convert to little endian format + + // vin: Create TxIn from the input utxo + // Details of this input tx are not known at this point + let input_tx_in = bitcoin::TxIn { + previous_output: OutPoint { + txid: bitcoin::Txid::from_slice(&txid_little_endian).unwrap(), // bitcoin::Txid expects the bytes in little-endian format + vout: funding_utxo_index, + }, + script_sig: ScriptBuf::new(), // Empty for segwit transactions - script goes in witness + sequence: Sequence::MAX, // Default sequence, allows immediate spending (no RBF or timelock) + witness: bitcoin::Witness::new(), // Empty for now, will be filled with signature and pubkey after signing + }; + + let spend_wpubkey_hash = WPubkeyHash::from_byte_array(spend_output_pubkey_hash_bytes.try_into().unwrap()); + let spend_output: TxOut = TxOut { + value: Amount::from_sat(spend_output_amount_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&spend_wpubkey_hash), + }; + + // The spend tx to eventually be signed and broadcast + let mut unsigned_spend_tx = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::locktime::absolute::LockTime::ZERO, + input: vec![input_tx_in], + output: vec![spend_output], + }; + + // Create the leaf hash + let leaf_script = ScriptBuf::from_bytes(leaf_script_bytes.clone()); + let leaf_hash: TapLeafHash = TapLeafHash::from_script(&leaf_script, LeafVersion::TapScript); + + /* prevouts parameter tells the sighash algorithm: + 1. The value of each input being spent (needed for fee calculation and sighash computation) + 2. The scriptPubKey of each input being spent (ie: type of output & how to validate the spend) + */ + let prevouts = vec![TxOut { + value: Amount::from_sat(funding_utxo_amount_sats), + script_pubkey: ScriptBuf::from_bytes(funding_script_pubkey_bytes.clone()), + }]; + info!("prevouts: {:?}", prevouts); + + let spending_tx_input_index = 0; + + // Create SighashCache + // At this point, sighash_cache does not know the values and type of input UTXO + let mut tapscript_sighash_cache = SighashCache::new(&mut unsigned_spend_tx); + + // Compute the sighash + let tapscript_sighash: TapSighash = tapscript_sighash_cache.taproot_script_spend_signature_hash( + spending_tx_input_index, // input_index + &Prevouts::All(&prevouts), + leaf_hash, + TapSighashType::All + ).unwrap(); + + info!("sighash: {:?}", tapscript_sighash); + + let spend_msg = Message::from(tapscript_sighash); + + let mut derived_witness: Witness = Witness::new(); + let mut sig_bytes = Vec::new(); + match leaf_script_type { + LeafScriptType::SlhDsaOnly => { + if leaf_script_priv_keys_bytes.len() != 1 { + panic!("SlhDsaOnly requires exactly one private key"); + } + let secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice( + Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[0]).unwrap(); + let signature = sign(&secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S"); + debug!("SlhDsaOnly signature.bytes: {:?}", signature.bytes.len()); + let mut sig_bytes_with_sighash = signature.bytes.clone(); + sig_bytes_with_sighash.push(TapSighashType::All as u8); + derived_witness.push(&sig_bytes_with_sighash); + sig_bytes = signature.bytes; + }, + LeafScriptType::SchnorrOnly => { + if leaf_script_priv_keys_bytes.len() != 1 { + panic!("SchnorrOnly requires exactly one private key"); + } + // assumes bytes are in big endian format + let secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap(); + + // Spending a p2tr UTXO thus using Schnorr signature + // The aux_rand parameter ensures that signing the same message with the same key produces the same signature + // Otherwise (without providing aux_rand), the secp256k1 library internally generates a random nonce for each signature + let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand( + &spend_msg, + &secret_key.keypair(&SECP), + &[0u8; 32] // 32 zero bytes of auxiliary random data + ); + sig_bytes = signature.serialize().to_vec(); + let mut sig_bytes_with_sighash = sig_bytes.clone(); + sig_bytes_with_sighash.push(TapSighashType::All as u8); + derived_witness.push(&sig_bytes_with_sighash); + debug!("SchnorrOnly signature bytes: {:?}", sig_bytes.len()); + }, + LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => { + if leaf_script_priv_keys_bytes.len() != 2 { + panic!("SchnorrAndSlhDsa requires exactly two private keys (Schnorr first, then SLH-DSA)"); + } + + // Generate Schnorr signature (first key) + let schnorr_secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap(); + let schnorr_signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand( + &spend_msg, + &schnorr_secret_key.keypair(&SECP), + &[0u8; 32] // 32 zero bytes of auxiliary random data + ); + // Build combined signature for return value (without sighash bytes) + let mut combined_sig_bytes = schnorr_signature.serialize().to_vec(); + debug!("SchnorrAndSlhDsa schnorr_sig_bytes: {:?}", combined_sig_bytes.len()); + + // Generate SLH-DSA signature (second key) + let slh_dsa_secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice( + Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[1]).unwrap(); + + // Debug: Print the private key being used for signature creation + info!("SLH-DSA DEBUG: Using private key for signature creation: {}", hex::encode(&leaf_script_priv_keys_bytes[1])); + + let slh_dsa_signature = sign(&slh_dsa_secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S"); + debug!("SchnorrAndSlhDsa slh_dsa_signature.bytes: {:?}", slh_dsa_signature.bytes.len()); + + // Add SLH-DSA signature to combined signature for return value + combined_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes); + sig_bytes = combined_sig_bytes; + + // Build witness with sighash bytes + let mut witness_sig_bytes = schnorr_signature.serialize().to_vec(); + witness_sig_bytes.push(TapSighashType::All as u8); + witness_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes); + witness_sig_bytes.push(TapSighashType::All as u8); + derived_witness.push(&witness_sig_bytes); + } + LeafScriptType::Mixed => { + // Mixed is not a valid type for spending - the actual leaf type should be used + panic!("LeafScriptType::Mixed is not valid for spending. Use the actual leaf type (SchnorrOnly or SlhDsaOnly)."); + } + LeafScriptType::NotApplicable => { + panic!("LeafScriptType::NotApplicable is not applicable"); + } + } + // Note: sighash byte is now appended to signatures, not as separate witness element + derived_witness.push(&leaf_script_bytes); + derived_witness.push(&control_block_bytes); + + let derived_witness_vec: Vec = derived_witness.iter().flatten().cloned().collect(); + + // Update the witness data for the tx's first input (index 0) + *tapscript_sighash_cache.witness_mut(spending_tx_input_index).unwrap() = derived_witness; + + // Get the signed transaction. + let signed_tx_obj: &mut Transaction = tapscript_sighash_cache.into_transaction(); + + let tx_hex = bitcoin::consensus::encode::serialize_hex(&signed_tx_obj); + + return SpendDetails { + tx_hex, + sighash: tapscript_sighash.as_byte_array().to_vec(), + sig_bytes: sig_bytes, + derived_witness_vec: derived_witness_vec, + }; +} + + +pub fn create_p2tr_utxo(merkle_root_hex: String, internal_pubkey_hex: String) -> UtxoReturn { + + let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap(); + let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap()); + + let pub_key_string = format!("02{}", internal_pubkey_hex); + let internal_pubkey: PublicKey = pub_key_string.parse::().unwrap(); + let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into(); + + + let script_buf: ScriptBuf = ScriptBuf::new_p2tr(&SECP, internal_xonly_pubkey, Option::Some(merkle_root)); + let script: &Script = script_buf.as_script(); + let script_pubkey = script.to_hex_string(); + + let bitcoin_network = get_bitcoin_network(); + + // 4) derive bech32m address and verify against test vector + // p2mr address is comprised of network HRP + WitnessProgram (version + program) + let bech32m_address = Address::p2tr( + &SECP, + internal_xonly_pubkey, + Option::Some(merkle_root), + bitcoin_network + ); + + return UtxoReturn { + script_pubkey_hex: script_pubkey, + bech32m_address: bech32m_address.to_string(), + bitcoin_network, + }; + +} + + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#examples +pub fn tagged_hash(tag: &str, data: &[u8]) -> String { + + // Create a hash of the tag first + let tag_hash = sha256::Hash::hash(tag.as_bytes()); + + // Create preimage: tag_hash || tag_hash || message + // tag_hash is prefixed twice so that the prefix is 64 bytes in total + let mut preimage = sha256::Hash::engine(); + preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // First tag hash + preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // Second tag hash + preimage.write_all(data).unwrap(); // Message data + let hash = sha256::Hash::from_engine(preimage).to_byte_array(); + hex::encode(hash) +} + +pub fn serialize_script(script: &Vec) -> Vec { + // get length of script as number of bytes + let length = script.len(); + + // return script with compact size prepended + let mut result = compact_size(length as u64); + result.extend_from_slice(&script); + result +} + +/// Encodes an integer into Bitcoin's compact size format +/// Returns a Vec containing the encoded bytes +fn compact_size(n: u64) -> Vec { + if n <= 252 { + vec![n as u8] + } else if n <= 0xffff { + let mut result = vec![0xfd]; + result.extend_from_slice(&(n as u16).to_le_bytes()); + result + } else if n <= 0xffffffff { + let mut result = vec![0xfe]; + result.extend_from_slice(&(n as u32).to_le_bytes()); + result + } else { + let mut result = vec![0xff]; + result.extend_from_slice(&n.to_le_bytes()); + result + } +} + +pub fn acquire_schnorr_keypair() -> UnifiedKeypair { + + /* OsRng typically draws from the OS's entropy pool (hardware random num generators, system events, etc), ie: + * 1. $ cat /proc/sys/kernel/random/entropy_avail + * 2. $ sudo dmesg | grep -i "random\|rng\|entropy" + + The Linux kernel's RNG (/dev/random and /dev/urandom) typically combines multiple entropy sources: ie: + * Hardware RNG (if available) + * CPU RNG instructions (RDRAND/RDSEED) + * Hardware events (disk I/O, network packets, keyboard/mouse input) + * Timer jitter + * Interrupt timing + */ + let keypair = Keypair::new(&SECP, &mut OsRng); + + let privkey: SecretKey = keypair.secret_key(); + let pubkey: (XOnlyPublicKey, Parity) = XOnlyPublicKey::from_keypair(&keypair); + UnifiedKeypair::new_schnorr(privkey, pubkey.0) +} + +pub fn verify_schnorr_signature_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool { + + // schnorr is 64 bytes so remove possible trailing Sighash Type byte if present + let mut sig_bytes = signature.to_vec(); + if sig_bytes.len() == 65 { + sig_bytes.pop(); // Remove the last byte + } + let signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap(); + let message = Message::from_digest_slice(message).unwrap(); + let pubkey = XOnlyPublicKey::from_slice(pubkey_bytes).unwrap(); + verify_schnorr_signature(signature, message, pubkey) +} + +pub fn verify_slh_dsa_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool { + + // Remove possible trailing Sighash Type byte if present (SLH-DSA-128S is 7856 bytes, so 7857 would indicate SIGHASH byte) + let mut sig_bytes = signature.to_vec(); + if sig_bytes.len() == 7857 { + sig_bytes.pop(); // Remove the last byte + } + + info!("verify_slh_dsa_via_bytes: signature length: {:?}, message: {:?}, pubkey_bytes: {:?}", + sig_bytes.len(), + hex::encode(message), + hex::encode(pubkey_bytes)); + + let signature = bitcoinpqc::Signature::try_from_slice(Algorithm::SLH_DSA_128S, &sig_bytes).unwrap(); + let public_key: bitcoinpqc::PublicKey = bitcoinpqc::PublicKey::try_from_slice(Algorithm::SLH_DSA_128S, pubkey_bytes).unwrap(); + verify(&public_key, message, &signature).is_ok() +} + +pub fn verify_schnorr_signature(mut signature: Signature, message: Message, pubkey: XOnlyPublicKey) -> bool { + + // schnorr is 64 bytes so remove possible trailing Sighash Type byte if present + if signature.serialize().to_vec().len() == 65 { + let mut sig_bytes = signature.serialize().to_vec(); + sig_bytes.pop(); // Remove the last byte + signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap(); + } + let is_valid: bool = SECP.verify_schnorr(&signature, &message, &pubkey).is_ok(); + if !is_valid { + error!("verify schnorr failed:\n\tsignature: {:?}\n\tmessage: {:?}\n\tpubkey: {:?}", + signature, + message, + hex::encode(pubkey.serialize())); + } + is_valid +} + +/* 1. Re-constructs merkle_root from merkle_path (found in control_block) and provided script. + 2. Determines the parity of the output key via the control byte (found in the control block). + - the parity bit indicates whether the output key has an even or odd Y-coordinate + 3. Computes the tap tweak hash using the internal key and reconstructed merkle root. + - tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root) + 4. Verifies that the provided output key can be derived from the internal key using the tweak. + - tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root) + 5. This proves the script is committed to in the taptree described by the output key. + */ +pub fn verify_taproot_commitment(control_block_hex: String, output_key: XOnlyPublicKey, script: &Script) -> bool { + + let control_block_bytes = hex::decode(control_block_hex).unwrap(); + let control_block: ControlBlock = ControlBlock::decode(&control_block_bytes).unwrap(); + + return control_block.verify_taproot_commitment(&SECP, output_key, script); +} + +fn acquire_slh_dsa_keypair() -> UnifiedKeypair { + /* + In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to: + * Initialize hash function parameters within the key generation + * Seed the Merkle tree construction that forms the public key + * Generate the secret key components that enable signing + */ + let random_data = get_random_bytes(128); + let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data) + .expect("Failed to generate SLH-DSA-128S keypair"); + UnifiedKeypair::new_slh_dsa(keypair) +} + +fn get_random_bytes(size: usize) -> Vec { + let mut bytes = vec![0u8; size]; + rng().fill_bytes(&mut bytes); + bytes +} diff --git a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs new file mode 100644 index 0000000000..bd86ba21fd --- /dev/null +++ b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs @@ -0,0 +1,274 @@ +use std::collections::HashSet; +use bitcoin::{Network, ScriptBuf}; +use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash}; +use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo, P2MR_LEAF_VERSION}; +use bitcoin::hashes::Hash; + +use hex; +use log::debug; +use once_cell::sync::Lazy; + +use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn}; +use p2mr_ref::error::P2MRError; +use p2mr_ref::{create_p2mr_utxo, tagged_hash}; + +// This file contains tests that execute against the BIP360 script-path-only test vectors. + +static TEST_VECTORS: Lazy = Lazy::new(|| { + let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_construction.json"); + let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap(); + assert_eq!(test_vectors.version, 1); + test_vectors +}); + +static P2TR_USING_V2_WITNESS_VERSION_ERROR: &str = "p2tr_using_v2_witness_version_error"; +static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error"; +static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree"; +static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves"; +static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version"; +static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex"; +static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative"; +static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract"; + +#[test] +fn test_p2tr_using_v2_witness_version_error() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2TR_USING_V2_WITNESS_VERSION_ERROR).unwrap(); + let test_result: anyhow::Result<()> = process_test_vector_p2tr(test_vector); + assert!(matches!(test_result.unwrap_err().downcast_ref::(), + Some(P2MRError::P2trRequiresWitnessVersion1))); +} + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple +#[test] +fn test_p2mr_missing_leaf_script_tree_error() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap(); + let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector); + assert!(matches!(test_result.unwrap_err().downcast_ref::(), + Some(P2MRError::MissingScriptTreeLeaf))); +} + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple +#[test] +fn test_p2mr_single_leaf_script_tree() { + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_different_version_leaves() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap(); + let test_result = process_test_vector_p2mr(test_vector); + assert!(matches!(test_result.unwrap_err().downcast_ref::(), + Some(P2MRError::InvalidLeafVersion(_, _)))); +} + +#[test] +fn test_p2mr_simple_lightning_contract() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_two_leaf_same_version() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_three_leaf_complex() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_three_leaf_alternative() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +fn process_test_vector_p2tr(test_vector: &TestVector) -> anyhow::Result<()> { + let script_pubkey_hex = test_vector.expected.script_pubkey.as_ref().unwrap(); + let script_pubkey_bytes = hex::decode(script_pubkey_hex).unwrap(); + if script_pubkey_bytes[0] != 0x51 { + return Err(P2MRError::P2trRequiresWitnessVersion1.into()); + } + Ok(()) +} + +fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { + + let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref(); + + let mut tv_leaf_count: u8 = 0; + let mut current_branch_id: u8 = 0; + + // TaprootBuilder expects the addition of each leaf script with its associated depth + // It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash + // Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism + let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new(); + + let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new(); + let mut traversal_result: anyhow::Result<()> = Ok(()); + + // 1) traverse test vector script tree and add leaves to P2MR builder + if let Some(script_tree) = tv_script_tree { + + script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| { + + if traversal_result.is_err() { return; } + + if let TVScriptTree::Leaf(tv_leaf) = node { + + let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap(); + + // NOTE: IOT to execute script_info.control_block(..), will add these to a vector + let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone()); + let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap(); + + if tv_leaf.leaf_version != P2MR_LEAF_VERSION { + traversal_result = Err(P2MRError::InvalidLeafVersion(tv_leaf.id, tv_leaf.leaf_version).into()); + return; + } + + control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version)); + + let mut modified_depth = depth + 1; + if direction == Direction::Root { + modified_depth = depth; + } + debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}", + tv_leaf_count, depth, modified_depth, direction, tv_leaf.script); + + p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version) + .unwrap_or_else(|e| { + panic!("Failed to add leaf: {:?}", e); + }); + + tv_leaf_count += 1; + } else if let TVScriptTree::Branch { left, right } = node { + // No need to calculate branch hash. + // TaprootBuilder does this for us. + debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction); + current_branch_id += 1; + } + }); + }else { + return Err(P2MRError::MissingScriptTreeLeaf.into()); + } + + traversal_result?; + + let spend_info: P2mrSpendInfo = p2mr_builder.clone() + .finalize() + .unwrap_or_else(|e| { + panic!("finalize failed: {:?}", e); + }); + + let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap(); + + // 2) verify derived merkle root against test vector + let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap(); + assert_eq!( + derived_merkle_root.to_string(), + *test_vector_merkle_root, + "Merkle root mismatch" + ); + debug!("just passed merkle root validation: {}", test_vector_merkle_root); + + let test_vector_leaf_hashes_vec: Vec = test_vector.intermediary.leaf_hashes.clone(); + let test_vector_leaf_hash_set: HashSet = test_vector_leaf_hashes_vec.iter().cloned().collect(); + let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks; + let test_vector_control_blocks_set: HashSet = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect(); + let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap(); + let script_leaves: ScriptLeaves = tap_tree.script_leaves(); + + // TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors. + // 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks + for derived_leaf in script_leaves { + + let version = derived_leaf.version(); + let script = derived_leaf.script(); + let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch(); + + let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version); + let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array()); + assert!( + test_vector_leaf_hash_set.contains(&leaf_hash), + "Leaf hash not found in expected set for {}", leaf_hash + ); + debug!("just passed leaf_hash validation: {}", leaf_hash); + + // Each leaf in the script tree has a corresponding control block. + // Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here: + // https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block + // The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0). + // There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight. + // NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data + debug!("merkle_branch nodes: {:?}", merkle_branch); + let derived_control_block: P2mrControlBlock = P2mrControlBlock{ + merkle_branch: merkle_branch.clone(), + }; + let serialized_control_block = derived_control_block.serialize(); + debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}", + derived_control_block, + merkle_branch.len(), + derived_control_block.size(), + serialized_control_block.len()); + let derived_serialized_control_block = hex::encode(serialized_control_block); + assert!( + test_vector_control_blocks_set.contains(&derived_serialized_control_block), + "Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set + ); + debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block); + + } + + let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string()); + + assert_eq!( + p2mr_utxo_return.script_pubkey_hex, + *test_vector.expected.script_pubkey.as_ref().unwrap(), + "Script pubkey mismatch" + ); + debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex); + + let bech32m_address: String = p2mr_utxo_return.bech32m_address; + debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address); + + if p2mr_utxo_return.bitcoin_network == Network::Bitcoin { + assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch."); + } + + Ok(()) +} diff --git a/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs new file mode 100644 index 0000000000..a5ca855cad --- /dev/null +++ b/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs @@ -0,0 +1,240 @@ +use std::collections::HashSet; +use bitcoin::{Network, ScriptBuf}; +use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash}; +use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo}; +use bitcoin::hashes::Hash; + +use hex; +use log::debug; +use once_cell::sync::Lazy; + +use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn}; +use p2mr_ref::error::P2MRError; +use p2mr_ref::{create_p2mr_utxo, tagged_hash}; + +// This file contains tests that execute against the BIP360 script-path-only test vectors. + +static TEST_VECTORS: Lazy = Lazy::new(|| { + let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_pqc_construction.json"); + let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap(); + assert_eq!(test_vectors.version, 1); + test_vectors +}); + +static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error"; +static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree"; +static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves"; +static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version"; +static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex"; +static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative"; +static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract"; + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple +#[test] +fn test_p2mr_pqc_missing_leaf_script_tree_error() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap(); + let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector); + assert!(matches!(test_result.unwrap_err().downcast_ref::(), + Some(P2MRError::MissingScriptTreeLeaf))); +} + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple +#[test] +fn test_p2mr_pqc_single_leaf_script_tree() { + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_pqc_different_version_leaves() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_pqc_simple_lightning_contract() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_pqc_two_leaf_same_version() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_pqc_three_leaf_complex() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +#[test] +fn test_p2mr_pqc_three_leaf_alternative() { + + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let test_vectors = &*TEST_VECTORS; + let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap(); + process_test_vector_p2mr(test_vector).unwrap(); +} + +fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { + + let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref(); + + let mut tv_leaf_count: u8 = 0; + let mut current_branch_id: u8 = 0; + + // TaprootBuilder expects the addition of each leaf script with its associated depth + // It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash + // Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism + let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new(); + + let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new(); + + // 1) traverse test vector script tree and add leaves to P2MR builder + if let Some(script_tree) = tv_script_tree { + + script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| { + + if let TVScriptTree::Leaf(tv_leaf) = node { + + let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap(); + + // NOTE: IOT to execute script_info.control_block(..), will add these to a vector + let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone()); + let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap(); + control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version)); + + let mut modified_depth = depth + 1; + if direction == Direction::Root { + modified_depth = depth; + } + debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}", + tv_leaf_count, depth, modified_depth, direction, tv_leaf.script); + + // NOTE: Some of the the test vectors in this project specify leaves with non-standard versions (ie: 250 / 0xfa) + p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version) + .unwrap_or_else(|e| { + panic!("Failed to add leaf: {:?}", e); + }); + + tv_leaf_count += 1; + } else if let TVScriptTree::Branch { left, right } = node { + // No need to calculate branch hash. + // TaprootBuilder does this for us. + debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction); + current_branch_id += 1; + } + }); + }else { + return Err(P2MRError::MissingScriptTreeLeaf.into()); + } + + let spend_info: P2mrSpendInfo = p2mr_builder.clone() + .finalize() + .unwrap_or_else(|e| { + panic!("finalize failed: {:?}", e); + }); + + let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap(); + + // 2) verify derived merkle root against test vector + let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap(); + assert_eq!( + derived_merkle_root.to_string(), + *test_vector_merkle_root, + "Merkle root mismatch" + ); + debug!("just passed merkle root validation: {}", test_vector_merkle_root); + + let test_vector_leaf_hashes_vec: Vec = test_vector.intermediary.leaf_hashes.clone(); + let test_vector_leaf_hash_set: HashSet = test_vector_leaf_hashes_vec.iter().cloned().collect(); + let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks; + let test_vector_control_blocks_set: HashSet = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect(); + let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap(); + let script_leaves: ScriptLeaves = tap_tree.script_leaves(); + + // TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors. + // 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks + for derived_leaf in script_leaves { + + let version = derived_leaf.version(); + let script = derived_leaf.script(); + let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch(); + + let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version); + let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array()); + assert!( + test_vector_leaf_hash_set.contains(&leaf_hash), + "Leaf hash not found in expected set for {}", leaf_hash + ); + debug!("just passed leaf_hash validation: {}", leaf_hash); + + // Each leaf in the script tree has a corresponding control block. + // Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here: + // https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block + // The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0). + // There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight. + // NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data + debug!("merkle_branch nodes: {:?}", merkle_branch); + let derived_control_block: P2mrControlBlock = P2mrControlBlock{ + merkle_branch: merkle_branch.clone(), + }; + let serialized_control_block = derived_control_block.serialize(); + debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}", + derived_control_block, + merkle_branch.len(), + derived_control_block.size(), + serialized_control_block.len()); + let derived_serialized_control_block = hex::encode(serialized_control_block); + assert!( + test_vector_control_blocks_set.contains(&derived_serialized_control_block), + "Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set + ); + debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block); + + } + + let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string()); + + assert_eq!( + p2mr_utxo_return.script_pubkey_hex, + *test_vector.expected.script_pubkey.as_ref().unwrap(), + "Script pubkey mismatch" + ); + debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex); + + let bech32m_address: String = p2mr_utxo_return.bech32m_address; + debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address); + + if p2mr_utxo_return.bitcoin_network == Network::Bitcoin { + assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch."); + } + + Ok(()) +} diff --git a/bip-0360/ref-impl/rust/tests/p2mr_spend.rs b/bip-0360/ref-impl/rust/tests/p2mr_spend.rs new file mode 100644 index 0000000000..c06063bb50 --- /dev/null +++ b/bip-0360/ref-impl/rust/tests/p2mr_spend.rs @@ -0,0 +1,102 @@ +use log::info; +use bitcoin::blockdata::witness::Witness; + +use p2mr_ref::{ pay_to_p2wpkh_tx, serialize_script }; + +use p2mr_ref::data_structures::{SpendDetails, LeafScriptType}; + +/* The rust-bitcoin crate does not provide a single high-level API that builds the full Taproot script-path witness stack for you. + It does expose all the necessary types and primitives to build it manually and correctly. +*/ + +// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple +#[test] +fn test_script_path_spend_simple() { + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let script_inputs_count = hex::decode("03").unwrap(); + let script_inputs_bytes: Vec = hex::decode("08").unwrap(); + let leaf_script_bytes: Vec = hex::decode("5887").unwrap(); + let control_block_bytes: Vec = + hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap(); + let test_witness_bytes: Vec = hex::decode( + "03010802588721c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329", + ) + .unwrap(); + + let mut derived_witness: Witness = Witness::new(); + derived_witness.push(script_inputs_count); + derived_witness.push(serialize_script(&script_inputs_bytes)); + derived_witness.push(serialize_script(&leaf_script_bytes)); + derived_witness.push(serialize_script(&control_block_bytes)); + + info!("witness: {:?}", derived_witness); + + let derived_witness_vec: Vec = derived_witness.iter().flatten().cloned().collect(); + + assert_eq!(derived_witness_vec, test_witness_bytes); +} + + +// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature +// Spends from a p2mr UTXO to a p2wpk UTXO +#[test] +fn test_script_path_spend_signatures() { + let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error + + let funding_tx_id_bytes: Vec = + hex::decode("d1c40446c65456a9b11a9dddede31ee34b8d3df83788d98f690225d2958bfe3c").unwrap(); + + // The input index of the funding tx + let funding_tx_index: u32 = 0; + + let funding_utxo_amount_sats: u64 = 20000; + + // OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG + let input_leaf_script_bytes: Vec = + hex::decode("206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac").unwrap(); + + // Modified from learnmeabitcoin example + // Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path. + let input_control_block_bytes: Vec = + hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap(); + + let input_script_pubkey_bytes: Vec = + hex::decode("5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80") + .unwrap(); + let input_script_priv_key_bytes: Vec = hex::decode("9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189").unwrap(); + + // Convert to Vec> format expected by the function + let input_script_priv_keys_bytes: Vec> = vec![input_script_priv_key_bytes]; + + + // https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs + let spend_output_pubkey_bytes: Vec = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap(); + + let spend_output_amount_sats: u64 = 15000; + + let test_sighash_bytes: Vec = hex::decode("752453d473e511a0da2097d664d69fe5eb89d8d9d00eab924b42fc0801a980c9").unwrap(); + let test_signature_bytes: Vec = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f").unwrap(); + + // Modified from learnmeabitcoin example + // Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path. + let test_witness_bytes: Vec = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f01206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0acc1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap(); + + let result: SpendDetails = pay_to_p2wpkh_tx(funding_tx_id_bytes, + funding_tx_index, + funding_utxo_amount_sats, + input_script_pubkey_bytes, + input_control_block_bytes, + input_leaf_script_bytes, + input_script_priv_keys_bytes, // Now passing Vec> format + spend_output_pubkey_bytes, + spend_output_amount_sats, + LeafScriptType::SchnorrOnly // This test uses a Schnorr signature + ); + + assert_eq!(result.sighash.as_slice(), test_sighash_bytes.as_slice(), "sighash mismatch"); + assert_eq!(result.sig_bytes, test_signature_bytes, "signature mismatch"); + assert_eq!(result.derived_witness_vec, test_witness_bytes, "derived_witness mismatch"); + +} + diff --git a/bip-0374.mediawiki b/bip-0374.mediawiki index 840bbf815d..a8588b96e8 100644 --- a/bip-0374.mediawiki +++ b/bip-0374.mediawiki @@ -116,6 +116,10 @@ This proposal is compatible with all older clients. == Test Vectors and Reference Code == A reference python implementation is included [https://github.com/bitcoin/bips/blob/master/bip-0374/reference.py here]. +It uses a vendored copy of the [https://github.com/secp256k1lab/secp256k1lab/ secp256k1lab] library at version 1.0.0 +(commit [https://github.com/secp256k1lab/secp256k1lab/commit/44dc4bd893b8f03e621585e3bf255253e0e0fbfb +44dc4bd893b8f03e621585e3bf255253e0e0fbfb]). + Test vectors can be generated by running ./bip-0374/gen_test_vectors.py which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with ./bip-0374/run_test_vectors.py. == Changelog == diff --git a/bip-0374/gen_test_vectors.py b/bip-0374/gen_test_vectors.py index 792a59a45b..a828074e4a 100755 --- a/bip-0374/gen_test_vectors.py +++ b/bip-0374/gen_test_vectors.py @@ -1,30 +1,29 @@ #!/usr/bin/env python3 """Generate the BIP-0374 test vectors.""" import csv -import os -import sys +from pathlib import Path from reference import ( - TaggedHash, dleq_generate_proof, dleq_verify_proof, ) -from secp256k1 import G as GENERATOR, GE +from secp256k1lab.secp256k1 import G as GENERATOR, GE +from secp256k1lab.util import tagged_hash NUM_SUCCESS_TEST_VECTORS = 8 DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng" -FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') -FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') +FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv' +FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv' def random_scalar_int(vector_i, purpose): - rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) + rng_out = tagged_hash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) return int.from_bytes(rng_out, 'big') % GE.ORDER def random_bytes(vector_i, purpose): - rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) + rng_out = tagged_hash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) return rng_out diff --git a/bip-0374/reference.py b/bip-0374/reference.py index e0fcbeaf97..8068b9c25c 100755 --- a/bip-0374/reference.py +++ b/bip-0374/reference.py @@ -2,30 +2,22 @@ """Reference implementation of DLEQ BIP for secp256k1 with unit tests.""" -from hashlib import sha256 +from pathlib import Path import random -from secp256k1 import G, GE import sys import unittest +# Prefer the vendored copy of secp256k1lab +sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) +from secp256k1lab.secp256k1 import G, GE +from secp256k1lab.util import tagged_hash, xor_bytes + DLEQ_TAG_AUX = "BIP0374/aux" DLEQ_TAG_NONCE = "BIP0374/nonce" DLEQ_TAG_CHALLENGE = "BIP0374/challenge" -def TaggedHash(tag: str, data: bytes) -> bytes: - ss = sha256(tag.encode()).digest() - ss += ss - ss += data - return sha256(ss).digest() - - -def xor_bytes(lhs: bytes, rhs: bytes) -> bytes: - assert len(lhs) == len(rhs) - return bytes([lhs[i] ^ rhs[i] for i in range(len(lhs))]) - - def dleq_challenge( A: GE, B: GE, C: GE, R1: GE, R2: GE, m: bytes | None, G: GE, ) -> int: @@ -33,7 +25,7 @@ def dleq_challenge( assert len(m) == 32 m = bytes([]) if m is None else m return int.from_bytes( - TaggedHash( + tagged_hash( DLEQ_TAG_CHALLENGE, A.to_bytes_compressed() + B.to_bytes_compressed() @@ -59,9 +51,9 @@ def dleq_generate_proof( assert len(m) == 32 A = a * G C = a * B - t = xor_bytes(a.to_bytes(32, "big"), TaggedHash(DLEQ_TAG_AUX, r)) + t = xor_bytes(a.to_bytes(32, "big"), tagged_hash(DLEQ_TAG_AUX, r)) m_prime = bytes([]) if m is None else m - rand = TaggedHash( + rand = tagged_hash( DLEQ_TAG_NONCE, t + A.to_bytes_compressed() + C.to_bytes_compressed() + m_prime ) k = int.from_bytes(rand, "big") % GE.ORDER diff --git a/bip-0374/run_test_vectors.py b/bip-0374/run_test_vectors.py index 4831fbe20b..54349aed48 100755 --- a/bip-0374/run_test_vectors.py +++ b/bip-0374/run_test_vectors.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 """Run the BIP-DLEQ test vectors.""" import csv -import os +from pathlib import Path import sys from reference import ( dleq_generate_proof, dleq_verify_proof, ) -from secp256k1 import GE +from secp256k1lab.secp256k1 import GE -FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') -FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') +FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv' +FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv' all_passed = True diff --git a/bip-0374/secp256k1lab/.github/workflows/main.yml b/bip-0374/secp256k1lab/.github/workflows/main.yml new file mode 100644 index 0000000000..4950b96550 --- /dev/null +++ b/bip-0374/secp256k1lab/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Tests +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx ruff check . + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + - run: uvx mypy . diff --git a/bip-0374/secp256k1lab/.python-version b/bip-0374/secp256k1lab/.python-version new file mode 100644 index 0000000000..bd28b9c5c2 --- /dev/null +++ b/bip-0374/secp256k1lab/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/bip-0374/secp256k1lab/CHANGELOG.md b/bip-0374/secp256k1lab/CHANGELOG.md new file mode 100644 index 0000000000..15779717c4 --- /dev/null +++ b/bip-0374/secp256k1lab/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-03-31 + +Initial release. diff --git a/bip-0374/secp256k1lab/COPYING b/bip-0374/secp256k1lab/COPYING new file mode 100644 index 0000000000..e8f2163641 --- /dev/null +++ b/bip-0374/secp256k1lab/COPYING @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2009-2024 The Bitcoin Core developers +Copyright (c) 2009-2024 Bitcoin Developers +Copyright (c) 2025- The secp256k1lab Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bip-0374/secp256k1lab/README.md b/bip-0374/secp256k1lab/README.md new file mode 100644 index 0000000000..dbc9dbd04c --- /dev/null +++ b/bip-0374/secp256k1lab/README.md @@ -0,0 +1,13 @@ +secp256k1lab +============ + +![Dependencies: None](https://img.shields.io/badge/dependencies-none-success) + +An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education. + +Features: +* Low-level secp256k1 field and group arithmetic. +* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). +* ECDH key exchange. + +WARNING: The code in this library is slow and trivially vulnerable to side channel attacks. diff --git a/bip-0374/secp256k1lab/pyproject.toml b/bip-0374/secp256k1lab/pyproject.toml new file mode 100644 index 0000000000..a0bdd19f42 --- /dev/null +++ b/bip-0374/secp256k1lab/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "secp256k1lab" +version = "1.0.0" +description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education" +readme = "README.md" +authors = [ + { name = "Pieter Wuille", email = "pieter@wuille.net" }, + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +maintainers = [ + { name = "Tim Ruffing", email = "me@real-or-random.org" }, + { name = "Jonas Nick", email = "jonasd.nick@gmail.com" }, + { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" } +] +requires-python = ">=3.9" +license = "MIT" +license-files = ["COPYING"] +keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", +] +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0374/secp256k1lab/src/secp256k1lab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py new file mode 100644 index 0000000000..ba839d16e1 --- /dev/null +++ b/bip-0374/secp256k1lab/src/secp256k1lab/bip340.py @@ -0,0 +1,73 @@ +# The following functions are based on the BIP 340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + +from .secp256k1 import FE, GE, G +from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash + + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_xonly() + + +def schnorr_sign( + msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340" +) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand)) + P = d0 * G + assert not P.infinity + d = d0 if P.has_even_y() else GE.ORDER - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand)) + k0 = ( + int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg)) + % GE.ORDER + ) + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = k0 * G + assert not R.infinity + k = k0 if R.has_even_y() else GE.ORDER - k0 + e = ( + int_from_bytes( + tagged_hash( + tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg + ) + ) + % GE.ORDER + ) + sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER) + assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix) + return sig + + +def schnorr_verify( + msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340" +) -> bool: + if len(pubkey) != 32: + raise ValueError("The public key must be a 32-byte array.") + if len(sig) != 64: + raise ValueError("The signature must be a 64-byte array.") + try: + P = GE.from_bytes_xonly(pubkey) + except ValueError: + return False + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (r >= FE.SIZE) or (s >= GE.ORDER): + return False + e = ( + int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg)) + % GE.ORDER + ) + R = s * G - e * P + if R.infinity or (not R.has_even_y()) or (R.x != r): + return False + return True diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0374/secp256k1lab/src/secp256k1lab/ecdh.py @@ -0,0 +1,16 @@ +import hashlib + +from .secp256k1 import GE, Scalar + + +def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE: + """TODO""" + shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey) + assert not shared_secret.infinity # prime-order group + return shared_secret + + +def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes: + """TODO""" + shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey) + return hashlib.sha256(shared_secret.to_bytes_compressed()).digest() diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/keys.py b/bip-0374/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0374/secp256k1lab/src/secp256k1lab/keys.py @@ -0,0 +1,15 @@ +from .secp256k1 import GE, G +from .util import int_from_bytes + +# The following function is based on the BIP 327 reference implementation +# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py + + +# Return the plain public key corresponding to a given secret key +def pubkey_gen_plain(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= GE.ORDER - 1): + raise ValueError("The secret key must be an integer in the range 1..n-1.") + P = d0 * G + assert not P.infinity + return P.to_bytes_compressed() diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/py.typed b/bip-0374/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..6e262bf51e --- /dev/null +++ b/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -0,0 +1,454 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE: int + + def __init__(self, a=0, b=1): + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, type(self)): + num = a._num + den = a._den + else: + num = a % self.SIZE + den = 1 + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE + else: + den = (den * b) % self.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num = num + self._den = den + + def __add__(self, a): + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented + + def __radd__(self, a): + """Compute the sum of an integer and a field element.""" + return type(self)(a) + self + + @classmethod + # REVIEW This should be + # def sum(cls, *es: Iterable[Self]) -> Self: + # but Self needs the typing_extension package on Python <= 3.12. + def sum(cls, *es): + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) + + def __sub__(self, a): + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented + + def __rsub__(self, a): + """Compute the difference of an integer and a field element.""" + return type(self)(a) - self + + def __mul__(self, a): + """Compute the product of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented + + def __rmul__(self, a): + """Compute the product of an integer with a field element.""" + return type(self)(a) * self + + def __truediv__(self, a): + """Compute the ratio of two field elements (second may be int).""" + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented + + def __pow__(self, a): + """Raise a field element to an integer power.""" + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) + + def __neg__(self): + """Negate a field element.""" + return type(self)(-self._num, self._den) + + def __int__(self): + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE + self._den = 1 + return self._num + + def sqrt(self): + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError + + def is_square(self): + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self): + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a): + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + return (self._num - self._den * a) % self.SIZE == 0 + + def to_bytes(self): + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @classmethod + def from_int_checked(cls, v): + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v): + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b): + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b): + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) + + def __str__(self): + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self): + """Get a string representation of this field element.""" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self): + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + GE objects are immutable. + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = Scalar.SIZE + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + @property + def infinity(self): + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self): + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self): + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + + def __init__(self, x=None, y=None): + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self._infinity = True + else: + # Initialize as point on the curve (and check that it is). + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self._infinity = False + self._x = fx + self._y = fy + + def __add__(self, a): + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def sum(*ps): + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps): + """Compute a (batch) scalar group element multiplication. + + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(int(a), p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a): + """Multiply an integer with a group element.""" + if self == G: + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) + + def __neg__(self): + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def __sub__(self, a): + """Subtract a group element from another.""" + return self + (-a) + + def __eq__(self, a): + """Check if two group elements are equal.""" + return (self - a).infinity + + def has_even_y(self): + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + + def to_bytes_compressed(self): + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_compressed_with_infinity(self): + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + + def to_bytes_uncompressed(self): + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self): + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x): + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + raise ValueError + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes_compressed(b): + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_uncompressed(b): + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + + @staticmethod + def from_bytes(b): + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + return GE.from_bytes_compressed(b) + else: + return GE.from_bytes_uncompressed(b) + + @staticmethod + def from_bytes_xonly(b): + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r + + @staticmethod + def is_valid_x(x): + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self): + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self): + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + + def __hash__(self): + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p): + self.table = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a): + result = GE() + a = int(a) + for bit in range(a.bit_length()): + if a & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/bip-0374/secp256k1lab/src/secp256k1lab/util.py b/bip-0374/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0374/secp256k1lab/src/secp256k1lab/util.py @@ -0,0 +1,24 @@ +import hashlib + + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() diff --git a/bip-0380.mediawiki b/bip-0380.mediawiki index 3f113f2a60..e6311224be 100644 --- a/bip-0380.mediawiki +++ b/bip-0380.mediawiki @@ -334,4 +334,7 @@ This Table lists all available Script expressions and the BIPs specifying them. |- | musig(KEY, KEY, ..., KEY) | [[bip-0390.mediawiki|390]] +|- +| sp(KEY), sp(KEY, KEY) +| [[bip-0392.mediawiki|392]] |} diff --git a/bip-0383.mediawiki b/bip-0383.mediawiki index 60ef16fedc..b4008f71ab 100644 --- a/bip-0383.mediawiki +++ b/bip-0383.mediawiki @@ -56,7 +56,7 @@ For values greater than 16, they must be a push of the signed little endian enco The only change for sortedmulti() is that the keys are sorted lexicographically prior to the creation of the output script. This sorting is on the keys that are to be put into the output script, i.e. after all extended keys are derived. -===Multiple Extended Keys=== +===Multiple Extended Keys=== When one or more the key expressions in a multi() or sortedmulti() expression are extended keys, the derived keys use the same child index. This changes the keys in lockstep and allows for output scripts to be indexed in the same way that the derived keys are indexed. diff --git a/bip-0390.mediawiki b/bip-0390.mediawiki index 2daed4d2de..a6b5526a67 100644 --- a/bip-0390.mediawiki +++ b/bip-0390.mediawiki @@ -7,6 +7,7 @@ Type: Informational Assigned: 2024-06-04 License: CC0-1.0 + Version: 0.2.0 Requires: 380, 328 @@ -33,8 +34,8 @@ and [[bip-0389.mediawiki|BIP-389]]. ===musig(KEY, KEY, ..., KEY)=== -The musig(KEY, KEY, ..., KEY) expression can only be used inside of a tr() or -rawtr() expression as a key expression. It additionally cannot be nested within another musig() +The musig(KEY, KEY, ..., KEY) expression can only be used inside of a tr(), rawtr() +or sp() expression as a key expression. It additionally cannot be nested within another musig() expression. Participant public keys may be repeated. The aggregate public key is produced by using the KeyAgg algorithm on all KEYs specified in the expression after performing all specified derivation. As with script expressions, KEY can contain child derivation specified by @@ -118,6 +119,11 @@ are likely to be familiar with them. The reference implementation is available in Bitcoin Core [[https://github.com/bitcoin/bitcoin/pull/31244|PR #31244]]. +==Changelog== + +* __0.2.0__ (2026-03-04) - Allow musig() inside sp() expressions. +* __0.1.0__ (2025-12-08) - Allow musig() inside rawtr() expressions. + ==Acknowledgements== Thanks to Pieter Wuille, Andrew Poelstra, Sanket Kanjalkar, Salvatore Ingala, and all others who diff --git a/bip-0392.mediawiki b/bip-0392.mediawiki new file mode 100644 index 0000000000..c1424bfdd2 --- /dev/null +++ b/bip-0392.mediawiki @@ -0,0 +1,114 @@ +
+  BIP: 392
+  Layer: Applications
+  Title: Silent Payment Output Script Descriptors
+  Authors: Craig Raw 
+  Status: Draft
+  Type: Specification
+  Assigned: 2026-02-06
+  License: BSD-2-Clause
+  Discussion: https://groups.google.com/g/bitcoindev/c/bP6ktUyCOJI
+  Requires: 341, 350, 352, 380
+
+ +==Abstract== + +This document specifies sp() output script descriptors for silent payments. +sp() descriptors take silent payment key material and describe P2TR outputs when combined with sender input public keys as defined in BIP352. + +==Copyright== + +This BIP is licensed under the BSD 2-clause license. + +==Motivation== + +BIP352 defines silent payments, a protocol for static payment addresses without on-chain linkability. +This descriptor provides a standardized way to represent silent payment outputs within the output descriptor framework, enabling wallet interoperability and backup/recovery using existing descriptor-based infrastructure. + +==Specification== + +A new top level script expression is defined: sp(). + +===Key Expressions=== + +Two new key expression types are defined for use with sp() descriptors: + +====spscan==== + +The spscan key expression encodes the scan private key and spend public key. +It is a [https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki Bech32m] encoding of: +* The human-readable part "spscan" for mainnet, "tspscan" for testnets +* The data-part values: +** The character "q", to represent silent payments version 0 +** The payload: ser256(bscan) || serP(Bspend) + +====spspend==== + +The spspend key expression encodes both the scan and spend private keys. +It is a [https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki Bech32m] encoding of: +* The human-readable part "spspend" for mainnet, "tspspend" for testnets +* The data-part values: +** The character "q", to represent silent payments version 0 +** The payload: ser256(bscan) || ser256(bspend) + +Note: The serialization of ser256(p) and serP(P) follows the definition in BIP352. + +===sp()=== + +The sp(KEY) or sp(KEY,KEY) expression can only be used as a top level descriptor. + +sp(KEY) takes a single key expression as an argument, which must be either an spscan or spspend encoded key, optionally with key origin information. +If included, the key origin information specifies the fingerprint and derivation path to the depth from which the scan and spend keys are derived using the child paths recommended in BIP352 (1h/0 for scan, 0h/0 for spend). + +sp(KEY,KEY) takes two key expressions. +The first key expression represents the scan key and must be a single private key (e.g. a WIF compressed private key or xprv extended private key). +The second key expression represents the spend key and may be any key expression as defined in BIP380 or other key expression BIPs (e.g. musig() as defined in BIP390) that represents a single key, public or private. +Note however that uncompressed keys are not allowed under any sp() expression, as BIP352 only permits compressed public keys. + +When combined with sender input public keys, the descriptor produces P2TR output scripts describing silent payments made to wallets represented by the key expression(s). + +The output scripts produced are BIP341 taproot outputs as specified in BIP352. + +==Examples== + +Valid descriptors: + +* sp(spscan1q...) - Using spscan encoded key (watch-only) +* sp([deadbeef/352h/0h/0h]spscan1q...) - With key origin +* sp(spspend1q...) - Using spspend encoded key (full wallet) +* sp(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600) - WIF scan key with compressed public spend key (watch-only) +* sp([deadbeef/352h/0h/0h]xprv.../0h,xpub.../0h) - Extended private scan key with extended public spend key (watch-only) +* sp([deadbeef/352h/0h/0h]xprv.../0h,xprv.../0h) - Extended private keys for both scan and spend (full wallet) +* sp(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,musig(03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,023590a94e768f8e1815c2f24b4d80a8e3149316c3518ce7b7ad338368d038ca66)) - WIF scan key with MuSig2 aggregate spend key (watch-only) + +Invalid descriptors: + +* sp() requires at least one key expression +* sp(xpub...) single argument form requires spscan or spspend encoded key +* sp(xpub...,xpub...) two argument form requires private scan key (e.g. WIF or xprv) +* sp(spscan1q...,spscan1q...) two argument form requires single key expressions +* sp(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600) uncompressed private key +* sh(sp(spscan1q...)) sp() is top level only +* wsh(sp(spscan1q...)) sp() is top level only + +==Usage Notes== + +For watch-only wallets, use spscan encoding or the two argument form with a public spend key. +For full wallets that can both scan and spend, use spspend encoding or the two argument form with a private spend key. + +When using the two argument form, the scan key must be private (e.g. WIF or xprv) since scanning requires the private scan key. + +==Backwards Compatibility== + +sp() descriptors use the format and general operation specified in BIP380. +As this is a wholly new descriptor, it is not compatible with any prior implementation. +The scripts produced are BIP341 taproot outputs, making them indistinguishable from other taproot outputs on-chain. + +==Reference Implementation== + +TBD + +==Test Vectors== + +TBD + diff --git a/bip-0434.md b/bip-0434.md new file mode 100644 index 0000000000..afa770e168 --- /dev/null +++ b/bip-0434.md @@ -0,0 +1,322 @@ +``` + BIP: 434 + Layer: Peer Services + Title: Peer Feature Negotiation + Authors: Anthony Towns + Status: Draft + Type: Specification + Assigned: 2026-01-14 + License: BSD-2-Clause + Discussion: 2025-12-19: https://gnusha.org/pi/bitcoindev/aUUXLgEUCgGb122o@erisian.com.au/T/#u + 2020-08-21: https://gnusha.org/pi/bitcoindev/20200821023647.7eat4goqqrtaqnna@erisian.com.au/ + Version: 0.1.0 +``` + +## Abstract + +This BIP defines a peer-to-peer (P2P) message that can be used for +announcements and negotiation related to support of new peer-to-peer +features. + +## Motivation + +Historically, new peer-to-peer protocol changes have been tied to +bumping the protocol version, so that nodes know to only attempt +feature negotiation with peers that support the feature. Coordinating +the protocol version across implementations, when different clients may +have different priorities for features to implement, is an unnecessary +burden in the upgrade process for P2P features that do not require +universal support. And at a more philosophical level, having the P2P +protocol be [permissionlessly extensible][permless-extensible], with no +coordination required between implementations or developers, seems ideal +for a decentralized system. + +Many earlier P2P protocol upgrades were implemented as new messages +sent after a peer connection is set up (ie, after receipt of a `verack` +message by both sides). See [BIP 130 (sendheaders)][BIP130], [BIP 133 +(feefilter)][BIP133], and [BIP 152 (compact blocks)][BIP152] for some +examples. However, for some P2P upgrades, it is helpful to perform +feature negotiation prior to a connection being fully established +(ie, prior to the `verack` being received by both sides). [BIP 155 +(addrv2)][BIP155] and [BIP 339 (wtxid-relay)][BIP339] are examples of +this approach, which involves sending and receiving a single new message +(`sendaddrv2` and `wtxidrelay` respectively), in between `version` and +`verack` to indicate support of the new feature. + +In all these cases, sending new messages on the network raises the +question of what non-implementing software will do with such messages. The +common behavior observed on the network was for software to ignore +unknown messages received from a peer, so these proposals posed minimal +risk of potential network partitioning. In fact, supporting protocol +extensibility in this manner was given as an explicit reason to ignore +unknown messages in Bitcoin's [first release][0.1-extensibility]. + +However, if nodes respond to unknown messages by disconnecting, then +the network might partition in the future as incompatible software is +deployed. And in fact, some clients on the network have historically +discouraged or disallowed unknown messages, both between `version` +and `verack` (eg, Bitcoin Core discouraged such messages between +[PR#9720][PR#9720] and [PR#19723][PR#19723], and btcd disallowed +such messages until [PR#1812][btcd#1812], but see also discussion in +[#1661][btcd#1661]), as well as after `verack`. + +To maximise compatibility with such clients, most of these BIPs require +that peers bump the protocol version: + + * [BIP 130][BIP130] requires version 70012 or higher, + * [BIP 133][BIP133] requires version 70013 or higher, + * [BIP 152][BIP152] recommends version 70014/70015 or higher, and + * [BIP 339][BIP339] requires version 70016 or higher. + +And while [BIP 155][BIP155] does not specify a minimum protocol version, +implementations have [added][PR#20564] a de facto requirement of version +70016 or higher. + +In this BIP, we propose codifying and generalising the mechanism used by +[BIP 339][BIP339] for future P2P upgrades, by adding a single new feature +negotiation message that can be reused for advertising arbitrary new +features, and requiring that implementing software ignore unknown features +that might be advertised. This allows future upgrades to negotiate new +features by exchanging messages prior to exchanging `verack` messages, +without concerns of being unnecessarily disconnected by a peer which +doesn't understand the messages, and without needing to coordinate +updating the protocol version. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in RFC 2119. + +For the purposes of this section, `CompactSize` refers to the +variable-length integer encoding used across the existing P2P protocol +to encode array lengths, among other things, in 1, 3, 5 or 9 bytes. Only +`CompactSize` encodings which are minimally-encoded (ie the shortest +length possible) are used by this specification. + +Nodes implementing this BIP: + + * MUST advertise a protocol version number `>= 70017`, + * MUST NOT send `feature` messages to peers that advertise a protocol + version number `< 70017`, + * MUST accept `feature` messages received after the `version` message + and before the `verack` message, and + * MUST NOT send `feature` messages after sending the `verack` message. + +In addition, nodes implementing this BIP: + + * SHOULD ignore unknown messages received after the `version` message + and before the `verack` message, + * MAY ignore `feature` messages sent after `verack`, and + * MAY disconnect peers who send `feature` messages after `verack`. + +Feature specifications based on this BIP: + + * MUST forbid sending messages it introduces after `verack` to a peer + that has not indicated support for the feature via a `feature` + message. + +### `feature` message + +The payload of the `feature` message contains exactly the following data: + +| Type | Name | Description | +| ----------- | ------------- | ----------- | +| string | `featureid` | Unique identifier for the feature | +| byte-vector | `featuredata` | Feature-specific configuration data | + +The `featureid` is encoded in the usual way, that is, as a `CompactSize` +specifying the string length, followed by that many bytes. The string +length MUST be between 4 and 80, inclusive. The string SHOULD include +only printable ASCII characters (ie, each byte should have a value +between 32 and 126, inclusive). + +Likewise, `featuredata` is encoded as a `CompactSize` specifying the +byte-vector size, followed by that many bytes. How these bytes are +interpreted is part of the feature's specification. The byte-vector size +MUST NOT be more than 512 bytes. Note that the `featuredata` field is not +optional, so if no data is required, an empty vector should be provided, +ie serialized as `CompactSize` of 0. + +Nodes implementing this BIP MUST ignore `feature` messages specifying a +`featureid` they do not support, so long as the payload conforms to the +requirements above. + +Nodes implementing this BIP MAY disconnect peers that send `feature` +messages where the `feature` message's payload cannot be correctly +parsed (including having missing or additional data), even if they do +not recognise the `featureid`. + +The `featureid` MUST be a globally unique identifier for the feature. +For features published as a BIP, the `featureid` SHOULD be the assigned +BIP number, eg "BIP434", or be based on the BIP number (eg, "BIP434v2" +where the "v2" suffix covers versioning, or "BIP434.3" where the ".3" +suffix covers part 3 of the BIP). For experimental features that do not +(yet) have a BIP number assigned, some other unique identifier MUST be +chosen, such as a URL to the repository where development is taking place, +or the sha256 digest of some longer reference. + +#### `feature` message 1-byte identifier + +Nodes implementing both this BIP and [BIP 324 (v2 P2P encrypted +transport)][BIP324] MUST treat a message with a 1-byte `message_type` +equal to `37` that is received prior to `verack` as the `feature` message. + +### Feature negotiation + +It is RECOMMENDED that feature negotiation be designed and implemented +as follows: + + * all `feature` messages and the `verack` message should be sent + immediately on receipt of the peer's `version` message + * any negotiation calculations should be performed immediately on + receipt of the peer's `verack` message + +This structure is fairly easy to implement, and avoids introducing any +significant latency that might result from more interactive negotiation +methods. + +Feature specifications defining a `featureid` MAY make use of the +following approaches: + +#### Feature advertisement: + + 1. Send a `feature` message advertising the `featureid` unconditionally + 2. Accept messages related to the feature unconditionally + 3. Only send messages defined by the feature if the peer sent + a valid `feature` message for the `featureid`. + +This approach is appropriate for many simple features that define +new messages, particularly where an implementation might only +implement sending or receiving a message, but not both, eg [BIP 35 +(mempool)][BIP35]. + +#### Feature coordination: + + 1. Send a `feature` message advertising the `featureid` unconditionally + 2. Check if the peer sends the same `feature` message (or a compatible + one), and enable the feature for this peer if so. + 3. Only send/accept messages or encode data items according to the + feature's specification if the feature is enabled for this peer. + +This approach is appropriate for upgrades to data encoding in +P2P messages, eg [BIP 339 (wtxidrelay)][BIP339] or [BIP 155 +(addrv2)][BIP155]. + +#### Feature versioning: + + 1. Send `feature` messages for multiple incompatible features, eg + `BIP434v3`, `BIP434v2`, `BIP434v1`, ordered from most preferred + to least. + 2. Track the corresponding `feature` messages from your peer. + 3. If you were the listening peer, enable your highest preference feature + that your peer also supports. + 4. If you were the initiating peer, enable the first feature that your + peer announced, that you also support. + 5. For example if the listening peer sends `BIP434v3`, `BIP434v2`, + `BIP434v1`, and the initiating peer sends `BIP434v1`, `BIP434v2`, + then the listening peer should select `BIP434v2` when `verack` + is received, and the initiating peer should select `BIP434v2` + as soon as `feature BIP434v2` is received. + 6. Conversely, if the initiating peer sends `BIP434v3`, `BIP434v2`, + `BIP434v1`, and the listening peer sends `BIP434v1`, `BIP434v2`, + then the listening peer should select `BIP434v1` when `verack` + is received, and the initiating peer should select `BIP434v1` + as soon as `feature BIP434v1` is received. + 7. In most cases, implementations should simply advertise incompatible + features in order from most recent to oldest, on the basis that + the only reason to make incompatible updates is because there are + significant improvements. Exceptions to that may occur when two + incompatible features are both receiving active development, or + when an implementation has only partially implemented the latest + spec, and the older spec is better supported (and thus should be + listed first, as the preferred protocol to adopt). + +This approach may be appropriate when making substantial changes to a +deployed protocol and backwards compatibility is desirable on a short-term +basis, or when there is disagreement amongst implementations or users +as to which approach is most desirable. + +## Considerations + +The advantage this approach has over bumping the protocol version +number when introducing new P2P messages or data structures, is that no +coordination is required (that is, there is no longer a question whether +version "n+1" belongs to Alice's new feature, or Bob's new feature), +and there is no implication that supporting each new feature means all +prior features are also supported. + +The advantage this approach has over defining new messages for each +feature is that the `featureid` can be much longer (at up to 80 +bytes) than a message type id (which are limited to 12 bytes). With a +[BIP 324][BIP324] one-byte `message_type`, the overhead compared to that +approach is also kept small. + +This approach is largely equivalent to adding a [payload to the `verack` +message][verack-payload] (eg, a vector of `featureid`, `featuredata` +pairs). It was chosen because: + + * it retains compatibility with any implementations that expect `verack` + to have no payload; + * it allows peers to process each feature request individually, rather than + having to first load the configuration information for all features into + memory at once (in order to validate the message's checksum), and then + deal with each feature's configuration; + * limiting the maximum message payload size you accept (eg to 4MB) + does not limit the number of features you can accept; and + * we have experience with negotiating features with individual messages, + but no experience with doing so via `verack` payload. + +A mild disadvantage compared to using a `verack` payload is that this +approach allows the possibility of interactive feature negotiation prior +to `verack`. However interactive feature negotiation is always possible +simply by having the initiating peer disconnect and reconnect after +discovering the listening peer's supported features. + +This specification attempts to maximise compatibility with implementations +that prefer to fully validate each message received: + + * `feature` messages, even for unknown features, must always be fully + parseable into a `featureid` and `featuredata` + * Ignoring unknown messages prior to `verack` is only a recommendation, + not a requirement, so compliant implementations may disconnect on an + unknown message that cannot be validated. + * Sending unknown messages after `verack` is explicitly forbidden, + in so far as that is possible. + +## Backward compatibility + +Clients specifying a version number prior to `70017` remain fully +compatible with this change. + +Clients specifying a version number of `70017` or higher that do not +implement this BIP remain fully compatible provided they do not disconnect +peers upon receiving unexpected messages received between `version` and +`verack`. + +## Acknowledgements + +Much of the logic here, and much of the text in the motivation section, +is based on Suhas Daftuar's 2020 post on [Generalizing feature +negotiation][suhas-draft]. + +## Copyright + +This BIP is licensed under the 2-clause BSD license. + +[BIP130]: https://github.com/bitcoin/bips/blob/master/bip-0130.mediawiki +[BIP133]: https://github.com/bitcoin/bips/blob/master/bip-0133.mediawiki +[BIP152]: https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki +[BIP155]: https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki +[BIP339]: https://github.com/bitcoin/bips/blob/master/bip-0339.mediawiki +[BIP35]: https://github.com/bitcoin/bips/blob/master/bip-0035.mediawiki +[BIP324]: https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki +[verack-payload]: https://gnusha.org/pi/bitcoindev/B514142F-382B-4D49-B68D-0115ECBD1D79@voskuil.org/ +[PR#20564]: https://github.com/bitcoin/bitcoin/pull/20564 +[PR#9720]: https://github.com/bitcoin/bitcoin/pull/9720 +[PR#19723]: https://github.com/bitcoin/bitcoin/pull/19723 +[btcd#1812]: https://github.com/btcsuite/btcd/pull/1812 +[btcd#1661]: https://github.com/btcsuite/btcd/issues/1661 +[permless-extensible]: https://github.com/bitcoin/bitcoin/pull/20564#issuecomment-738456560 +[0.1-extensibility]: https://github.com/benjiqq/bitcoinArchive/blob/master/bitcoin0.1/src/main.cpp#L2035-L2039 +[suhas-draft]: https://gnusha.org/pi/bitcoindev/CAFp6fsE=HPFUMFhyuZkroBO_QJ-dUWNJqCPg9=fMJ3Jqnu1hnw@mail.gmail.com/ diff --git a/bip-0442.md b/bip-0442.md new file mode 100644 index 0000000000..928973a4fb --- /dev/null +++ b/bip-0442.md @@ -0,0 +1,251 @@ +``` + BIP: 442 + Layer: Consensus (soft fork) + Title: OP_PAIRCOMMIT + Authors: moonsettler + Brandon Black + Status: Draft + Type: Specification + Assigned: 2024-12-09 + License: BSD-3-Clause + Discussion: https://delvingbitcoin.org/t/op-paircommit-as-a-candidate-for-addition-to-lnhance/1216 + https://groups.google.com/g/bitcoindev/c/si6ZNIkVfOw/m/29VY_YRrCgAJ +``` + +## Abstract + +This BIP proposes a new tapscript opcode, `OP_PAIRCOMMIT`, which enables efficient and secure commitment to pairs of stack elements using a tagged hash construction. This opcode provides limited vector commitment functionality, facilitating more expressive contracts and improved data availability in Bitcoin scripts. + +## Summary + +For taproot script spends with leaf version *0xc0* (see [BIP-342]), `OP_PAIRCOMMIT` replaces `OP_SUCCESS205` (*0xcd*). When executed, `OP_PAIRCOMMIT` pops the top two stack elements, computes a *PairCommit* tagged SHA256 hash over their compact size and data, and pushes the resulting 32-byte hash onto the stack. + +## Specification + +Notation follows [BIP-340], including the tagged hash notation: +*hashtag(x) = SHA256(SHA256(tag) || SHA256(tag) || x)* + +Stack operation: + +- If fewer than 2 elements are present, script execution fails. +- Let the stack be *[..., x1, x2]* (top is rightmost). +- Compute: `pc` = *hashPairCommit(compact_size(len(x1)) || x1 || compact_size(len(x2)) || x2)*[^1] +- Pop `x2` and `x1` from the stack. +- Push `pc` onto the stack. + +[^1]: The number of SHA256 blocks is minimized in typical use cases. The tag can be precomputed as a SHA256 mid-state, requiring only two hash cycles for two 32-byte items, or one for two smaller items. + +## Motivation + +Bitcoin scripts often commit to single data items (e.g., hash/time locks, P2PKH), but lack native support for committing to multiple items together. `OP_PAIRCOMMIT` enables Merklized commitments, allowing contracts to commit to trees of elements with a single hash. This supports use cases such as hash lock contracts where any pre-image in a Merkle tree can unlock a spend. + +Combined with `OP_CHECKSIGFROMSTACK`, signing commitments to multiple items enables complex delegation (e.g., delegating to `key1` after time `t1` and `key2` after time `t2`). Previously, such delegation required key laddering[^2], which is more costly in validation. `OP_PAIRCOMMIT` enforces relationships between items efficiently. + +[^2]: Key laddering involves a sequence of keys signing tuples of items and keys, which is costly in bytes and sigops. See [key laddering post] for details. + +## Examples + +### Committing to more than 2 elements + +`OP_PAIRCOMMIT` can commit to a vector of stack elements securely and efficiently. + +```perl +# Commit to three elements: a, b, c +# pc-hash = PC(a, PC(b, c)) + +# Witness: +OP_PAIRCOMMIT # , PC(b, c) +OP_PAIRCOMMIT # PC(a, PC(b, c)) + # PC(a, PC(b, c)), +OP_EQUALVERIFY # +# ... +``` + +### Use in Lightning Symmetry + +Lightning Symmetry contracts require data availability for contested closes. [^3] By forcing parties to include settlement transaction hashes in the witness, later updates can reconstruct scripts of intermediate states while only keeping the latest state. + +[^3]: The required data is a full CTV hash of the settlement transaction when there are open HTLCs, or merely the difference in balance between the channel partners in other cases. Whether the latter optimization would be used is an implementation detail not further discussed here. + +```perl +# S: 500000000 +# internal-key: BIP-327 aggregate key of channel participants +# state-n-hash: { nLockTime(S+n), out(contract, amount(A)+amount(B)) } +# settlement-n-hash: { nSequence(2w), out(A, amount(A)), out(B, amount(B)) } +# state-n-recovery-data: { settlement-n-hash or state-n-balance } +``` +#### Example channel script (pseudo-code) + +```perl +# Witness: +OP_CHECKTEMPLATEVERIFY # , , +OP_PAIRCOMMIT # , PC(state-n-recovery-data, state-n-hash) +OP_INTERNALKEY # , PC(state-n-recovery-data, state-n-hash), +OP_CHECKSIGFROMSTACK # <1> + # <1>, +OP_CHECKLOCKTIMEVERIFY # <1>, +OP_DROP # <1> +``` + +#### Channel update script (pseudo-code) for m > n + +```perl +OP_IF + # Witness: + OP_CHECKTEMPLATEVERIFY # , , + OP_PAIRCOMMIT # , PC(state-m-recovery-data, state-m-hash) + OP_INTERNALKEY # , PC(state-m-recovery-data, state-m-hash), + OP_CHECKSIGFROMSTACK # <1> + # <1>, + OP_CHECKLOCKTIMEVERIFY # <1>, + OP_DROP # <1> +OP_ELSE + # Empty witness stack + # + OP_CHECKTEMPLATEVERIFY # + OP_0NOTEQUAL # <1> +OP_ENDIF +``` + +These constructions ensure both parties sign the same pair hash, requiring inclusion of both update and settlement hashes in the witness. [^4] [^5] + +[^4]: `state-n-hash` commits to a specific `nLockTime` value for the transaction through `OP_CHECKTEMPLATEVERIFY`; `OP_CHECKLOCKTIMEVERIFY` ensures that the state progression can only go forward (the transaction needs to have greater `nLockTime` value than the intermediate state +[^5]: `OP_0NOTEQUAL` can be omitted (any non-zero value left on the stack would be accepted by the script interpreter). + +### In MATT + +The Merklize All The Things ([MATT]) framework uses `OP_CAT` to combine items for commitments. `OP_PAIRCOMMIT` provides a more ergonomic and secure alternative[^6]. + +[^6]: Naive use of `OP_CAT` is vulnerable to byte shifting attacks. E.g. *0x0102 || 0x03* equals *0x01 || 0x0203*. Mitigation requires length checking or hashing. + +## Alternative approaches + +Alternative approaches considered and rejected: + +- `OP_CAT`[^6][^9] +- SHA256 streaming opcodes[^9] +- Merkle operation opcodes +- 'Kitty' CAT: `OP_CAT` with size limits +- `OP_CHECKTEMPLATEVERIFY` committing to the taproot annex[^7] +- `OP_CHECKSIGFROMSTACK` on n elements +- `OP_VECTORCOMMIT`: generalized for n > 2 elements +- ReKey/Laddering[^2] +- `OP_RETURN`[^8] + +[^7]: Committing to the taproot annex allows one additional item, but it is not accessible to script. +[^8]: `OP_RETURN` can commit to additional data, but is costly and not accessible to script. +[^9]: `OP_PAIRCOMMIT` enables useful scripts without the risks of `OP_CAT` (see [CAT-tricks-I], [CAT-tricks-II]). + +## Reference Implementation + +### Code + +```c++ +case OP_PAIRCOMMIT: { + // OP_PAIRCOMMIT is only available in Tapscript + // ... + // x1 x2 -- hash + if (stack.size() < 2) { + return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION); + } + const valtype& vch1 = stacktop(-2); + const valtype& vch2 = stacktop(-1); + + uint256 hash = PairCommitHash(vch1, vch2); + + stack.pop_back(); + stack.pop_back(); + stack.emplace_back(hash.begin(), hash.end()); + break; +} +``` +```c++ +const HashWriter HASHER_PAIRCOMMIT{TaggedHash("PairCommit")}; + +uint256 PairCommitHash(const std::vector& x1, const std::vector& x2) +{ + return (HashWriter{HASHER_PAIRCOMMIT} << x1 << x2).GetSHA256(); +} +``` + +### Pull Request + +https://github.com/lnhance/bitcoin/pull/6/files + +## Rationale + +### Cost comparison of Lightning Symmetry constructions + +| Method | Channel Script | Update Script | Update Witness1 | Force Close2 | Contest | Settlement Mechanism | +|:----------------------|---------------:|--------------:|---------------------------:|------------------------:|--------:|:--------------------:| +| APO-Annex | 8 WU | 113 WU | 100 WU | 1221 WU | 627 WU| SigOp | +| APO-Return | 8 WU | 113 WU | 66 WU | 1359 WU | 765 WU| SigOp | +| CTV+CSFS | 43 WU | 81 WU | 98 WU | 1394 WU | 765 WU| HashEq | +| CTV+CSFS+IKEY | 10 WU | 48 WU | 98 WU | 1328 WU | 732 WU| HashEq | +| CTV+CSFS+IKEY+PC | 11 WU | 49 WU | 131 WU | 1191 WU | 594 WU| HashEq | +| THIKCS-Annex | 10 WU | 49 WU | 98 WU | 1160 WU | 563 WU| HashEq | +| THIKCS-Return | 10 WU | 49 WU | 66 WU | 1329 WU | 733 WU| HashEq | + +1 *Witness is the same weight for both Force Close and Contest in LN-Symmetry* \ +2 *Total cost of unilateral close transactions* \ +3 *THIKCS is short for TEMPLATEHASH+IKEY+CSFS* + +### Proving general computation using trees + +`OP_PAIRCOMMIT` enables Merkle tree commitments, which can represent functions where inputs and corresponding outputs are the leaves. Taproot trees can be 128 levels deep, therefore including up to 2128 possible leaf scripts that can represent complex functions. This is already over the practical computational limits to enumerate and Merklize. + +## Backward Compatibility + +Constraining `OP_SUCCESS` opcodes allows deployment as a backwards-compatible soft fork. Reliance on `OP_SUCCESS205` behavior will be invalidated by `OP_PAIRCOMMIT`. + +## Deployment + +TBD + +## Credits + +Jeremy Rubin, Salvatore Ingala, Anthony Towns, Ademan555, Psifour + +## Copyright + +This document is licensed under the 3-clause BSD license. + +## References + +1. LNhance bitcoin repository: [lnhance] +2. Lightning Symmetry: [eltoo] +3. OP_CAT: [BIP-347], [BIN-2024-0001] +4. OP_CHECKTEMPLATEVERIFY: [BIP-119] +5. OP_CHECKSIGFROMSTACK: [BIP-348], [BIN-2024-0003] +6. OP_CHECKCONTRACTVERIFY: [BIP-443] +7. OP_INTERNALKEY: [BIP-349], [BIN-2024-0004] +8. OP_TEMPLATEHASH: [BIP-446] +9. Tagged hash: [BIP-340] +10. MuSig2: [BIP-327] + +[lnhance]: https://github.com/lnhance/bitcoin +[eltoo]: https://github.com/instagibbs/bolts/blob/eltoo_draft/XX-eltoo-transactions.md +[CAT-tricks-I]: https://medium.com/blockstream/cat-and-schnorr-tricks-i-faf1b59bd298 +[CAT-tricks-II]: https://medium.com/blockstream/cat-and-schnorr-tricks-ii-2f6ede3d7bb5 +[MATT]: https://merkle.fun +[Lightning Symmetry write-up]: https://delvingbitcoin.org/t/ln-symmetry-project-recap/359 +[key laddering post]: https://rubin.io/bitcoin/2024/12/02/csfs-ctv-rekey-symmetry/ + +[//]: # (BIPs referenced) +[BIP-119]: https://github.com/bitcoin/bips/tree/master/bip-0119.mediawiki +[BIP-327]: https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki +[BIP-340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +[BIP-341]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki +[BIP-342]: https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki +[BIP-347]: https://github.com/bitcoin/bips/blob/master/bip-0347.mediawiki +[BIP-348]: https://github.com/bitcoin/bips/blob/master/bip-0348.md +[BIP-349]: https://github.com/bitcoin/bips/blob/master/bip-0349.md +[BIP-443]: https://github.com/bitcoin/bips/blob/master/bip-0443.mediawiki +[BIP-446]: https://github.com/instagibbs/bips/blob/bip_op_templatehash/bip-0446.md +[BIN-2024-0001]: https://github.com/bitcoin-inquisition/binana/blob/master/2024/BIN-2024-0001.md +[BIN-2024-0003]: https://github.com/bitcoin-inquisition/binana/blob/master/2024/BIN-2024-0003.md +[BIN-2024-0004]: https://github.com/bitcoin-inquisition/binana/blob/master/2024/BIN-2024-0004.md + +[//]: # (Internal links) +[optimal]: #cost-comparison-of-ln-symmetry-constructions +