diff --git a/README.mediawiki b/README.mediawiki index ed4e1dd24a..f8ce4228fe 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -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 @@ -1171,6 +1185,13 @@ 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 +|- | [[bip-0347.mediawiki|347]] | Consensus (soft fork) | OP_CAT in Tapscript @@ -1360,6 +1381,13 @@ 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-0443.mediawiki|443]] | Consensus (soft fork) | OP_CHECKCONTRACTVERIFY diff --git a/bip-0054.md b/bip-0054.md index d278f58a7e..c472aba534 100644 --- a/bip-0054.md +++ b/bip-0054.md @@ -150,12 +150,38 @@ that include 64-byte transactions. The coinbase transaction is usually crafted by mining pool software. To the best of the authors' knowledge, there does not exist an open source reference broadly in use today for such software. We encourage mining pools to update their software to craft coinbase transactions that are -forward-compatible with the changes proposed in this BIP. +forward-compatible with the changes proposed in this BIP. This can be done by using the new +`getblocktemplate` fields described below, once node software supports it. + +## getblocktemplate changes + +The template Object of the `getblocktemplate` JSON-RPC call ([bip-0022][BIP22]) is extended with +the following keys: + +| Key | Required | Type | Description | +|-----|----------|------|-------------| +| `coinbase_locktime` | Yes | Number | coinbase `nLockTime` value | +| `coinbase_sequence` | Yes | Number | coinbase `nSequence` value | +| `coinbase_version` | Yes | Number | coinbase `nVersion` value | + +Types are JSON types as defined in [bip-0022][BIP22]. + +The `coinbase_locktime` field specifies the exact value that MUST be used for the coinbase +transaction's `nLockTime` field. + +The `coinbase_sequence` field specifies a value that SHOULD be used for the coinbase transaction +input's `nSequence` field. If a different value is used, it MUST NOT be `0xffffffff`[^12]. + +The `coinbase_version` field specifies the value that SHOULD be used for the coinbase transaction's +`nVersion` field[^13]. + ## Reference implementation An implementation of BIP54 for Bitcoin Core is available [here][inquisition-implem]. +The `getblocktemplate` extension is implemented in [bitcoin/bitcoin#34419][GBT implem]. + ## Test vectors Documented test vectors are available [here](./bip-0054/test_vectors/) for all mitigations @@ -218,6 +244,23 @@ bip-0034 height commitment and the corresponding future block height. coinbase transactions as not having duplicate past Consensus Cleanup activation would be consistent for any implementation which enforces `nLockTime` from the genesis block, which is the behaviour notably of Bitcoin Core but also of all other implementations the authors are aware of. +[^12]: **Why SHOULD for `coinbase_sequence`?** + The only consensus constraint on `nSequence` is to disallow `0xffffffff`. + The server could communicate this via a bit mask, but for simplicity it + provides the entire `nSequence` value. Clients SHOULD use this value, so + that future soft forks can safely add additional constraints. +[^13]: **Why is `coinbase_version` included?** + This BIP does not constrain the coinbase transaction's `nVersion`, but + including it means `getblocktemplate` now covers all coinbase transaction + fields that could potentially be constrained by a future soft fork. The + coinbase input's prevout txid (32 zero bytes) and vout index (`0xffffffff`) + are fixed by consensus, so they can be safely hardcoded in mining software. + At the time of writing, there is no consensus constraint on transaction + versions. Transaction version 2 is the latest version with defined + semantics, as specified in [bip-0068][BIP68], but its relative lock-time + rules do not apply to the coinbase input. Nonetheless, clients SHOULD use + this value so that they don't need to be updated if a future soft fork + constrains `nVersion`. [BIP30]: https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki [BIP-XXXX]: https://github.com/TheBlueMatt/bips/blob/7f9670b643b7c943a0cc6d2197d3eabe661050c2/bip-XXXX.mediawiki @@ -242,3 +285,6 @@ notably of Bitcoin Core but also of all other implementations the authors are aw [Core 29.0]: https://bitcoincore.org/en/releases/29.0 [inquisition-implem]: https://github.com/darosior/bitcoin/tree/2509_inquisition_consensus_cleanup [Core 30.0]: https://bitcoincore.org/en/releases/30.0 +[BIP22]: https://github.com/bitcoin/bips/blob/master/bip-0022.mediawiki +[BIP68]: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki +[GBT implem]: https://github.com/bitcoin/bitcoin/pull/34419 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..cbe8d06330 --- /dev/null +++ b/bip-0110.mediawiki @@ -0,0 +1,332 @@ +
+  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 witness stack elements 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. + +==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. + +'''Why is there an exception for BIP16 redeemScripts?''' + +The content of redeemScripts are another script, which is then executed. +Its contents are then also subject to the same OP_PUSHDATA* restrictions. +Restricting it 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. The state machine remains in ACTIVE, but consensus rule enforcement is conditional: rules are only enforced for blocks with height < activation_height + active_duration. After 52416 blocks, 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 + +The FAILED state is never reached because timeout is disabled. ACTIVE 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. After active_duration blocks, the deployment remains in the ACTIVE state but rules are no longer enforced. There is no separate EXPIRED state in the state machine. + +==Credits== + +Original draft and advice: Luke-Jr 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-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/secp256k1.py b/bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py old mode 100755 new mode 100644 similarity index 55% rename from bip-0374/secp256k1.py rename to bip-0374/secp256k1lab/src/secp256k1lab/secp256k1.py index b83d028f92..6e262bf51e --- a/bip-0374/secp256k1.py +++ b/bip-0374/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-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-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/