diff --git a/README.mediawiki b/README.mediawiki
index 140eed3076..cf438ba462 100644
--- a/README.mediawiki
+++ b/README.mediawiki
@@ -1513,6 +1513,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority
| bubb1es, haris
| Specification
| Draft
+|-
+| [[bip-0460.mediawiki|460]]
+| Consensus (soft fork)
+| CISA for Taproot Key Path Spends in SegWit v2
+| Fabian Jahr
+| Specification
+| Draft
|}
diff --git a/bip-0460.mediawiki b/bip-0460.mediawiki
new file mode 100644
index 0000000000..5ddf40c4a2
--- /dev/null
+++ b/bip-0460.mediawiki
@@ -0,0 +1,541 @@
+
+ BIP: 460
+ Layer: Consensus (soft fork)
+ Title: CISA for Taproot Key Path Spends in SegWit v2
+ Authors: Fabian Jahr
+ Status: Draft
+ Type: Specification
+ Assigned: 2026-07-28
+ License: CC0-1.0 OR MIT
+ Discussion: 2026-07-18: https://groups.google.com/g/bitcoindev/c/1XH6sBLWZuA
+ Requires: 340, 341, 458, 459
+
+
+== Introduction ==
+
+=== Abstract ===
+
+This document proposes cross-input signature aggregation (CISA) for Bitcoin transactions.
+A new witness version (v2) enables Taproot-style key path spending with two Schnorr signature aggregation schemes: half-aggregation and full-aggregation.
+Half-aggregation non-interactively reduces ''n'' signatures from ''64*n'' bytes to ''(n+1)*32'' bytes, while full-aggregation reduces them to a constant 64 bytes but requires interactive signing.
+Each input's witness starts with a marker byte indicating its aggregation mode (half-agg, full-agg, or opt-out), optionally followed by a sighash type.
+The opt-out mode exists for protocols that depend on a specific signature appearing on chain, such as some adaptor signature protocols.
+
+=== Copyright ===
+
+This document, along with the [[bip-0460/test-vectors.py|test vector generator]] and test vectors, is licensed under CC0-1.0 or the MIT License, at your option.
+The vendored copy of the secp256k1lab library in [[bip-0460/secp256k1lab/|bip-0460/secp256k1lab]] is licensed under the MIT License only, as stated in its [[bip-0460/secp256k1lab/COPYING|COPYING]] file.
+
+=== Motivation ===
+
+Signature data makes up a large share of Bitcoin transaction sizes.
+Each Taproot key path spend requires a 64-byte [[bip-0340.mediawiki|BIP 340]] Schnorr signature, so transactions with many inputs spend much of their weight on signatures.
+
+The linearity of Schnorr signatures allows combining multiple signatures into a more compact form.
+Cross-input signature aggregation applies this to the signatures of different inputs within one transaction.
+
+This provides:
+
+* '''Lower fees and smaller transactions''': Signature data shrinks with the number of aggregated inputs, reducing transaction weight and fees and freeing up block space
+* '''Reduced cost of privacy''': Aggregation reduces the signature cost of any batched transaction, and collaborative transactions such as CoinJoin and PayJoin benefit the most per participant because they batch the inputs of many users
+* '''Flexible usage''': Two aggregation schemes serve different use cases and individual inputs can opt out
+* '''Protocol compatibility''': The explicit opt-out protects protocols that depend on a specific signature appearing on chain
+
+Use cases include:
+
+* '''CoinJoin/PayJoin transactions''': With CISA, the per-participant cost of CoinJoin and PayJoin transactions decreases as the number of participants increases, making privacy-enhancing usage cheaper
+* '''Consolidation and batching''': Businesses and users combining many small UTXOs into a single output or multiple payment outputs benefit from reduced transaction costs
+* '''Lightning channel opens and splices''': Dual funding and splicing combine inputs from both peers in a collaborative transaction, and the existing interactive negotiation between the peers can accommodate the signing rounds of full-aggregation
+
+Aside from these use cases, any transaction spending more than one witness v2 input benefits from CISA through fee savings.
+
+== Design ==
+
+This BIP introduces a new witness version (v2) that enables signature aggregation for Taproot-style key path spends.
+The design reuses the existing Taproot and tapscript machinery wherever possible and makes aggregation opt-in on a per-input basis, so witness v2 outputs can be adopted independently of whether aggregation is used.
+Both aggregation schemes are supported, inputs can opt out explicitly, and aggregation is limited to key path spends.
+The reasoning behind these decisions is covered in the [[#rationale|Rationale]] section.
+
+The main design decision is that every witness v2 key path input carries a marker byte plus an optional sighash byte.
+For aggregated inputs, the last input in each aggregation group includes the aggregate signature concatenated with the marker.
+This design:
+
+* Shows which inputs participate in aggregation
+* Lets each input specify its own sighash type
+* Allows mixing aggregated, opted-out, and non-aggregated inputs in the same transaction
+* Commits to the aggregation mode in the signature message, protecting opted-out inputs from non-consensual aggregation by third parties (even potential future block-wide aggregation)
+* Supports half-aggregation and full-aggregation within the same transaction (as separate groups, one per scheme)
+
+== Specification ==
+
+=== Witness version 2 ===
+
+Witness version 2 outputs are created with a scriptPubKey of OP_2 <32-byte witness program> (or 0x5220{32-byte witness program}).
+
+Validation of witness v2 spends is identical to the witness version 1 rules of [[bip-0341.mediawiki|BIP 341]], with the following modifications for key path spends:
+
+* The single witness stack element remaining after removal of the optional annex is not interpreted as a signature. Its interpretation follows the marker rules defined below.
+* The key path signature message and signature verification are replaced by the rules defined below.
+
+All other rules apply unchanged.
+In particular script path spending and its upgrade mechanisms, annex handling, and the treatment of witness programs that are not exactly 32 bytes long or are wrapped in P2SH follow BIP 341.
+Signatures inside script path spends are not aggregated under this BIP.
+
+=== Marker definitions ===
+
+This BIP defines three marker bytes for witness v2 key path spends:
+
+* 0xbb: '''Opt-out marker'''. The input does not participate in aggregation and uses a standard BIP 340 signature.
+* 0xbc: '''Half-aggregation marker'''. The input participates in half-aggregation.
+* 0xbd: '''Full-aggregation marker'''. The input participates in full-aggregation.
+
+The marker byte is not an opcode, since a key path witness element is not script.
+The only structural constraint is that markers must be disjoint from the annex prefix 0x50.
+The chosen values fall into the range 0xbb to 0xfe, which is unassigned in legacy script and corresponds to OP_SUCCESS opcodes in tapscript, so they do not collide with opcodes commonly seen in scripts.
+
+A key path spend whose witness element is empty or starts with any other byte value is invalid.
+Undefined marker values cannot serve as an upgrade hook because witness data is not committed to by the output.
+A marker value that validated unconditionally would make every witness v2 output spendable by anyone.
+Future aggregation modes are therefore expected to be introduced through new witness versions or new leaf versions instead of new marker values.
+
+=== Witness structure ===
+
+Aggregation changes only the content of the witness.
+The transaction format is unchanged.
+A transaction may contain at most two aggregation groups: one half-aggregation group and one full-aggregation group.
+Aggregated and non-aggregated inputs can be mixed in any order, with the constraint that all markers for a group must appear before that group's aggregate signature.
+The input carrying a group's aggregate signature is called the ''final input'' of the group.
+Only witness v2 key path spends can participate in aggregation groups.
+Inputs spending other output types, as well as witness v2 script path spends, are validated independently under their own rules and can be combined freely with aggregated inputs in the same transaction.
+
+Each witness v2 key path spend uses a single witness element (plus optional annex) that concatenates the marker, an optional sighash byte, and an optional signature:
+
+* '''Marker''': First byte (0xbb, 0xbc, or 0xbd)
+* '''Sighash type''': Optional second byte. If absent, SIGHASH_DEFAULT (0x00) is used'''Why does the sighash byte follow the marker instead of the signature?''' Traditionally the sighash byte is appended to the signature, as in the 65-byte signatures of BIP 341. Here aggregation placeholders carry no signature, so their sighash byte can only follow the marker. Appending it to the signature in the other forms would put the sighash byte in two different positions depending on the form, while the chosen order keeps it in the same position in every form.
+* '''Signature''': For opted-out inputs or the final input of an aggregation group
+
+The marker and the length of the witness element together determine its interpretation:
+
+{| class="wikitable"
+|+ Valid witness element forms by marker and length
+|-
+! Marker !! Length !! Content after the marker !! Interpretation
+|-
+| 0xbb || 65 bytes || 64-byte signature || Opted-out, SIGHASH_DEFAULT
+|-
+| 0xbb || 66 bytes || sighash and 64-byte signature || Opted-out, explicit sighash
+|-
+| 0xbc or 0xbd || 1 byte || none || Aggregation placeholder, SIGHASH_DEFAULT
+|-
+| 0xbc or 0xbd || 2 bytes || sighash || Aggregation placeholder, explicit sighash
+|-
+| 0xbc || ''(n+1)*32+1'' bytes || half-agg signature || Half-aggregation final, SIGHASH_DEFAULT
+|-
+| 0xbc || ''(n+1)*32+2'' bytes || sighash and half-agg signature || Half-aggregation final, explicit sighash
+|-
+| 0xbd || 65 bytes || 64-byte signature || Full-aggregation final, SIGHASH_DEFAULT
+|-
+| 0xbd || 66 bytes || sighash and 64-byte signature || Full-aggregation final, explicit sighash
+|}
+
+Here ''n'' is the number of inputs in the half-aggregation group.
+Since ''n'' is not yet known during parsing, a 0xbc element of 65 bytes or more matches these rows if its length modulo 32 is 1 or 2, and the exact length is checked during group validation.
+A witness element whose marker and length combination matches no row of this table is invalid.
+In particular a 1-byte or 2-byte element with the opt-out marker is invalid, since an opted-out input always carries a signature.
+
+In all 2-byte-prefixed forms the explicit sighash byte must not be 0x00.
+SIGHASH_DEFAULT can only be expressed by omitting the sighash byte.
+This mirrors the rule for 65-byte signatures in BIP 341 and ensures that every signature message has exactly one witness encoding, preventing third parties from malleating the wtxid by switching between equivalent encodings.
+
+=== Common signature message ===
+
+The signature message commits to the input's aggregation mode in addition to the transaction data.
+Let ''SigMsg(hash_type, ext_flag)'' be the common signature message function defined in BIP 341, including all of its failure conditions.
+For a witness v2 key path spend with marker byte ''agg_mode'' and sighash type ''hash_type'', the message is computed as:
+
+
+m = hash_TapSighash(0x01 || agg_mode || SigMsg(hash_type, 0))
+
+
+The first byte is the sighash epoch.
+BIP 341 messages use epoch 0x00 and reserve other values for signature algorithms that make invasive changes to how hashing is performed, such as this one.
+The ''agg_mode'' byte is the marker byte of the input (0xbb, 0xbc, or 0xbd).
+
+Committing to the aggregation mode makes aggregation consensual.
+A signature created for an opted-out input is not valid as a member of an aggregation group and vice versa.
+Without this commitment any third party could fold an opted-out signature into the half-aggregation group unchanged, because half-aggregation of existing signatures is a public operation.
+See [[#why-explicit-opt-out|Why explicit opt-out]] for details.
+
+Valid sighash types are:
+
+* 0x00: SIGHASH_DEFAULT (only expressible by omitting the sighash byte)
+* 0x01: SIGHASH_ALL
+* 0x02: SIGHASH_NONE
+* 0x03: SIGHASH_SINGLE
+* 0x81: SIGHASH_ALL | SIGHASH_ANYONECANPAY
+* 0x82: SIGHASH_NONE | SIGHASH_ANYONECANPAY
+* 0x83: SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
+
+Note: SIGHASH_ANYONECANPAY composes with aggregation only partially, see [[#usage-of-anyonecanpay|Usage of ANYONECANPAY]].
+
+=== Opted-out key path spends ===
+
+An opted-out witness v2 key path spend has a witness stack containing:
+
+* '''Without annex''': [0xbb || signature] (65 bytes) or [0xbb || sighash || signature] (66 bytes)
+* '''With annex''': [0xbb || signature] [0x50...] or [0xbb || sighash || signature] [0x50...]
+
+Validation proceeds using standard BIP 340 signature verification against the output key.
+
+=== Aggregated key path spends ===
+
+Every aggregated input's witness element contains at least the marker byte.
+The last aggregated input also contains the aggregate signature concatenated with the marker.
+
+'''All aggregated inputs except the last:'''
+
+* '''Without annex''': [marker] (1 byte) or [marker || sighash] (2 bytes)
+* '''With annex''': [marker] [0x50...] or [marker || sighash] [0x50...]
+
+Where marker is 0xbc (half-agg) or 0xbd (full-agg).
+
+'''Last aggregated input:'''
+
+* '''Without annex''': [marker || signature] or [marker || sighash || signature]
+* '''With annex''': [marker || signature] [0x50...] or [marker || sighash || signature] [0x50...]
+
+An aggregation group may consist of a single input.
+In that case the group's only input carries the marker and the aggregate signature, which is 64 bytes for both schemes.
+
+=== Validation rules ===
+
+Witness v2 key path spends are validated in three passes:
+
+'''Pass 1: Parse and collect aggregation information'''
+
+For each witness v2 key path spend:
+
+1. '''Extract annex if present''':
+* If the witness stack has at least two elements, and the first byte of the last element is 0x50, this last element is the annex and is removed from the witness stack
+
+2. '''Parse the witness element''':
+* First byte determines marker type. Fail if it is not 0xbb, 0xbc, or 0xbd
+* Marker and length together determine the structure per the table above. Fail if the combination matches no defined structure
+* Extract ''hash_type'' and signature if present
+* Verify ''hash_type'' is valid (0x01-0x03, 0x81-0x83 in explicit form). Fail on an explicit 0x00
+
+3. '''Classify input''':
+* '''0xbb''': Opted-out input, always carries a signature
+* '''0xbc/0xbd without signature''': Aggregation placeholder
+* '''0xbc/0xbd with signature''': Aggregation final
+
+The structure table is exhaustive, so every witness element that parses successfully falls into exactly one class.
+
+'''Pass 2: Validate aggregation group structure'''
+
+1. '''Group formation rules''':
+* At most one half-aggregation group (marker 0xbc)
+* At most one full-aggregation group (marker 0xbd)
+* Each group must have exactly one final input (with signature), fail otherwise
+* Fail if a group's marker reappears after that group's final input
+* Opted-out inputs (marker 0xbb) do not form groups
+
+2. '''For each aggregation group''':
+* Count ''n'' = number of inputs in the group
+* Verify aggregate signature size, failing on a mismatch:
+** Half-agg: must equal ''(n+1)*32'' bytes
+** Full-agg: must equal 64 bytes
+
+'''Pass 3: Verify signatures'''
+
+1. '''For each opted-out input''':
+* Compute the message ''m = hash_TapSighash(0x01 || 0xbb || SigMsg(hash_type, 0))''
+* Verify the 64-byte signature using BIP 340 against the output key (the 32-byte witness program of the spent output)
+
+2. '''For each aggregation group''':
+* Collect the group's inputs, consisting of all inputs with matching markers that appeared before the final input plus the final input itself
+* Order the group's (pubkey, message) pairs by input index, where the pubkey of each pair is the 32-byte witness program of the spent output
+* For each aggregated input ''i'': compute ''mi = hash_TapSighash(0x01 || marker || SigMsg(hash_typei, 0))''
+* For half-agg: verify with ''VerifyAggregate(aggsig, pm_aggd)'' as defined in [[bip-0458.mediawiki|BIP 458]], where ''pm_aggd'' is the ordered list of pairs
+* For full-agg: verify with ''Verify'' as defined in [[bip-0459.mediawiki|BIP 459]] over the ordered pubkey and message lists
+
+=== Signing ===
+
+Signers compute the common signature message defined above with the marker byte of their input as ''agg_mode''.
+The secret key is the Taproot tweaked private key corresponding to the 32-byte witness program, derived as in BIP 341.
+
+'''Opted-out inputs''':
+The signer creates an ordinary BIP 340 signature over the message and places it in the witness element after the marker.
+
+'''Half-aggregated inputs''':
+Each signer creates an ordinary BIP 340 signature over its message in a single stateless pass.
+These signatures do not appear on chain individually.
+Once all signatures of the group are available, any party can combine them with ''Aggregate'' or ''IncAggregate'' as defined in [[bip-0458.mediawiki|BIP 458]], over the group's pairs ordered by input index, and place the result in the final input's witness element.
+The aggregating party should verify the individual signatures first.
+This identifies a disruptive signer and is necessary because ''Aggregate'' does not validate its inputs, see BIP 458.
+
+'''Full-aggregated inputs''':
+The group's signers run the interactive signing protocol of [[bip-0459.mediawiki|BIP 459]].
+The public key list of the session is the ordered list of the group's 32-byte witness programs and the message list contains the corresponding signature messages in the same order.
+Each signer uses its tweaked secret key, see the [[bip-0459.mediawiki#key-tweaking|Key Tweaking]] section of BIP 459.
+The 64-byte signature returned by ''SigAgg'' is placed in the final input's witness element.
+Any change that affects a group member's signature message or the group's ordered member list invalidates the session and requires a new one.
+Secret nonces must never be reused across signing sessions, see BIP 459.
+
+== Examples ==
+
+The final input of each aggregation group carries the aggregate signature and therefore more witness weight than the other group members.
+How this weight difference is accounted for between the participants of a collaborative transaction is a protocol level concern outside the scope of this BIP.
+
+'''Example 1: Three inputs, all half-aggregated with SIGHASH_DEFAULT'''
+
+Input 0 witness: [0xbc] # 1 byte: half-agg placeholder
+Input 1 witness: [0xbc] # 1 byte: half-agg placeholder
+Input 2 witness: [0xbc || 128-byte sig] # 129 bytes: half-agg final (n=3)
+
+Total overhead: 3 marker bytes
+Signature size reduction: 192 bytes → 128 bytes
+
+
+'''Example 2: Three inputs full-aggregated with SIGHASH_ALL'''
+
+Input 0 witness: [0xbd || 0x01] # 2 bytes: full-agg placeholder, SIGHASH_ALL
+Input 1 witness: [0xbd || 0x01] # 2 bytes: full-agg placeholder, SIGHASH_ALL
+Input 2 witness: [0xbd || 0x01 || 64-byte sig] # 66 bytes: full-agg final (n=3), SIGHASH_ALL
+
+Total overhead: 3 marker bytes
+Signature size reduction: 192 bytes → 64 bytes
+
+
+'''Example 3: Witness v1 input alongside a witness v2 aggregation group'''
+
+Input 0 witness: [64-byte sig] # witness v1 (Taproot) key path spend
+Input 1 witness: [0xbc] # 1 byte: half-agg placeholder
+Input 2 witness: [0xbc || 96-byte sig] # 97 bytes: half-agg final (n=2)
+
+Input 0 spends a Taproot output and does not participate in aggregation.
+Inputs 1 and 2 form the half-aggregation group.
+
+Total overhead: 2 marker bytes
+Signature size reduction (v2 inputs): 128 bytes → 96 bytes
+
+
+'''Example 4: Mixed transaction'''
+
+Input 0 witness: [0xbb || 64-byte sig] # 65 bytes: opted-out, SIGHASH_DEFAULT
+Input 1 witness: [0xbc || 0x03] # 2 bytes: half-agg, SIGHASH_SINGLE
+Input 2 witness: [0xbd] # 1 byte: full-agg placeholder
+Input 3 witness: [0xbd] # 1 byte: full-agg placeholder
+Input 4 witness: [0xbc || 96-byte sig] # 97 bytes: half-agg final (n=2)
+Input 5 witness: [0xbb || 0x82 || 64-byte sig] # 66 bytes: opted-out, SIGHASH_NONE|SIGHASH_ANYONECANPAY
+Input 6 witness: [0xbd || 64-byte sig] # 65 bytes: full-agg final (n=3)
+
+This transaction has:
+- Two opted-out inputs (0, 5) with different sighash types
+- One half-aggregation group (inputs 1, 4)
+- One full-aggregation group (inputs 2, 3, 6)
+
+Total overhead: 7 marker bytes
+Signature size reduction: 448 bytes → 288 bytes
+
+
+== Security ==
+
+'''Non-consensual aggregation''':
+The signature message commits to the aggregation mode, so third parties cannot move signatures between opted-out witnesses and aggregation groups, see [[#why-explicit-opt-out|Why explicit opt-out]].
+Signatures also cannot be replayed between witness versions because the signature message always commits to the scriptPubKey of the output being spent.
+
+'''Adaptor signature incompatibility''':
+Aggregation is incompatible with protocols that extract a secret from a specific signature appearing on chain, most notably some adaptor signature protocols.
+Adaptor protocols in which the final signature is delivered directly between the parties are not affected.
+Protocols that rely on on-chain extraction should use opted-out signatures for those inputs.
+
+'''Shared security of the three modes''':
+Every witness v2 output can be spent through any of the three key path modes, chosen by whoever produces the witness.
+The security of v2 outputs therefore rests on all three verification algorithms together, and a soundness failure in BIP 340, BIP 458, or BIP 459 would affect all v2 outputs regardless of the mode their owners intended to use.
+Both aggregation schemes come with security proofs that reduce to established assumptions in the same discrete logarithm setting as BIP 340, see the security discussions in BIP 458 and BIP 459.
+
+'''Witness malleability''':
+For key path spends every witness element size is fixed by consensus and the annex is committed by the signature message, so third parties cannot change the weight of a fully signed input.
+Given the nonce part of an aggregate signature, only one value of ''s'' satisfies the verification equation, and replacing the nonce part requires knowledge of the individual signatures.
+Third parties therefore cannot malleate witness v2 key path witness data.
+Opted-out inputs carry plain BIP 340 signatures, which are non-malleable.
+
+'''Transaction level verification''':
+Aggregated inputs add one transaction level verification step per aggregation group, while parsing, structure checks, opted-out inputs, and script path spends remain per-input operations.
+Implementations that parallelize script validation per input can schedule the group verification as one additional check.
+Existing wtxid based validation caches remain correct since any witness change alters the wtxid.
+Aggregate verification can also join verification batches together with individual BIP 340 signatures, so aggregation does not preclude batch validation.
+
+'''Computation limits''':
+Aggregation does not increase the worst-case signature verification cost of a block.
+Full-aggregation requires about half the elliptic curve multiplications per input of individual BIP 340 verification, while half-aggregation requires about the same number in a single batchable equation.
+The densest packing of signature verification work per weight unit remains tapscript under the BIP 342 validation weight budget, which this BIP does not change.A minimal Taproot key path input weighs 230 weight units, 164 for the input base data plus 66 for the witness element, and BIP 340 verification costs two elliptic curve multiplications per signature.
+Per member of a large group, full-aggregation accounts for 167 weight units and half-aggregation for 199 weight units including the member's 32-byte share of the aggregate signature, ignoring small per-group constants.
+The BIP 459 verification equation costs ''n+1'' multiplications for a group of ''n'' inputs, while the BIP 458 equation costs about ''2n'' since the randomizers multiply both the nonce point and the challenge term of each member.
+Ignoring transaction overhead, a block filled with minimal full-aggregation members needs about 4,000,000 / 167 ≈ 24,000 multiplications and one filled with minimal half-aggregation members about 2 * 4,000,000 / 199 ≈ 40,200, compared to 2 * 4,000,000 / 230 ≈ 34,800 for minimal Taproot key path spends.
+The half-aggregation count is about 16 percent higher, but its per-input cost matches batch verified BIP 340 signatures and the whole group is verified as one multi-scalar multiplication, which is faster in practice than the same number of independent verifications.
+All of these remain far below the BIP 342 validation weight budget, which admits one tapscript signature verification per 50 weight units, or 80,000 verifications with about 160,000 multiplications per block.
+Aggregation also does not increase the maximum number of inputs per block, since an input spending an output that requires no signature, such as a bare OP_TRUE script, weighs only 164 weight units today.
+
+'''Mode fingerprinting''':
+The marker bytes are visible on chain, so distinctive mode combinations can fingerprint the wallets or protocols that created a transaction.
+Participants of collaborative transactions should prefer one mode for all participating inputs if they want to avoid this.
+
+== Test vectors ==
+
+Wallet operation vectors covering scriptPubKey and address computation, signature message computation, and witness construction for both key path and script path spending can be found in [[bip-0460/wallet-test-vectors.json|wallet-test-vectors.json]].
+
+Transaction level validity cases, including marker parsing failures, group structure failures, annex handling, script path spending, and the aggregation mode commitment, can be found in [[bip-0460/consensus-test-vectors.json|consensus-test-vectors.json]].
+
+Both files are generated by [[bip-0460/test-vectors.py|test-vectors.py]], which builds on the reference code of BIP 458 and BIP 459.
+All vectors use deterministic test-only keys and nonces, and BIP 340 signatures are created with an all-zero auxiliary randomness array.
+
+== Reference implementation ==
+
+A work in progress implementation for Bitcoin Core is available at https://github.com/fjahr/bitcoin/tree/2026-07-cisa-txwide.
+It implements the validation rules of this BIP and validates all consensus test vectors.
+
+== Rationale ==
+
+=== Why a new witness version? ===
+
+A new witness version is necessary because BIP 341 completely specifies the validation rules for witness v1, fixing key path witnesses to 64-byte or 65-byte signatures.
+The 1-byte and 2-byte marker elements used by this BIP are invalid under those rules, and a soft fork can only turn valid transactions into invalid ones, not the reverse.
+Aggregation therefore cannot be retrofitted onto existing Taproot outputs.
+
+=== Why the marker-based design? ===
+
+The primary alternative to markers is a design without them, where aggregated inputs carry only a 1-byte sighash placeholder and aggregation applies to all witness v2 inputs of a transaction or to none of them.
+Compared to that alternative the marker costs at most one additional byte per input.
+In exchange the marker provides three properties:
+
+'''Per-input consent''':
+An all-or-nothing design couples the choices of unrelated parties.
+In a CoinJoin a single participant that requires a plain on-chain signature, for example for an adaptor signature based protocol, would force every other participant to forgo aggregation.
+With markers this choice is local to the input, which matters most in exactly the multi-party transactions that CISA is designed to make cheaper.
+
+'''Self-describing witnesses''':
+Every input is classified locally by the first byte of its witness element instead of inferring a transaction-wide mode from the shape of all witnesses.
+This avoids inference edge cases.
+For example, in the placeholder design the final aggregated input's witness consists of a sighash byte followed by the aggregate signature as a separate element, and an aggregate signature whose first byte happens to be 0x50 is misparsed as an annex under the BIP 341 annex rule.
+Roughly one in 256 honestly generated aggregate signatures would hit this case and force the signers to start over.
+The defined marker values are disjoint from the annex prefix by construction.
+
+'''Two coexisting groups''':
+A transaction can contain one half-aggregation group and one full-aggregation group at the same time.
+This allows a tightly coordinated subset of signers, for example the inputs of a single wallet, to use full-aggregation while the remaining inputs of a collaborative transaction use half-aggregation.
+
+A more flexible alternative with multiple aggregation groups per scheme, similar to the bucket concept from early aggregation discussions, was rejected.
+Multiple groups would only benefit full-aggregation, since half-aggregation groups can be merged by anyone after signing.
+They would allow several smaller signing sessions instead of one session spanning all aggregated inputs, for example through a group number encoded in the marker byte.
+However, with SIGHASH_DEFAULT the signers of every group still have to agree on the complete output list of the transaction before signing, making coordination across groups necessary.
+Decoupling the groups through SIGHASH_SINGLE with SIGHASH_ANYONECANPAY is limited to groups of signers with a single input and output each, reveals which input pays which output, and lets anyone split the merged transaction apart again.
+Half-aggregation already provides this flexibility without any joint signing session at a cost of 32 weight units per input, and a separate transaction per group provides it at the cost of repeating the transaction overhead.
+Group numbers would also reveal which inputs belong together, which has negative privacy implications.
+
+=== Why explicit opt-out? ===
+
+Half-aggregation of existing signatures is a public operation.
+Any third party in possession of valid BIP 340 signatures, such as a miner or a relaying node, can aggregate them without the signers' involvement.
+Some protocols depend on a specific signature appearing on chain.
+Most notably, some adaptor signature protocols extract a secret from the difference between an on-chain signature and a previously shared adaptor signature, and this extraction becomes impossible once the signature has been folded into an aggregate.
+
+The marker byte alone cannot protect such inputs, since a third party could replace the opt-out marker with an aggregation marker and fold the unchanged signature into the group.
+The protection comes from the signature message committing to the aggregation mode.
+A signature over the opt-out flavored message only verifies in an opted-out witness and is useless as an aggregation group member.
+Aggregation is therefore consensual for every input.
+The explicit marker byte itself serves parsing rather than protection.
+Without it a 65-byte witness element would be ambiguous, it could be an opted-out signature with a sighash byte or a full-aggregation final signature without one.
+
+A consequence of the mode commitment is that the aggregation mode is chosen at signing time.
+A signature created for aggregation cannot later be used as a standalone opted-out signature either, so signers that want to keep both options open must produce two signatures.
+Planning for aggregation carries no size penalty if the group ends up with a single member, since a single-member group signature is 64 bytes, the same size as an opted-out signature.
+
+The explicit opt-out also leaves the door open for a potential future block-wide half-aggregation to cover all signatures that did not opt out.
+
+=== Why two aggregation schemes? ===
+
+Half-aggregation and full-aggregation serve different use cases:
+
+{| class="wikitable"
+|+ Comparison of aggregation schemes
+|-
+! Property !! Half-Aggregation !! Full-Aggregation
+|-
+| '''Signature size''' || ''(n+1)*32'' bytes || 64 bytes (constant)
+|-
+| '''Interactivity''' || Non-interactive || Interactive (2 rounds)
+|-
+| '''Use case''' || All transaction types || Interactive protocols such as CoinJoins, wallet consolidation
+|-
+| '''Incremental''' || Yes || No
+|}
+
+Full-aggregation offers roughly twice the savings of half-aggregation but requires all signers to run a stateful two-round protocol within one signing session.
+Mishandling the secret nonce state between the rounds can lead to key extraction, which makes supporting it on hardware signers and air-gapped devices demanding.
+The slow adoption of MuSig2 by signing devices illustrates this cost.
+
+Half-aggregation signers produce plain BIP 340 signatures in a single stateless pass, and the aggregation happens downstream of all signing.
+This fits existing asynchronous multi-party workflows, for example PSBT based signing ceremonies where cosigners are available at different times, or [[bip-0077.md|BIP 77]] Async Payjoin where sender and receiver are not required to be online at the same time.
+It also avoids giving every participant of a large collaborative transaction a second opportunity to stall the protocol between signing rounds.
+Finally, half-aggregation composes with workflows where the input set is still growing after signing has started, such as ANYONECANPAY based fee bumping, because aggregation can happen once the set is final.
+
+=== Why key path only? ===
+
+Key path spends have one signature per input that is visible from the witness structure alone, which makes their aggregation rules simple.
+The use cases motivating this BIP, collaborative transactions and consolidations, also typically use simple key path spends.
+Aggregating signatures from within scripts, in contrast, conflicts with the tapscript upgrade mechanisms.
+Whether a signature operation contributes to the transaction level aggregate depends on script execution, but a script containing an OP_SUCCESS opcode succeeds without being executed.
+A later soft fork redefining such an opcode would change which signatures the aggregate has to cover, so transactions valid under the new rules would be invalid for older nodes, turning the redefinition into a hard fork.
+This is a variant of the problem described in [https://gnusha.org/pi/bitcoindev/20180321040618.GA4494@erisian.com.au/ the 2018 aggregation discussion by Anthony Towns], whose mitigations rely on execution order, which the parse time OP_SUCCESS semantics of tapscript do not provide.
+Script execution raises further design questions, for example the BIP 342 sigops budget assumes 50 witness weight units per signature check and would be exhausted by short placeholder signatures, and the aggregate signature must be located before execution while group membership only becomes known during it.
+Solving these questions is left for a separate BIP with its own upgrade semantics.
+
+=== Usage of ANYONECANPAY ===
+
+This BIP permits all standard sighash types at the consensus level, including SIGHASH_ANYONECANPAY (0x80 flag).
+Whether to aggregate is an application level decision, not a consensus concern, so the consensus rules stay permissive.
+
+'''Full-aggregation''':
+The signing session of BIP 459 fixes the group's public key and message lists, so the membership of a full-aggregation group cannot grow after signing has started.
+ANYONECANPAY still composes with full-aggregation in one direction.
+Once the group members have signed with ANYONECANPAY, additional opted-out, half-aggregated, or non-v2 inputs can be appended to the transaction without invalidating the group, because the group's messages do not commit to those inputs.
+Only extending the full-aggregation group itself with later inputs is impossible.
+
+'''Half-aggregation''':
+Half-aggregation composes with ANYONECANPAY directly.
+Signers create their signatures over the half-aggregation flavored message at signing time, further inputs are added later, and once the input set is final anyone can aggregate the collected signatures without further involvement of the signers.
+
+=== Future considerations ===
+
+'''Block-wide aggregation''':
+Block-wide aggregation remains a potential future optimization and is not precluded by this design.
+
+'''Script path aggregation''':
+Script path aggregation can be specified in a separate BIP and added in the same or a future soft fork.
+
+== Deployment ==
+
+This BIP is to be deployed by a soft fork activation mechanism.
+The activation parameters are to be determined and will be specified in a future update of this document.
+Since other proposals also target witness version 2, the witness version is subject to coordination and may change in a future update as well.
+
+== Backward compatibility ==
+
+=== Non-upgraded nodes ===
+
+Nodes that have not upgraded to support witness v2 treat witness v2 outputs as anyone-can-spend, per the [[bip-0141.mediawiki|BIP 141]] unknown witness version rule.
+Depending on their policy settings, such nodes might not relay witness v2 transactions or include them in blocks.
+
+=== Non-upgraded wallets ===
+
+Wallets without witness v2 support cannot create or spend witness v2 outputs but can send to witness v2 addresses if they support Bech32m.
+
+== Acknowledgements ==
+
+Thanks to Vojtěch Strnad for feedback on early versions of this BIP, in particular for prompting a precise description of the script path aggregation obstacles and for streamlining the specification.
+
+== Footnotes ==
+
+
diff --git a/bip-0460/consensus-test-vectors.json b/bip-0460/consensus-test-vectors.json
new file mode 100644
index 0000000000..b7b5240a5c
--- /dev/null
+++ b/bip-0460/consensus-test-vectors.json
@@ -0,0 +1,787 @@
+{
+ "testCases": [
+ {
+ "id": "halfagg-valid",
+ "description": "Two-member half-aggregation group, SIGHASH_DEFAULT",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0101bc0161bcea2177dced256cdb2d2aa0edc720461d183c68c351a8cdfbc351ee5d9be562d8e80aa5ce400fe83bb967789df6ffe4dee8001fd1a18c1339b3e12f66e13e187ce0962c139ac022d0f51bc513fee73e6469eca90226a599886c1e3b1cad83b7b700000000",
+ "valid": true
+ },
+ {
+ "id": "fullagg-valid",
+ "description": "Two-member full-aggregation group, SIGHASH_ALL",
+ "prevouts": [
+ {
+ "scriptPubKey": "52208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e4",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522018b6491469cc78b764b01669b0a5a4e892f57e8c1b20b89bc1ca6dce3e881b51",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010254d36a0897340110cc34b526d73b206175ae4f5db4d25fe7bdeef3a8a5e89afd0000000000ffffffff835a02826d69e61b563ec713020bd2b7c3bc3848223cda2b3c52bbdcd3e6267a0000000000ffffffff01c07fdc0b000000002252208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e40102bd010142bd0160ff22c66f7fe9a0911ab6a8a03542510b494e2e1d9de2246e670ce6acd16dc99229ab127ded30ca694b3a846a4e16fbba4246a21a7d21876aac6e18f36ac1c700000000",
+ "valid": true
+ },
+ {
+ "id": "mixed-valid",
+ "description": "Opted-out input alongside a two-member half-aggregation group",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220d0d1709a7b6eef6e5309f9115b163b0a72295b837923a5251641352e5d6ae21f",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52206bcb2a825ab91fa41da5f6cc1b51edf07ce6dc769e497d741c39ee4de7eb0966",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010305a375e47ab8044473e9642b89e8d0e23c39e6dff3ebc3decd3a21e36df5c8f10000000000ffffffffa098dbe94bda678231b9b104eadb0afd61f44a955146c5518b039ffbdc54eee60000000000fffffffff861074487871c360c0a79a8a0520e6efea15cb031a2b249d5342c417a18d4710000000000ffffffff01c060d21100000000225220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce0141bb43d8bb5ec6c46d6b696dde071298a1045863bd187edcdfc2cf15f9f548ae87219a43ffa759856d9d388993ca6d66b71cd6737c0b9cc1ee62188acf5675cddb2c0101bc0161bcb7f385ee1e8cee81cc4f5dd06cfa4471807f48aa61d6791c8d0851459a15cea51587da1c10801778e65bb13619388ec6973f05fbb00c1ab88b1d51e5e60d5dc1ef4662179ab2043b4c919730e9a0cb45a69f6f5ab5738b34a942ef7c76ec4f7900000000",
+ "valid": true
+ },
+ {
+ "id": "marker-undefined",
+ "description": "Witness element starts with the undefined marker byte 0xbe",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220d0d1709a7b6eef6e5309f9115b163b0a72295b837923a5251641352e5d6ae21f",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52206bcb2a825ab91fa41da5f6cc1b51edf07ce6dc769e497d741c39ee4de7eb0966",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010305a375e47ab8044473e9642b89e8d0e23c39e6dff3ebc3decd3a21e36df5c8f10000000000ffffffffa098dbe94bda678231b9b104eadb0afd61f44a955146c5518b039ffbdc54eee60000000000fffffffff861074487871c360c0a79a8a0520e6efea15cb031a2b249d5342c417a18d4710000000000ffffffff01c060d21100000000225220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce0141be43d8bb5ec6c46d6b696dde071298a1045863bd187edcdfc2cf15f9f548ae87219a43ffa759856d9d388993ca6d66b71cd6737c0b9cc1ee62188acf5675cddb2c0101bc0161bcb7f385ee1e8cee81cc4f5dd06cfa4471807f48aa61d6791c8d0851459a15cea51587da1c10801778e65bb13619388ec6973f05fbb00c1ab88b1d51e5e60d5dc1ef4662179ab2043b4c919730e9a0cb45a69f6f5ab5738b34a942ef7c76ec4f7900000000",
+ "valid": false,
+ "failureReason": "undefined marker byte"
+ },
+ {
+ "id": "sighash-explicit-default",
+ "description": "Opted-out input encodes SIGHASH_DEFAULT explicitly as a 0x00 sighash byte",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220d0d1709a7b6eef6e5309f9115b163b0a72295b837923a5251641352e5d6ae21f",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52206bcb2a825ab91fa41da5f6cc1b51edf07ce6dc769e497d741c39ee4de7eb0966",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010305a375e47ab8044473e9642b89e8d0e23c39e6dff3ebc3decd3a21e36df5c8f10000000000ffffffffa098dbe94bda678231b9b104eadb0afd61f44a955146c5518b039ffbdc54eee60000000000fffffffff861074487871c360c0a79a8a0520e6efea15cb031a2b249d5342c417a18d4710000000000ffffffff01c060d21100000000225220735471e81ea57222642f2198afe0c9753bcb9669beae68aa90a49c0dee85f6ce0142bb0043d8bb5ec6c46d6b696dde071298a1045863bd187edcdfc2cf15f9f548ae87219a43ffa759856d9d388993ca6d66b71cd6737c0b9cc1ee62188acf5675cddb2c0101bc0161bcb7f385ee1e8cee81cc4f5dd06cfa4471807f48aa61d6791c8d0851459a15cea51587da1c10801778e65bb13619388ec6973f05fbb00c1ab88b1d51e5e60d5dc1ef4662179ab2043b4c919730e9a0cb45a69f6f5ab5738b34a942ef7c76ec4f7900000000",
+ "valid": false,
+ "failureReason": "explicit 0x00 sighash byte"
+ },
+ {
+ "id": "group-no-final",
+ "description": "Half-aggregation markers without an input carrying the aggregate signature",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0101bc0101bc00000000",
+ "valid": false,
+ "failureReason": "aggregation group has no final input"
+ },
+ {
+ "id": "marker-after-final",
+ "description": "Half-aggregation marker appears after the group's final input",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220f4746153737986ea947e2d197427a62d01f251630de2be1c575124f0d932366b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52200a9deddd6e095811ffe5c061dbee3ba0c57d107d73ae1f5bd04ffaf851495e95",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52202c8ed1bfdfdedf23b4a63a374a5a4b3c7575403f93c7bddb10be5011a2c6a471",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010392ccaa54e5e62f3bd670417a6b572c92cccc3da6bc1cc66df5e503d40112a44f0000000000ffffffff5928ed52c21e87265486ae544e6415b381ee2914638c962c864564c5e0302a770000000000fffffffff0687ec7cc1986f69faeccca2bc20fec6290472ea48f384c8b830c656e4a3d6d0000000000ffffffff01c060d21100000000225220f4746153737986ea947e2d197427a62d01f251630de2be1c575124f0d932366b0101bc0161bc0904e70d45681dd516e58aaf575e78f86c9cff6694a126b8de04aac5c919d16e8676a78d02e381009db0547e06c6c7113fefa4dd1f00128ce38a83e9a3af77a82db7a7bdb47e87a6af6e9358661c0c2e8516f51b12cdfedd63977dfa18f68e4a0101bc00000000",
+ "valid": false,
+ "failureReason": "marker after group final"
+ },
+ {
+ "id": "aggsig-size-mismatch",
+ "description": "Aggregate signature is 128 bytes for a two-member half-aggregation group (expected 96)",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0101bc0181bcea2177dced256cdb2d2aa0edc720461d183c68c351a8cdfbc351ee5d9be562d8e80aa5ce400fe83bb967789df6ffe4dee8001fd1a18c1339b3e12f66e13e187ce0962c139ac022d0f51bc513fee73e6469eca90226a599886c1e3b1cad83b7b7000000000000000000000000000000000000000000000000000000000000000000000000",
+ "valid": false,
+ "failureReason": "aggregate signature size mismatch"
+ },
+ {
+ "id": "cross-mode-fold",
+ "description": "Opt-out flavored signatures folded into a half-aggregation group by a third party",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220ca8344e759bfb6492d1ea6cfaafac886df066bc099ac3455632b2d3680dea261",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220c1253f68ccd5faf5e3dfa399bb499092ecd39384be321aee8ac476b0773569c8",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102c0f4bc942ecfa2798ebbcc06bcbda200152e1d99c434d93c3f3b769e292a7fb10000000000ffffffff109ee50850f7dc5b6b5ece311830798e23b94f633f6afb62043d768fac7d24910000000000ffffffff01c07fdc0b00000000225220ca8344e759bfb6492d1ea6cfaafac886df066bc099ac3455632b2d3680dea2610101bc0161bc208ac07c0c4160b3a33ac4b14c8aba5b6a372d4fc373928d46e44b79d14af8af2b1a519b0b24c69b7ae6c4b3029e7668f9083ae9ba35fd35e2dc47a54847435c88f6c02cf28555825fd53ce921a535f8adf785bed153d8203bddd3143643e53800000000",
+ "valid": false,
+ "failureReason": "aggregate signature invalid, messages commit to opt-out mode"
+ },
+ {
+ "id": "empty-witness",
+ "description": "Witness v2 input with an empty witness",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b000141bb8e3dbf1e013584c323c3e47010e50d82e7dc97caf4a2031e26f1f67304bc98d5f559eeaa31ed50e12bcdc93df50a984fe0ed4778315e8a3f85f8179f3b62d97d00000000",
+ "valid": false,
+ "failureReason": "empty witness"
+ },
+ {
+ "id": "optout-no-sig",
+ "description": "Opt-out marker as a 1-byte witness element without a signature",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0101bb0141bb8e3dbf1e013584c323c3e47010e50d82e7dc97caf4a2031e26f1f67304bc98d5f559eeaa31ed50e12bcdc93df50a984fe0ed4778315e8a3f85f8179f3b62d97d00000000",
+ "valid": false,
+ "failureReason": "opt-out marker without signature"
+ },
+ {
+ "id": "optout-sighash-no-sig",
+ "description": "Opt-out marker with an explicit sighash byte but without a signature",
+ "prevouts": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff01c07fdc0b000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0102bb010141bb8e3dbf1e013584c323c3e47010e50d82e7dc97caf4a2031e26f1f67304bc98d5f559eeaa31ed50e12bcdc93df50a984fe0ed4778315e8a3f85f8179f3b62d97d00000000",
+ "valid": false,
+ "failureReason": "opt-out marker without signature"
+ },
+ {
+ "id": "annex-committed-valid",
+ "description": "Half-aggregation group whose first member carries an annex that is committed in its signature message",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220e3af94f2fb5e23a1967c1f4fad8914fec3505f1c7e735b795a62d0209e9a176c",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220af9693bcd26365ebb9fea91dfbfacf2b96a2e3c06dc2a16f706a15d8ee3a4cc5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001022b72230eed37d5af4f7400a9eb61103cbbf1f9a4dd3b85e7231186ef9fa51fa80000000000ffffffffccadb04d02ec2ae661263fe6801aacf882ed7a01c6ac6aa2cce40f2b9ca16f780000000000ffffffff01c07fdc0b00000000225220e3af94f2fb5e23a1967c1f4fad8914fec3505f1c7e735b795a62d0209e9a176c0201bc10504349534120616e6e657820746573740161bc1d353f924fcde6d6f4a390c831b77ba5b8c54359a1d26c902e7f6d9ec670d6c31cbc8db7b701dd35e61873370a2b0d32d42ffe3a223b448a0d39253dc93576753f6d2faa075a33ae794d564a3f02b9f44ff3fa8fbaf05d4428eac022bc72ee7f00000000",
+ "valid": true
+ },
+ {
+ "id": "annex-stripped",
+ "description": "Annex removed by a third party from the transaction of annex-committed-valid",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220e3af94f2fb5e23a1967c1f4fad8914fec3505f1c7e735b795a62d0209e9a176c",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220af9693bcd26365ebb9fea91dfbfacf2b96a2e3c06dc2a16f706a15d8ee3a4cc5",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001022b72230eed37d5af4f7400a9eb61103cbbf1f9a4dd3b85e7231186ef9fa51fa80000000000ffffffffccadb04d02ec2ae661263fe6801aacf882ed7a01c6ac6aa2cce40f2b9ca16f780000000000ffffffff01c07fdc0b00000000225220e3af94f2fb5e23a1967c1f4fad8914fec3505f1c7e735b795a62d0209e9a176c0101bc0161bc1d353f924fcde6d6f4a390c831b77ba5b8c54359a1d26c902e7f6d9ec670d6c31cbc8db7b701dd35e61873370a2b0d32d42ffe3a223b448a0d39253dc93576753f6d2faa075a33ae794d564a3f02b9f44ff3fa8fbaf05d4428eac022bc72ee7f00000000",
+ "valid": false,
+ "failureReason": "signature message committed to the annex"
+ },
+ {
+ "id": "scriptpath-valid",
+ "description": "Witness v2 script path spend of a single CHECKSIG leaf under BIP 341/342 rules, alongside an opted-out key path input",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220e8605ff15e754192a9d6c053969ae935932c82ceaeea0fed9a152f0c6b4b424b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522044fdf832ec12334a9c3316767eae801d4cc9fa26723267e58107cc4574e3b64d",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001027ce7215887dea171a102c5f9c3b5a9835ee098a1fe51a8e6f6bddedd0b173f400000000000ffffffff992524364c91d1d04d24dbf8ea0444e5d2ac69944c0f84ac48d0756abf1bfa1a0000000000ffffffff01c07fdc0b0000000022522084dba7b2063ea2384cada9c8a3b014659c51c9f79ab4e5a40fee826fd9a167790340d49a402de13f7d71177322ff53b56d0e6210e9aa9d902b4a91db9ab1892bc7ef7363cd73d75c09034dec2aa7e2044bc877b16e81e1ff3aa40282420559340aaf222038c01dda6433ce79606dda3f70ee5dd525104bbbba5b0fd96f56d6d634e92f71ac21c139920c3a54000624c1923187eebbbd3fcdc409302757139c1a1b003bc9276da40141bb7534948a6eb700346e41482f52940e7c342b3daaea3eab1bd4910b1d3f3ce19634ce4f9f2f89b2c457ace7926c3eb1c42f599f6e012daa1f3b507630bbeaa22d00000000",
+ "valid": true
+ },
+ {
+ "id": "cross-mode-fullagg",
+ "description": "Full-aggregation signature created over opt-out flavored messages, placed in a full-aggregation group",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220d8ce2162ebd4a65d21981113193e470a7605970928b5fcfe9636d0a2d4cecc4f",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cd51647dde7739fd367ab5666c6b6b32ce728a456ca59621df488418abba93c6",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010277580c79efee3a3be2056167eb3c5785e480b1bb86255389d32709d071d75fe80000000000ffffffff934ab77ec846c517e4f710c23f2b1cb8c6df84754287bea2c52cb2876260b33f0000000000ffffffff01c07fdc0b00000000225220d8ce2162ebd4a65d21981113193e470a7605970928b5fcfe9636d0a2d4cecc4f0101bd0141bd2faf825526767386f5a6b80cf8726a5405168ce19990db8fc07d735f74267e2feb295116eb483fce45488c6b7543efc4d93c77a4fadbcc061e9a7b6c9b5112f800000000",
+ "valid": false,
+ "failureReason": "aggregate signature invalid, messages commit to opt-out mode"
+ },
+ {
+ "id": "fullagg-size-mismatch",
+ "description": "Full-aggregation final input carries 96 bytes after the marker (97-byte 0xbd element, matches no defined witness structure)",
+ "prevouts": [
+ {
+ "scriptPubKey": "52208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e4",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522018b6491469cc78b764b01669b0a5a4e892f57e8c1b20b89bc1ca6dce3e881b51",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010254d36a0897340110cc34b526d73b206175ae4f5db4d25fe7bdeef3a8a5e89afd0000000000ffffffff835a02826d69e61b563ec713020bd2b7c3bc3848223cda2b3c52bbdcd3e6267a0000000000ffffffff01c07fdc0b000000002252208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e40102bd010161bd60ff22c66f7fe9a0911ab6a8a03542510b494e2e1d9de2246e670ce6acd16dc99229ab127ded30ca694b3a846a4e16fbba4246a21a7d21876aac6e18f36ac1c7000000000000000000000000000000000000000000000000000000000000000000000000",
+ "valid": false,
+ "failureReason": "marker and length match no defined structure"
+ },
+ {
+ "id": "two-groups-valid",
+ "description": "Mixed transaction mirroring Example 4 of the BIP: two opted-out inputs, a half-aggregation group with a SIGHASH_SINGLE member, and a full-aggregation group, with interleaved input indices",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220b3fd2ebe2399bee6b51828e7fcb82ae1752516c6866c058516a8d592f38b19e8",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52209123086f2d8d878088ec611185f841658513af7f29a1359685eda09536d7dc5d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52201ce55104c3bd91d20b8d7702d4157bb5a70bc0b6e63cd73c3e7684b9c3156d70",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522049875458881f78c4ed3d5fb0e95ee527a2dce487ebad7cc92bdf28a9017df888",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220fbfa11a63ffeff576f5d408c5618ed8761aeb5b6dc92ab36055b792c3cd8ceea",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52207ae16cb88e80051ca988443009fde24fe4d6638f5870fa5cdc6689755bfa24a3",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220ad8f2c1d50a83478441fb5b5887dc53dcf5f342d63a4a1b0e848aef31f2d25b1",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001074dd302fda269f70608bc946098680dbfee25271c19d636ccc42076049f66d3850000000000ffffffff2b5f4f73da1e6ae2f7dbeddf95ceee3219ce7f4a14780bb957453015ad21c9840000000000ffffffffa43d649606b2b1ba5690d9e796f73f4ac8a4621594a29feb75f7313be68c2a2c0000000000ffffffff89bcd4114dfed0f70ab200fbcbad208d2a17cb23f4d582d34c280cb23ef00db20000000000ffffffff92fc75b30a6e897056ed31a32ea0785e9130fd04a2cdf38f8e8a80c9015c65d90000000000ffffffffba7b09c4178344c89056482bbb515c7dd45bccd7de7d6c9b766ee151490b63ff0000000000ffffffff10d992fa14c1f6bb3d82f31b9e8315cb0dd1caf5f631d9ce1c84f6d8762ebbb40000000000ffffffff028093dc1400000000225220b3fd2ebe2399bee6b51828e7fcb82ae1752516c6866c058516a8d592f38b19e84051cd14000000002252209123086f2d8d878088ec611185f841658513af7f29a1359685eda09536d7dc5d0141bbfdbbe8761ce935325846c709e237c8b6d988cfc011711e4433e4adcfa5b687cfaf3f2356a4315d75134df3f7f9ecc62c213852521556feca1b67fa17f96aa8fb0102bc030101bd0101bd0161bc495cbe82aa8b5c134aaa6e482294108232706b1b9bc17ec6dd2a142921a27c32ca82d465c5b280bc90d0724e3193d84a36df9761df5927ef7317b1109b692e1a492f2be9f5618dc8e391914cafb33a9a9f4909c2b8870ab2f8b9d16cbb5ef7030142bb82d80b7af609de62718602261f4cb16863bf177636a6dc01b8cb5b7c4076722483ed3d21b52a424d54fa0a57b973c838f46292811430c5167e9d63c0af5583fc3e0141bd2022cecf27fe3fa8def798dc85fccd4b530fe9c099129c445c2500f0ba8860fdf02de7d3dbc1cb7ebcb4f83e66262d15b0ebf3ffaecf0400c59440ae1f678dfc00000000",
+ "valid": true
+ },
+ {
+ "id": "sighash-single-no-output",
+ "description": "Opted-out input uses SIGHASH_SINGLE at input index 1 of a single-output transaction",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220f082518f206f3be02e70d0b1eabd7152c6e4e3cd8c7df5517f198a049e1b5520",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52207cef82518a7ac1c239b1e33a92197ea618f06ba71be021f59a73bb1f39d2acf2",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010242df0f438abfad5a59cfa19d12c0a7954b04301830c63049f741c0ed73e591f00000000000ffffffffb354dc5ffad02a76b72e6d702aecd38161d43bc42c1de51dc4a62c5bcb4fe6ae0000000000ffffffff01c07fdc0b00000000225220f082518f206f3be02e70d0b1eabd7152c6e4e3cd8c7df5517f198a049e1b55200141bbd38b3477eaa9d244d9b429c0e51ae356ebef4e128b44e68b2e1a0a9fb547fcd3fa71139811b43427c3482d7c66c4639541c788e1a515ae3a7b241f09bd29c0a60142bb03c54d70601b5db396b929c03164f519172f3735666bc49c7d779581ec3bd733d61e374239621f67b2e6e2f4a6dc59c2cb19f806251a6fc36d681a7bd98ee950ea00000000",
+ "valid": false,
+ "failureReason": "SIGHASH_SINGLE input without corresponding output"
+ },
+ {
+ "id": "halfagg-two-finals",
+ "description": "Two inputs each carrying a 64-byte half-aggregation final element",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220ba77666df5eb8e5c526acedd38d36fc5eaa66da3e26460b71fb7b164089aeb1d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220379fd1c1e12519df482f6c99b464892f04f59418b20b5f65105a93e240e99013",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001025cb6b3fb7d3453d43f571a65d462f30f76bcf4a9cfe6bbd13b59ce601b8475cb0000000000ffffffff57b6ff1c4d459776ed587d8242d7d41c913c64b264eb1f72145489b677fadf4f0000000000ffffffff01c07fdc0b00000000225220ba77666df5eb8e5c526acedd38d36fc5eaa66da3e26460b71fb7b164089aeb1d0141bc7607930ce963eb16ef197fafd0d91dc4ddae836d56745e49713f4d10d0a801e730acc481cbcc2797ddeebb575808191501821c634b2e9f628427d30e0a2876f50141bc5866fbabc10dd40c01e0945b2e414ba9bc509f36275ef21738483dda50a3f6019c35b81f37fbc4c98be423dc9f782ae71f835f5870495ae31519a8147c4b1b4600000000",
+ "valid": false,
+ "failureReason": "more than one final input in a group"
+ },
+ {
+ "id": "fullagg-two-finals",
+ "description": "Two inputs each carrying a 64-byte full-aggregation final element",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220a0bb2d2ca5161c1c7bf13ba18767027e3c68e24a23e8fffce66f590c7990f2c1",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522080fa96d74b89de3ae09becd3ab08a1f337471ec9a0104ef6571bdc24737826ba",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102faf294804fe43f7dfec177b001477ea5227e47cfa71dc1e4aa60a84d94af0d0e0000000000ffffffff0056c298771ca89f13e7ce4d0b03a282f4803a332d6cf73050ed20cc4bf113b30000000000ffffffff01c07fdc0b00000000225220a0bb2d2ca5161c1c7bf13ba18767027e3c68e24a23e8fffce66f590c7990f2c10141bd652b3eb418ba83065dbb167cb7a18a62fe5fe5b8c5db46cb29d2d19b72bd438a67bdc36329abb657c7019f026672bc2617fae0fcdee88a9e688a16ecd3a9cb120141bd3620a5c90db6b1799ae20ac5af2de813f25b6a08fcdf9fdae7582b793cdbb38da4959abbd580892647d122ac3a71f05e9a1b3fac25e2dadecba7fdd04affb91300000000",
+ "valid": false,
+ "failureReason": "more than one final input in a group"
+ },
+ {
+ "id": "fullagg-no-final",
+ "description": "Full-aggregation markers without an input carrying the aggregate signature",
+ "prevouts": [
+ {
+ "scriptPubKey": "522014ef07b53514ff8cd94d4e6f4ae0fe246bed37e20c365679f404be89a84a941f",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220ce9d369fb59e92098b3e0389cb8d91a3e0807fe51b04dfb7f77a50312498bb02",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102027c0bbee08a5b2b927eac3b88b8305a951fa546550f67a6c52b5542bab2f8ce0000000000ffffffffda74bc8dc984d4dcf61700d4e246a2b781c65d3583e4a99760ef88747fad4bf20000000000ffffffff01c07fdc0b0000000022522014ef07b53514ff8cd94d4e6f4ae0fe246bed37e20c365679f404be89a84a941f0101bd0101bd00000000",
+ "valid": false,
+ "failureReason": "aggregation group has no final input"
+ },
+ {
+ "id": "fullagg-marker-after-final",
+ "description": "Full-aggregation marker appears after the group's final input",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220f94afc4ad57d30e591c8c2e36c9f4436a4583a110048f909759dcc4afd1eca0a",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220029fb6b380929cdc76037e241819adbc7a27a14335a855d087bbe2e957a62110",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220130647b26b8fa4baf6e7974cc280be301ae0a186b5b43b36f8fb12b680281eec",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010325af39ed72d725fdc2edcbfbeca5f24ace00352010e3e10de0d0792b6ed6d9500000000000ffffffff6e55f5fe52f5b4d3f707a20ac5cf6280acfca6b29bfd17c2249eac2b5799d2e70000000000ffffffff5875cf9e3bd9c121e935691bdc84c18daf7f3a9f697a525e59330829282857f90000000000ffffffff01c060d21100000000225220f94afc4ad57d30e591c8c2e36c9f4436a4583a110048f909759dcc4afd1eca0a0101bd0141bd5d274cac012ba1f2125e0e880b75be1e5d557ca88928a89380142a0f5b8e425dbe41a79173fb73be2407e46d39ddb8a7e0e27307afb80a66590d9c50b1f4513d0101bd00000000",
+ "valid": false,
+ "failureReason": "marker after group final"
+ },
+ {
+ "id": "halfagg-single-member",
+ "description": "Half-aggregation group with a single member carrying a 64-byte aggregate",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220efce560c8eaf170db44a9af7691f8807be52d8314c4575aa46ba4fce54c2b0c6",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010187fea558630296d581abd8d24b147faeaa371a0d325edcf565b6719d8fd5ea310000000000ffffffff01c09ee60500000000225220efce560c8eaf170db44a9af7691f8807be52d8314c4575aa46ba4fce54c2b0c60141bcbf6915975cc9bf5ab48830c0f34026519f668d453624848298d9e17fbf1362792f96def59bbab5ae47ab790704392b1e456703ad14bf72c375e874fff750dd1a00000000",
+ "valid": true
+ },
+ {
+ "id": "fullagg-single-member",
+ "description": "Full-aggregation group with a single member",
+ "prevouts": [
+ {
+ "scriptPubKey": "522009d2ff94dc12d14b0b63c95dcdc8cd2a77a3eecca3bbf2809d322dc8b6b5d62b",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000101787d107a893487240e835409c4cd5c68420bd6301ee936c2b201e90ad5bc8de60000000000ffffffff01c09ee6050000000022522009d2ff94dc12d14b0b63c95dcdc8cd2a77a3eecca3bbf2809d322dc8b6b5d62b0141bd4c72a1b21ae90aff0ca628336daed2f1a8630febac3992bb6cdff5c30a55af917fc1768c4b3ffc1b9e0643bf4c4e9efa536186eea376b08675cab1da0845f32700000000",
+ "valid": true
+ },
+ {
+ "id": "annex-lookalike",
+ "description": "Single witness element starting with the annex prefix 0x50",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220c0354bcb3c4b096849967c23f7f424baa649b15777a1ed25d52770064a84be10",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522000277756e6ec3da81b4854d52ecac45717fc3c5cc3b27ddcf6e3755b41fd87c9",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102948f52623998b05a3ba6679673c6894c7b9a596869421be46b54c7db521a7a8d0000000000ffffffff31509114c2c60b92f3e6037bf52ad75bac26572a19178051fd1ee1e7ae08ead30000000000ffffffff01c07fdc0b00000000225220c0354bcb3c4b096849967c23f7f424baa649b15777a1ed25d52770064a84be100110504349534120616e6e657820746573740141bbd5e1b9f00d6e05a954cd5fc9f088686a7295f3a9ba52ce6211427b7b64a35dc976c6c58f06ccfb796970f4191d7361f4812d549421608f6c02d38b981503603600000000",
+ "valid": false,
+ "failureReason": "undefined marker byte, a single element is never an annex"
+ },
+ {
+ "id": "annex-on-final",
+ "description": "Half-aggregation group whose final input carries an annex that is committed in its signature message",
+ "prevouts": [
+ {
+ "scriptPubKey": "52207dfc09f1e03f0d901bc2410833199965cb2a884891ad9889ff5352b29abb2922",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220f9744be4cd3ce7a2fd0b6a453f94e758db9b4abc47825460cfe48d41100f10e4",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102395d48eb0b3ef75dd62014fffcff868fdea29bb830b9fe14e11ae84b22e0bc6d0000000000ffffffffeb44386a3268ac6fb39f78bc9d7023176bd027ff6d7cff7007facf4b98455b840000000000ffffffff01c07fdc0b000000002252207dfc09f1e03f0d901bc2410833199965cb2a884891ad9889ff5352b29abb29220101bc0261bca6be4df7499feb83100032270c47f4e5e6170c46de0613bd7b75b94774505a96076f82a2e92e4b6d2810850b5d8e645ea91fea52e6464873d22ad66e80cc9dfc54cdf15793c29b96df45afcb2126572d2113debb199942bddbcfb9f927c169df10504349534120616e6e6578207465737400000000",
+ "valid": true
+ },
+ {
+ "id": "sighash-value-undefined",
+ "description": "Opted-out input with the undefined sighash value 0x04",
+ "prevouts": [
+ {
+ "scriptPubKey": "52207eab9a8cd328cf1ed6329998569ec6b195c368c02cf5c6108476fe95426d883e",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522002d94ebbd12c91590497f89072b4c5b52a77f09f4f1e6f7da33ccccda240f6f7",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001029e94c92941217af1eae64bcfa170116a85cf01c1b0a68d61f4f12bdd09bfdbdc0000000000ffffffff0109ff9ad5f993dec28e407eced18a14380ef4f1d2fb5951f46e8a3506b605b50000000000ffffffff01c07fdc0b000000002252207eab9a8cd328cf1ed6329998569ec6b195c368c02cf5c6108476fe95426d883e0142bb04abb8afa3e434a4b009e7169e182cebd05e00359afc9109b0e548a9a759ead713eb3436e4637d9d48b8cd19f4eba79ab3f14f39e815395fdedb223f136342d69d0141bb2615f34eeefb93d6aed1beb7dc39d92e3488ba1a3d28194c903054aa900aee3658be27fbe553ab280c926f123db99c6a25bc6074163e9e200c2970becd744edf00000000",
+ "valid": false,
+ "failureReason": "undefined sighash type"
+ },
+ {
+ "id": "halfagg-final-too-small",
+ "description": "33-byte 0xbc element, below the 65-byte minimum for a half-aggregation final",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220525bedf0f14fdace49ed96c64c32603f79890443a3e97de56195e56c69b12820",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522052448ca22a975f6526b124857f9cc09aab766848bc29fc8dae549688f2bd6bfb",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001021ae3614f251ce37d8a9514730e17068c60b46853d1bf515b825d1a51643067eb0000000000ffffffff49a39eef33a2b03cd9ac2ed493f1c19a67d269b499b728cd4d942bf119baf8840000000000ffffffff01c07fdc0b00000000225220525bedf0f14fdace49ed96c64c32603f79890443a3e97de56195e56c69b128200141bb87389e3937ea1c5da6301a0e882b8afb7b2f37c40b2f13717e77ecc510b5db44b7d4d34a2e71dea80cb491350e306fe5b35b860c1beda77d44fdd45dedfb1c5a0121bc2040ba76514a01b76d05a375a63122423b390bd68d9c11c9514353adb713991f00000000",
+ "valid": false,
+ "failureReason": "marker and length match no defined structure"
+ },
+ {
+ "id": "program-not-32-bytes",
+ "description": "Witness v2 output with a 33-byte witness program spent with an arbitrary witness",
+ "prevouts": [
+ {
+ "scriptPubKey": "522185ea1c8b957e39246317d01dd5fba0526394b55775db91f32ffd6f983a64c0ec6d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220e3c7e3c4b02437debd6e5ab4cf859378de04da3089be27fac7d7ee4143372075",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001028a2396376d580b848cc4b43d0b0bb2e3481c75ba80be547df52b6069714540960000000000ffffffff2d63ef4986cd3f7b7e7a30bfce4028fd7ee5ca2032f4c2150af17cca84e55edc0000000000ffffffff01c07fdc0b000000002252209e1284ba591f050e4b1475d050d0d54048a15caf983a56b488d25d59f7854cba0101be0141bb801798f3cc7e81b8af4376de6ad06903e001d099ecf772d415bb93d84bcc1366eac41bcf71ff152f7d3234fc9756891645e602757cb1bc6226a9b24109dca27800000000",
+ "valid": true
+ },
+ {
+ "id": "program-offcurve-optout",
+ "description": "Opted-out input whose witness program is not a valid x-only public key",
+ "prevouts": [
+ {
+ "scriptPubKey": "522064c3ae43dac00d99f5ebf5ec074d41a5e4e4ddb7c715564755696b7499d5665c",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52204a9fe450c24c111425a9dd4632f4fc94ed2714681717e369c059789e3751889e",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001024dbb065d92ed45586f93ce8fea4d58381e09c3cb1bc7ebabb40e6d7e3fd87c3a0000000000ffffffffd9aef3faf3ec7368e3932fc48ee1f50a233573ecb42a05c886598994956e8abb0000000000ffffffff01c07fdc0b000000002252202991591a0ebcde9e2fd143ea0bda8265a67659aeeb5396935b4025e1c633d0300141bb4ec4690ce502a5f0ab5fab245db9be42ccc650780be0f3ecaa4082a3a68a69428c363b40db3cf86e4251d733a577f3c994982e2965eac7a892e392126a29fcfa0141bbc3ff92b304caa6f495850d42bf992950631f2187c0ea67d50d34542d6c736d8f0080ad84f44dcd4a3280ec9cbee91adce23eeb6a5a9af28e6a9cd89a1af1f13100000000",
+ "valid": false,
+ "failureReason": "witness program is not a valid x-only public key"
+ },
+ {
+ "id": "program-offcurve-in-group",
+ "description": "Half-aggregation group member whose witness program is not a valid x-only public key",
+ "prevouts": [
+ {
+ "scriptPubKey": "52202b57dcc98a4977eb9e5d9c7d2b8b6ea7eccad98ee1a40f77dc43f80c94be5829",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52203ecbcb60ef10bbe471afc9317fda3ac61a26422688d9f9183484ea08b439ae94",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010273d7bf0559f1173564708c1e2d0ebe6175b72f5adee692dc6bf9ce52b75f038b0000000000ffffffff6e048d65284060c05b39ed176a8998ea32e8dd730eb3933a60e755796031f4a50000000000ffffffff01c07fdc0b00000000225220aa5413f1dcbd6a3f50db0278bcba58570af3e0d845807b5bcdabd92245739d1f0101bc0161bc7ad0b4a52df2eb8e61ebbcf7f318dee3a1b54679c8e4314827a60d368633adbcf6a577712f6ee97781d101525cf34d0eb31471f35c85a2624ab257461e1ffedebd29211b614cf25019cd4345df309889ef09078f21c470907cea39c813e0b72700000000",
+ "valid": false,
+ "failureReason": "witness program is not a valid x-only public key"
+ },
+ {
+ "id": "scriptpath-unknown-leaf-version",
+ "description": "Witness v2 script path spend with unknown leaf version 0xc2, which succeeds unconditionally per BIP 341",
+ "prevouts": [
+ {
+ "scriptPubKey": "52203393a7b8c41d3c155cb9e1f9a49a76d1eef2c2de0b5fb76a693a7340001527d7",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52204b87263c4be4747effe770027e7677d8ec6bb46522c50a101d6d43c468265194",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102f9fba162f92dfd4eb91c55cdd8822a997bc4f4cce7e1dc7dd39debb837aa287a0000000000fffffffff82399ba5d5eaf6a581d7ac39883afc9e8f090134fea8afa3eb1c18b9b82c3c90000000000ffffffff01c07fdc0b00000000225220e524abd4c236566678fbb385b729120ef0e57b3ff78c055f807dc4a0ca615f6c02015121c3ab9432f842079557d76f597fdd75613784568e6d33b6a2ccb0cf7c442acf3e3e0141bbe7c2f08a316ac20a9e49401691984cd7778ce81b0c15abdfc4f3492552f86dc7b6e21ca06babc763b728f75241ea96a20f320bf029ba86b1d20fde7d04e5181500000000",
+ "valid": true
+ },
+ {
+ "id": "scriptpath-op-success",
+ "description": "Witness v2 script path spend whose leaf script is OP_SUCCESS187 (byte 0xbb), which succeeds unconditionally per BIP 342",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220756fafa4b405848a5e2ca44e6d2f1fffe7fff38d963c2217034b6c2cd44767fb",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220f76f37ca6052b7292c8ad1572f57e33e106e04439d2dde833b8324c312bf46c3",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102122000b6c37db73bea687e47db28ec20c7c593e2c294223968607d1b18e075610000000000ffffffff65c38e05ba06fb03830ff257c768e439cb21820683af5f970d17a4403794c9dc0000000000ffffffff01c07fdc0b00000000225220abcc959351c9c6d270152cdb3be1b64b1c7a0755c325603899b33154e3b1c19b0201bb21c10d59725454cf341d2b7bc31c95f1149903f64c33e4baedee3a5669a3cf44633b0141bb7a47297f8bee16458b273ef05d17e2ab4e90dc4bbac11b5509a2edf149117c6082add07c55c04a55cf469a1fc05e267bb684c5f43a8174594fa9be26aa5a833000000000",
+ "valid": true
+ },
+ {
+ "id": "v1-v2-mixed",
+ "description": "Witness v1 key path input alongside a two-member witness v2 half-aggregation group, mirroring Example 3 of the BIP",
+ "prevouts": [
+ {
+ "scriptPubKey": "5120a65a8d02e401105e9e68fd11af633b2135be019ccbf71bfc5fbdfb8e44ba2df7",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220f943a227ca855dd9d22cf25837122a0f0dc129d77b452d8b2c20b2bc709bb684",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522015d5e4e64a5220c7d2c68f5b97454b29c08d6642720017e3ba8d1d133ce98cf6",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010399d38299f14c13d666e271ff326cf8c5e43e21d70826e4f1749566a3e80610b90000000000ffffffffeaa41167c8b8ee495014701bc627f54734586036efed5912e68d96b8fe4fd48a0000000000ffffffff78d37d165ff26a505a04c1d645f1f1d393645c7ab1b2fd81aeb429537ebe81670000000000ffffffff01c060d21100000000225220a65a8d02e401105e9e68fd11af633b2135be019ccbf71bfc5fbdfb8e44ba2df70140f631a1ffd1cf8f1fc640dfecc5453aec039f4cef7f807d5821f5750f2ca44bef7c304c558dcc7ac49b22a247c5c9992121fc0796a8b73b7113fd23b25e6d82480101bc0161bc683645a000862a928c89d3982224214f40ee17daa20b826a4c4a102e91d644cc8a1dfc706d15974f9ef5509eaf0cd521da02f01decafe69045a15273c8ec7579a936b62607e610249f088f1a2c2bdaca8b3cdf0e97233e0a064b4a9ba7da6f0b00000000",
+ "valid": true
+ },
+ {
+ "id": "sighash-byte-stripped",
+ "description": "Explicit SIGHASH_ALL byte removed from a group member by a third party",
+ "prevouts": [
+ {
+ "scriptPubKey": "52205f56c844151d6ded6024f0c30ab90e436e03c5f199f66d77553fc0b8eee66377",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220366b81b84d19c39f58c8769cbd2b1f996542266c400c8de7e3997a5c143d7847",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102959f971cfdea5c89923108e7d2c9abf34a327ce3f0f1aa75e054f923b95b1cc20000000000ffffffff9e97d6493ed7423faad0636f6ba60f4a8eb6b26efa6e75513cf85da975c8c0eb0000000000ffffffff01c07fdc0b000000002252205f56c844151d6ded6024f0c30ab90e436e03c5f199f66d77553fc0b8eee663770101bc0161bc809b87ad64fd5246f69c246e81ef26c75323632c4016387b38efae7dd26155c57699d8ab2d766b924d454eebadb25596d86b44d1895b713dd53f016cea6b9484130a524a63ba98604780fae2977a728d57786bad1425815e4cc26ee7af36398600000000",
+ "valid": false,
+ "failureReason": "signature message committed to the sighash type"
+ },
+ {
+ "id": "empty-witness-element",
+ "description": "Witness v2 input whose only witness element is empty",
+ "prevouts": [
+ {
+ "scriptPubKey": "522080348d8193f0b62e05f9614234c03965f9db14665f38523bfe4fa38bc9beb468",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220a6365e706c43c32a7382608828bf4d219302a520286b02487807e63e5765813d",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001027b29d1ac4cd5356006588f0d3622ee61a6fc29ad2d299f2f6eb69cba0d7f5caf0000000000ffffffff9e6158b3c5934a484c25e47bd4dbbb3ca36c773d97a421e842806660232f0a0a0000000000ffffffff01c07fdc0b0000000022522080348d8193f0b62e05f9614234c03965f9db14665f38523bfe4fa38bc9beb46801000141bb1d705d72e4f78f6ab8b706a35febb22e7718516bed3ad5e8075372ebba9442648cbd067d2aaf303e95dc4b6eca9ce411baa903f63db577980495335e1e8c225700000000",
+ "valid": false,
+ "failureReason": "empty witness element"
+ },
+ {
+ "id": "sighash-single-no-output-group",
+ "description": "Half-aggregation group member uses SIGHASH_SINGLE at input index 1 of a single-output transaction",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220c8ce4e6a6a0d34357827bb0fe91cfd76f4dd76178783ade19eebf9709aa849f2",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220238b9c1c5f0a8267a1407055ab4710d68b6f17c78cae61a26bfde011ad304676",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010243255c54b0a85e8928e17b91f9f3f344372545f70a9c7e85450f4af1d2243cdd0000000000ffffffff45f25548efbd2113170e2c97cb5a2aecd9b0537956d766c60af33b54da9098320000000000ffffffff01c07fdc0b00000000225220c8ce4e6a6a0d34357827bb0fe91cfd76f4dd76178783ade19eebf9709aa849f20101bc0162bc03ce3a7541d82797a2e57bc68ea3845186c806e3c73ec6655fa6c98c376df7ffc948021e012ca5c831276f4c79425029651ed38bf6082dd791094da491062fcbcbeaced7ba3a69a604d8c83566fc0de5e9f5560b8faba71aa9278fda86240111d800000000",
+ "valid": false,
+ "failureReason": "SIGHASH_SINGLE input without corresponding output"
+ },
+ {
+ "id": "two-elements-no-annex",
+ "description": "Keypath-looking element followed by a second element that is not an annex, making this a script path spend with an invalid control block",
+ "prevouts": [
+ {
+ "scriptPubKey": "52200391079747abc6405211bd3086ac8d0cfa8289c509120f63dfb37348df78178d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52201e6238ab9bd6b76360d86ec5263f6de69dbd2d2766634cc4fa7d814328436633",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000102796ded7d3a447a3c4a578f9535a9a52bf44b65d6bfd795fe142c0747df38c42c0000000000ffffffff679f42915303ad3f898d2b0bfee543aa568cb6478000e44e58c271f4b6e52f0a0000000000ffffffff01c07fdc0b000000002252200391079747abc6405211bd3086ac8d0cfa8289c509120f63dfb37348df78178d0241bba222ccbc9774fad77ecbb49bc434149cb2588996fb0d0d85ba080b13ad2a6e71c5414e57c53d79970581ed70baf06ef0043de564e1875ab907de531ca6d1d1f801600141bb549198741331e47a107a08a170380304b3f3705524f3792cbe4b76e5821ec14c4dde31504e19dbbc37facab0273f0f78b0d9ff67058821acd5fc11e04b7ec22c00000000",
+ "valid": false,
+ "failureReason": "invalid control block size"
+ },
+ {
+ "id": "v1-style-spend",
+ "description": "Witness v2 output spent like a Taproot output with a bare 64-byte signature and no marker",
+ "prevouts": [
+ {
+ "scriptPubKey": "5220f5550ce6e940cf53f9f175a7a81e18e4dd2dac375a4433ad15599979368aac14",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220d8846f187dc43f7a2df28b59753aad58bccbdb873fa0eb0dbc191edb6d322e12",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010274f6fcc771690ea6f38fc9df6d221ea9e858787d79b2e192f91b03f4e5ec59720000000000ffffffff038baab567625964cdcf30d6bc337418829f2699b28f0d51d8ef861a77dca5260000000000ffffffff01c07fdc0b00000000225220f5550ce6e940cf53f9f175a7a81e18e4dd2dac375a4433ad15599979368aac14014047e40d22a31336eef7c12462d0ea621f3ebe34b38d5c241e58f1b2409520b35af4047fdd503e9077d512777a4bf3d3d6b9cf7751064646fd0de4f29740a489ca0141bbfdde485ecea96635d5c030b69d9bac36b1366b32b9684c2ddb7c83522bf8dab7511b5c807ad59311bc554b4dd260ecdf651220fd7fc55cb64d0436589278a56900000000",
+ "valid": false,
+ "failureReason": "undefined marker byte"
+ },
+ {
+ "id": "halfagg-large-group",
+ "description": "8-member half-aggregation group with a 289-byte final witness element, crossing the compact size boundary for witness elements",
+ "prevouts": [
+ {
+ "scriptPubKey": "52207ca5a23b5d8253b092e5e7c2ef60167966781689ccc0d311f119759beba0fba7",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52205eee9aada57d816a6cfbb21092b51b0e7fe93d2ee1f3dcd80e27343d419527f2",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522064d00057948fa1890a464fc996a8588b0ea8c6d2abe4c786a4cbc47d0c1cdd3c",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52201827c2d005bd8cb07201eee287ca25cd1e4b209e16fec42d3404fba65d68dc31",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220a8f1b32af980715b3538daa4af8da54ad9f5af4806855875ca955e85a8d807af",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52200a0448560df8dbe5e36c3563e2a51b20f0f08fc2cba5f139822958261ce4ce0d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220e4b368758dd3b238a1705b26cb07df8cb2a2521773701b64c3b573c499526fc0",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52201bfa0b74a1992ae24806abbf2aac3c3f0fb77e2c64083cf2e70313d0214abd73",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "02000000000108b8efc85febd6bd845177a7171d06cfd52e3a3c688dcfa32a8e8584841e838f770000000000ffffffffe3ef25b266108d5200cfd923167e4dd7b18072994257b674191ceafbed9f8cbf0000000000fffffffff535b8fca7bd89438e9ba575bdd71f12ececd5e34a7fe622459721dd437c71be0000000000ffffffff6fca2aaffb88ae969bcc62522629bb4e2e31af406a8fac5e2701bb6c3bb4305e0000000000ffffffffa026937889ed804a415cd70c9509dc7edf20ef28278ef23e35e2249f73457b570000000000ffffffff81a4bfd409211d0a308c24c36ded78110f5f5f751f00476ce9e06d6871b2d5690000000000ffffffff527ed3cee5f9c269fa74ac92fb266c65e61208faeb6b0619000dddda32cd167c0000000000ffffffffbde7534ac2149e11a2ccf7e7cdcf01964d068ca662df3d60c42e0cac08011bb30000000000ffffffff01c0c59f2f000000002252207ca5a23b5d8253b092e5e7c2ef60167966781689ccc0d311f119759beba0fba70101bc0101bc0101bc0101bc0101bc0101bc0101bc01fd2101bc9ec9d9e7172f588efca57173d91f63738dd5a9e4f6f03274046ecdbd854a22456b1cf84508ca880021423f40ffca58ff5d32455ab00d96f0a729e0a72bfcbc53187a1b6ab9fa96226abb5b47c18c489112924940f0f809230a904591290be90bc61c05f70bd771e926752988d74ced3f8bfe57d279f0464d626a1c2acc8a9da79f714ccf3230608d91695e954826083061c02cfcc6fd11f5aa706e54e5da4f8d724731033b468ad8c6c6f3b36f479963651e153041dad5a7b74284b418a80873ad8004c0267a9d9ed8a725046b66b6de030f6ce0d0c6f3a40bf1c2d29e46abdbc9e636dcde7f5c28cd5e18be362e44ce2da1e4a85ba62c99c777be590ed54af200653f735dbaaa2f016d38cb5a62c5cd4f98fbabd9894ca7d9559a901b4d546a00000000",
+ "valid": true
+ },
+ {
+ "id": "scriptpath-marker-lookalike",
+ "description": "Script path spend whose first stack element is a 1-byte 0xbc marker lookalike, which has no marker meaning outside key path spends",
+ "prevouts": [
+ {
+ "scriptPubKey": "522082ace6d1d7a138a915bc07569161b1b6484d7c800b5e2fab8b573aa3dce40175",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220ffaa7773252d946733d00cc9870e80add10e577893ee972332c910dfe0f0b61c",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "020000000001026c0780d0a76dc7e1f78a6fdcf25908625d50d37de0b055e4acd4198f23deb6a60000000000ffffffffd3c43fd83c79f225cf5208290086806fb0aab1111fcd10888aa972183b3c8fa10000000000ffffffff01c07fdc0b000000002252209895f077cf6760cdcdc729e244e345fe71f84d49bfd2958f01210a1737caf86e0301bc02755121c0c445296d4c0d625c2932df758a7f2a736d022bb6a55bb52c9eb52eb90ffea14f0141bb7b07982153dffd1f39ba765c1a82aeb9685ba5cd4276324b505ace7693025f8523958a6ee2ea747d6b475960dec71e746e37e0df3e3e1573058b5998032e302c00000000",
+ "valid": true
+ },
+ {
+ "id": "duplicate-key-groups",
+ "description": "Half-aggregation and full-aggregation groups whose two members each spend outputs with the same witness program (address reuse)",
+ "prevouts": [
+ {
+ "scriptPubKey": "522098425d28f35f2da20c400f472183065fb5b221d6860962c68bb6d20bac20b081",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "522098425d28f35f2da20c400f472183065fb5b221d6860962c68bb6d20bac20b081",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52206a0487f7f1b886fa757ea427cc370ff758c30e0e9ff194ebd20decb9a94eaa9d",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "52206a0487f7f1b886fa757ea427cc370ff758c30e0e9ff194ebd20decb9a94eaa9d",
+ "amountSats": 100000000
+ }
+ ],
+ "tx": "0200000000010491e7995af63730c1a6a372277b67a95dccb8610e91a99edfc1e91b227f7437830000000000ffffffff7ebcc3e7e23a3284bb376d66ed1e478a78d4c831e714adce963abd99630803360000000000ffffffff27f82605d137a2cda276cc7aec0e57862a1021b3ab26eb82202b14e50ab6fa5a0000000000ffffffffba4bce7307bda6108692c4b4e89eb98b7a0953ea26f798921557ffaac52b52120000000000ffffffff01c041c8170000000022522098425d28f35f2da20c400f472183065fb5b221d6860962c68bb6d20bac20b0810101bc0161bc4134bd40de37fe40f669ede09cbdd10c8314072b79df64d1b1d5586fca61bad473b25c213b8dcc75caa9ae39e1dc8fd26f507f718c73ddf5cd387fbfafee290ee17c14f19f8d5de990630bb924940ac910b4515a49b97fd3138add80e8f7d1f80101bd0141bd5577b6e37ddb2f2fb0b5e92ac81d62e5054f26095c4a7aaa83f4c1b7844c5b5bdaea62d60875842f86f51916f129278342f0757d7adf6e39f975685d1fa638e900000000",
+ "valid": true
+ }
+ ]
+}
diff --git a/bip-0460/fullagg.py b/bip-0460/fullagg.py
new file mode 100644
index 0000000000..cd1cf59d93
--- /dev/null
+++ b/bip-0460/fullagg.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+"""
+Reference implementation of BIP 459 DahLIAS fully aggregated signatures
+for secp256k1.
+
+WARNING: This implementation is for demonstration purposes only and is not
+optimized for production use.
+"""
+
+from pathlib import Path
+from typing import List, Tuple, Optional
+import secrets
+import sys
+
+sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
+from secp256k1lab.secp256k1 import G, GE, Scalar
+from secp256k1lab.util import tagged_hash, xor_bytes
+
+n = GE.ORDER
+
+FULLAGG_TAG_AUX = "FullAgg/aux"
+FULLAGG_TAG_NONCE = "FullAgg/nonce"
+FULLAGG_TAG_NONCECOEF = "FullAgg/noncecoef"
+FULLAGG_TAG_SIG = "FullAgg/sig"
+
+
+#
+# Helpers
+#
+
+def has_even_y(P: GE) -> bool:
+ return P.has_even_y()
+
+
+def cbytes(P: GE) -> bytes:
+ return P.to_bytes_compressed()
+
+
+def xbytes(P: GE) -> bytes:
+ return P.to_bytes_xonly()
+
+
+#
+# Key Tweaking
+#
+
+def TweakSK(sk: Scalar, t: Scalar, is_xonly: bool) -> Scalar:
+ d = sk if (not is_xonly or has_even_y(sk * G)) else -sk
+ return d + t
+
+
+def TweakPK(pk: GE, t: Scalar, is_xonly: bool) -> GE:
+ P = pk if (not is_xonly or has_even_y(pk)) else -pk
+ Q = P + t * G
+ assert not Q.infinity
+ return Q
+
+
+#
+# Nonce Generation and Aggregation
+#
+
+def NonceGen(sk: Optional[Scalar] = None,
+ extra_in: bytes = b'') -> Tuple[Tuple[Scalar, Scalar], Tuple[GE, GE]]:
+ rand_prime = secrets.token_bytes(32)
+ if sk is not None:
+ rand = xor_bytes(sk.to_bytes(), tagged_hash(FULLAGG_TAG_AUX, rand_prime))
+ else:
+ rand = rand_prime
+ r1 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x00'))
+ r2 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x01'))
+ assert r1 != 0 and r2 != 0
+ R1, R2 = r1 * G, r2 * G
+ return (r1, r2), (R1, R2)
+
+
+def NonceAgg(pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, GE]:
+ u = len(pubnonces)
+ R1, R2 = pubnonces[0]
+ for i in range(1, u):
+ R1 = R1 + pubnonces[i][0]
+ R2 = R2 + pubnonces[i][1]
+ if R1.infinity or R2.infinity:
+ raise ValueError("aggregate nonce is the point at infinity")
+ return R1, R2
+
+
+#
+# Session Values
+#
+
+def GetSessionValues(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
+ pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, Scalar]:
+ R1, R2 = aggnonce
+ u = len(pks)
+ nonce_data = cbytes(R1) + cbytes(R2)
+ for i in range(u):
+ nonce_data += xbytes(pks[i]) + msgs[i] + cbytes(pubnonces[i][1])
+ b = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCECOEF, nonce_data))
+ R = R1 + b * R2
+ assert not R.infinity
+ return R, b
+
+
+#
+# Signing
+#
+
+def Sign(secnonce: Tuple[Scalar, Scalar], sk: Scalar, m: bytes,
+ aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
+ pubnonces: List[Tuple[GE, GE]]) -> Scalar:
+ assert len(pks) == len(msgs) == len(pubnonces) >= 1
+ assert len(m) == 32 and all(len(mi) == 32 for mi in msgs)
+ r1, r2 = secnonce
+ assert r1 != 0 and r2 != 0
+ assert sk != 0
+ P = sk * G
+ R2_local = r2 * G
+
+ # Index lookup and uniqueness check
+ matches = [j for j in range(len(pubnonces)) if pubnonces[j][1] == R2_local]
+ assert len(matches) == 1
+ j = matches[0]
+ assert xbytes(pks[j]) == xbytes(P) and msgs[j] == m
+
+ R, b = GetSessionValues(aggnonce, pks, msgs, pubnonces)
+ e = Scalar(1) if has_even_y(R) else -Scalar(1)
+ d_prime = sk if has_even_y(P) else -sk
+
+ L = b''
+ for i in range(len(pks)):
+ L += xbytes(pks[i]) + msgs[i]
+ c_j = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[j]) + msgs[j]))
+ s_j = e * (r1 + b * r2) + c_j * d_prime
+ return s_j
+
+
+#
+# Aggregation
+#
+
+def SigAgg(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
+ pubnonces: List[Tuple[GE, GE]], psigs: List[Scalar]) -> Tuple[GE, Scalar]:
+ assert len(pks) == len(msgs) == len(pubnonces) == len(psigs) >= 1
+ assert all(len(mi) == 32 for mi in msgs)
+ R, _ = GetSessionValues(aggnonce, pks, msgs, pubnonces)
+ s = Scalar.sum(*psigs)
+ return R, s
+
+
+#
+# Partial Signature Verification
+#
+
+def PartialSigVerify(psig: Scalar, pks: List[GE], msgs: List[bytes],
+ pubnonces: List[Tuple[GE, GE]], signer_index: int) -> bool:
+ if not (len(pks) == len(msgs) == len(pubnonces) >= 1) or any(len(mi) != 32 for mi in msgs):
+ return False
+ if not (0 <= signer_index < len(pks)):
+ return False
+ i = signer_index
+ R1_i, R2_i = pubnonces[i]
+ P_i = GE.from_bytes_xonly(xbytes(pks[i]))
+ R, b = GetSessionValues(NonceAgg(pubnonces), pks, msgs, pubnonces)
+ e = Scalar(1) if has_even_y(R) else -Scalar(1)
+
+ L = b''
+ for k in range(len(pks)):
+ L += xbytes(pks[k]) + msgs[k]
+ c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i]))
+ R_eff_i = R1_i + b * R2_i
+ return psig * G == e * R_eff_i + c_i * P_i
+
+
+#
+# Aggregate Signature Verification
+#
+
+def Verify(pks: List[GE], msgs: List[bytes], sig: Tuple[GE, Scalar]) -> bool:
+ R_point, s = sig
+ if not (len(pks) == len(msgs) >= 1) or any(len(mi) != 32 for mi in msgs):
+ return False
+ u = len(pks)
+ R = GE.from_bytes_xonly(xbytes(R_point))
+ Ps = [GE.from_bytes_xonly(xbytes(pks[i])) for i in range(u)]
+
+ L = b''
+ for i in range(u):
+ L += xbytes(pks[i]) + msgs[i]
+ rhs = R
+ for i in range(u):
+ c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i]))
+ rhs = rhs + c_i * Ps[i]
+ return s * G == rhs
diff --git a/bip-0460/halfagg.py b/bip-0460/halfagg.py
new file mode 100644
index 0000000000..ee4615dc00
--- /dev/null
+++ b/bip-0460/halfagg.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+"""
+Schnorr signature half-aggregation reference implementation
+
+WARNING: This implementation is for demonstration purposes only and _not_ to
+be used in production environments.
+"""
+
+from pathlib import Path
+import sys
+
+sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
+from secp256k1lab.secp256k1 import (
+ G,
+ GE,
+ Scalar,
+)
+from secp256k1lab.util import (
+ tagged_hash,
+)
+
+
+def Aggregate(pms):
+ """
+ Aggregates an array of triples (public key, message, signature) into a single aggregate signature.
+
+ :param pms: An array of triples (public key, message, signature).
+ :return: The aggregate signature.
+ """
+
+ # Let aggsig = bytes(0)
+ aggsig = bytes([0] * 32)
+
+ pm_aggd = []
+
+ # Return IncAggregate(aggsig, pms0..u-1); fail if that fails.
+ return IncAggregate(aggsig, pm_aggd, pms)
+
+
+# IncAggregate(aggsig, pm_aggd0..v-1, pms_to_agg0..u-1)
+def IncAggregate(aggsig, pm_aggd, pms_to_agg):
+ """
+ Incrementally aggregates an additional array of triples (public key, message, signature)
+ into an existing aggregate signature.
+
+ :param aggsig: A byte array representing the aggregate signature.
+ :param pm_aggd: An array of tuples (public key, message).
+ :param pms_to_agg: An array of triples (public key, message, signature).
+ :return: The new aggregate signature.
+ """
+
+ v = len(pm_aggd)
+ u = len(pms_to_agg)
+
+ # Fail if len(aggsig) ≠ 32 * (v + 1)
+ if len(aggsig) != 32 * (v + 1):
+ raise ValueError("Length of aggsig must be 32 * (v + 1)")
+
+ r_values = []
+ pmr_to_agg = []
+
+ # For i = 0 .. v-1:
+ for i in range(v):
+ # Let (pki, mi) = pm_aggdi
+ (pki, mi) = pm_aggd[i]
+
+ # Let ri = aggsig[i⋅32:(i+1)⋅32]
+ ri = aggsig[i * 32:(i + 1) * 32]
+ r_values.append(ri)
+
+ pmr_to_agg.append((pki, mi, ri))
+
+ z_values = []
+ s_values = []
+
+ # For i = v .. v+u-1:
+ for i in range(v, v + u):
+ # Let (pki, mi, sigi) = pms_to_aggi-v
+ (pki, mi, sigi) = pms_to_agg[i - v]
+
+ # Let ri = sigi[0:32]
+ ri = sigi[0:32]
+ r_values.append(ri)
+
+ # Let si = int(sigi[32:64]); fail if si ≥ n
+ si = Scalar.from_bytes_checked(sigi[32:64])
+ s_values.append(si)
+
+ # If i = 0:
+ # Let zi = 1
+ # Else:
+ # Let zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n
+ pmr_to_agg += [(pki, mi, sigi[0:32]) for (pki, mi, sigi) in pms_to_agg]
+ z_values.append(hashHalfAgg_randomizer(pmr_to_agg, i))
+
+ # Let s = int(aggsig[(v⋅32:(v+1)⋅32]) + zv⋅sv + ... + zv+u-1⋅sv+u-1 mod n
+ s = Scalar.from_bytes_checked(aggsig[v * 32:(v + 1) * 32])
+
+ for i in range(u):
+ s = s + z_values[i] * s_values[i]
+
+ # Return r0 || ... || rv+u-1 || bytes(s)
+ return b''.join(r_values) + s.to_bytes()
+
+
+# VerifyAggregate(aggsig, pm_aggd0..u-1)
+def VerifyAggregate(aggsig, pm_aggd):
+ """
+ Verifies an aggregate signature against an array of public key and message tuples.
+
+ :param aggsig: A byte array representing the aggregate signature.
+ :param pm_aggd: An array of tuples (public key, message).
+ :return: Boolean indicating whether the verification is successful.
+ """
+
+ u = len(pm_aggd)
+
+ # Fail if len(aggsig) ≠ 32 * (u + 1)
+ if len(aggsig) != 32 * (u + 1):
+ raise ValueError("Length of aggsig must be 32 * (u + 1)")
+
+ z_values = []
+ R_values = []
+ P_values = []
+ e_values = []
+ r_values = []
+
+ # For i = 0 .. u-1:
+ for i in range(u):
+ # Let (pki, mi) = pm_aggdi
+ (pki, mi) = pm_aggd[i]
+
+ # Let Pi = lift_x(int(pki)); fail if that fails
+ try:
+ Pi = GE.from_bytes_xonly(pki)
+ except ValueError:
+ return False
+ P_values.append(Pi)
+
+ # Let ri = aggsig[i⋅32:(i+1)⋅32]
+ ri = aggsig[i * 32:(i + 1) * 32]
+ # Let Ri = lift_x(int(ri)); fail if that fails
+ try:
+ Ri = GE.from_bytes_xonly(ri)
+ except ValueError:
+ return False
+ R_values.append(Ri)
+ r_values.append(ri)
+
+ # Let ei = int(hashBIP0340/challenge(bytes(ri) || pki || mi)) mod n
+ ei = Scalar.from_bytes_wrapping(hashBIP0340_challenge(ri, pki, mi))
+ e_values.append(ei)
+
+ # If i = 0:
+ # Let zi = 1
+ # Else:
+ # Let zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n
+ pmr = [(pki, mi, ri) for (pki, mi), ri in zip(pm_aggd, r_values)]
+ z_values.append(hashHalfAgg_randomizer(pmr, i))
+
+ # Let s = int(aggsig[u⋅32:(u+1)⋅32]); fail if s ≥ n
+ try:
+ s = Scalar.from_bytes_checked(aggsig[u * 32:(u + 1) * 32])
+ except ValueError:
+ return False
+
+ # Fail if s⋅G ≠ z0⋅(R0 + e0⋅P0) + ... + zu-1⋅(Ru-1 + eu-1⋅Pu-1)
+ lhs = s * G
+ rhs = GE()
+ for i in range(u):
+ e = e_values[i]
+ P = P_values[i]
+ R = R_values[i]
+ rhsi = R + e * P
+
+ z = z_values[i]
+ rhsi = z * rhsi
+
+ rhs = rhs + rhsi
+
+ return lhs == rhs
+
+
+def hashBIP0340_challenge(sig, pubkey, msg):
+ return tagged_hash("BIP0340/challenge", sig + pubkey + msg)
+
+
+def hashHalfAgg_randomizer(pmr, index):
+ if index == 0:
+ return Scalar(1)
+
+ random_input = bytes()
+ for i in range(index + 1):
+ (pki, mi, ri) = pmr[i]
+ random_input += ri
+ random_input += pki
+ random_input += mi
+
+ return Scalar.from_bytes_wrapping(tagged_hash("HalfAgg/randomizer", random_input))
diff --git a/bip-0460/secp256k1lab/.github/workflows/main.yml b/bip-0460/secp256k1lab/.github/workflows/main.yml
new file mode 100644
index 0000000000..fb05230b3c
--- /dev/null
+++ b/bip-0460/secp256k1lab/.github/workflows/main.yml
@@ -0,0 +1,34 @@
+name: Tests
+on: [push, pull_request]
+jobs:
+ ruff:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v5
+ - run: uvx ruff check .
+ mypy:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install the latest version of uv, setup Python ${{ matrix.python-version }}
+ uses: astral-sh/setup-uv@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - run: uvx mypy .
+ unittest:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - run: python3 -m unittest
diff --git a/bip-0460/secp256k1lab/.gitignore b/bip-0460/secp256k1lab/.gitignore
new file mode 100644
index 0000000000..505a3b1ca2
--- /dev/null
+++ b/bip-0460/secp256k1lab/.gitignore
@@ -0,0 +1,10 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
diff --git a/bip-0460/secp256k1lab/.python-version b/bip-0460/secp256k1lab/.python-version
new file mode 100644
index 0000000000..2c0733315e
--- /dev/null
+++ b/bip-0460/secp256k1lab/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/bip-0460/secp256k1lab/CHANGELOG.md b/bip-0460/secp256k1lab/CHANGELOG.md
new file mode 100644
index 0000000000..4c756d3695
--- /dev/null
+++ b/bip-0460/secp256k1lab/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+#### Added
+ - Added new methods `Scalar.from_int_nonzero_checked` and `Scalar.from_bytes_nonzero_checked`
+ that ensure a constructed scalar is in the range `0 < s < N` (i.e. is non-zero and within the
+ group order) and throw a `ValueError` otherwise. This is e.g. useful for ensuring that newly
+ generated secret keys or nonces are valid without having to do the non-zero check manually.
+ The already existing methods `Scalar.from_int_checked` and `Scalar.from_bytes_checked` error
+ on overflow, but not on zero, i.e. they only ensure `0 <= s < N`.
+
+ - Added a new method `GE.from_bytes_compressed_with_infinity` to parse a compressed
+ public key (33 bytes) to a group element, where the all-zeros bytestring maps to the
+ point at infinity. This is the counterpart to the already existing serialization
+ method `GE.to_bytes_compressed_with_infinity`.
+
+## [1.0.0] - 2025-03-31
+
+Initial release.
diff --git a/bip-0460/secp256k1lab/COPYING b/bip-0460/secp256k1lab/COPYING
new file mode 100644
index 0000000000..e8f2163641
--- /dev/null
+++ b/bip-0460/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-0460/secp256k1lab/README.md b/bip-0460/secp256k1lab/README.md
new file mode 100644
index 0000000000..dbc9dbd04c
--- /dev/null
+++ b/bip-0460/secp256k1lab/README.md
@@ -0,0 +1,13 @@
+secp256k1lab
+============
+
+
+
+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-0460/secp256k1lab/pyproject.toml b/bip-0460/secp256k1lab/pyproject.toml
new file mode 100644
index 0000000000..68b927b384
--- /dev/null
+++ b/bip-0460/secp256k1lab/pyproject.toml
@@ -0,0 +1,34 @@
+[project]
+name = "secp256k1lab"
+version = "1.0.0"
+description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education"
+readme = "README.md"
+authors = [
+ { name = "Pieter Wuille", email = "pieter@wuille.net" },
+ { name = "Tim Ruffing", email = "me@real-or-random.org" },
+ { name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
+ { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
+]
+maintainers = [
+ { name = "Tim Ruffing", email = "me@real-or-random.org" },
+ { name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
+ { name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
+]
+requires-python = ">=3.11"
+license = "MIT"
+license-files = ["COPYING"]
+keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Education",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Security :: Cryptography",
+]
+dependencies = []
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/bip-0460/secp256k1lab/src/secp256k1lab/__init__.py b/bip-0460/secp256k1lab/src/secp256k1lab/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bip-0460/secp256k1lab/src/secp256k1lab/bip340.py b/bip-0460/secp256k1lab/src/secp256k1lab/bip340.py
new file mode 100644
index 0000000000..ba839d16e1
--- /dev/null
+++ b/bip-0460/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-0460/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0460/secp256k1lab/src/secp256k1lab/ecdh.py
new file mode 100644
index 0000000000..73f47fa1a7
--- /dev/null
+++ b/bip-0460/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-0460/secp256k1lab/src/secp256k1lab/keys.py b/bip-0460/secp256k1lab/src/secp256k1lab/keys.py
new file mode 100644
index 0000000000..3e28897e99
--- /dev/null
+++ b/bip-0460/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-0460/secp256k1lab/src/secp256k1lab/py.typed b/bip-0460/secp256k1lab/src/secp256k1lab/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bip-0460/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0460/secp256k1lab/src/secp256k1lab/secp256k1.py
new file mode 100644
index 0000000000..0526878d91
--- /dev/null
+++ b/bip-0460/secp256k1lab/src/secp256k1lab/secp256k1.py
@@ -0,0 +1,483 @@
+# Copyright (c) 2022-2023 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""Test-only implementation of low-level secp256k1 field and group arithmetic
+
+It is designed for ease of understanding, not performance.
+
+WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for
+anything but tests.
+
+Exports:
+* FE: class for secp256k1 field elements
+* GE: class for secp256k1 group elements
+* G: the secp256k1 generator point
+"""
+
+from __future__ import annotations
+from typing import Self
+
+# TODO Docstrings of methods still say "field element"
+class APrimeFE:
+ """Objects of this class represent elements of a prime field.
+
+ They are represented internally in numerator / denominator form, in order to delay inversions.
+ """
+
+ # The size of the field (also its modulus and characteristic).
+ SIZE: int
+
+ def __init__(self, a: int | Self = 0, b: int | Self = 1) -> None:
+ """Initialize a field element a/b; both a and b can be ints or field elements."""
+ if isinstance(a, type(self)):
+ num = a._num
+ den = a._den
+ else:
+ assert isinstance(a, int)
+ num = a % self.SIZE
+ den = 1
+ if isinstance(b, type(self)):
+ den = (den * b._num) % self.SIZE
+ num = (num * b._den) % self.SIZE
+ else:
+ assert isinstance(b, int)
+ den = (den * b) % self.SIZE
+ assert den != 0
+ if num == 0:
+ den = 1
+ self._num: int = num
+ self._den: int = den
+
+ def __add__(self, a: int | Self) -> Self:
+ """Compute the sum of two field elements (second may be int)."""
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._den + self._den * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num + self._den * a, self._den)
+ return NotImplemented
+
+ def __radd__(self, a: int) -> Self:
+ """Compute the sum of an integer and a field element."""
+ return type(self)(a) + self
+
+ @classmethod
+ def sum(cls, *es: Self) -> Self:
+ """Compute the sum of field elements.
+
+ sum(a, b, c, ...) is identical to (0 + a + b + c + ...)."""
+ return sum(es, start=cls(0))
+
+ def __sub__(self, a: int | Self) -> Self:
+ """Compute the difference of two field elements (second may be int)."""
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._den - self._den * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num - self._den * a, self._den)
+ return NotImplemented
+
+ def __rsub__(self, a: int) -> Self:
+ """Compute the difference of an integer and a field element."""
+ return type(self)(a) - self
+
+ def __mul__(self, a: int | Self) -> Self:
+ """Compute the product of two field elements (second may be int)."""
+ if isinstance(a, type(self)):
+ return type(self)(self._num * a._num, self._den * a._den)
+ if isinstance(a, int):
+ return type(self)(self._num * a, self._den)
+ return NotImplemented
+
+ def __rmul__(self, a: int) -> Self:
+ """Compute the product of an integer with a field element."""
+ return type(self)(a) * self
+
+ def __truediv__(self, a: int | Self) -> Self:
+ """Compute the ratio of two field elements (second may be int)."""
+ if isinstance(a, type(self)) or isinstance(a, int):
+ return type(self)(self, a)
+ return NotImplemented
+
+ def __pow__(self, a: int) -> Self:
+ """Raise a field element to an integer power."""
+ return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE))
+
+ def __neg__(self) -> Self:
+ """Negate a field element."""
+ return type(self)(-self._num, self._den)
+
+ def __int__(self) -> int:
+ """Convert a field element to an integer in range 0..SIZE-1. The result is cached."""
+ if self._den != 1:
+ self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE
+ self._den = 1
+ return self._num
+
+ def sqrt(self) -> Self | None:
+ """Compute the square root of a field element if it exists (None otherwise)."""
+ raise NotImplementedError
+
+ def is_square(self) -> bool:
+ """Determine if this field element has a square root."""
+ # A more efficient algorithm is possible here (Jacobi symbol).
+ return self.sqrt() is not None
+
+ def is_even(self) -> bool:
+ """Determine whether this field element, represented as integer in 0..SIZE-1, is even."""
+ return int(self) & 1 == 0
+
+ def __eq__(self, a: object) -> bool:
+ """Check whether two field elements are equal (second may be an int)."""
+ if isinstance(a, type(self)):
+ return (self._num * a._den - self._den * a._num) % self.SIZE == 0
+ elif isinstance(a, int):
+ return (self._num - self._den * a) % self.SIZE == 0
+ return False # for other types
+
+ def to_bytes(self) -> bytes:
+ """Convert a field element to a 32-byte array (BE byte order)."""
+ return int(self).to_bytes(32, 'big')
+
+ @classmethod
+ def from_int_checked(cls, v: int) -> Self:
+ """Convert an integer to a field element (no overflow allowed)."""
+ if v >= cls.SIZE:
+ raise ValueError
+ return cls(v)
+
+ @classmethod
+ def from_int_wrapping(cls, v: int) -> Self:
+ """Convert an integer to a field element (reduced modulo SIZE)."""
+ return cls(v % cls.SIZE)
+
+ @classmethod
+ def from_bytes_checked(cls, b: bytes) -> Self:
+ """Convert a 32-byte array to a field element (BE byte order, no overflow allowed)."""
+ v = int.from_bytes(b, 'big')
+ return cls.from_int_checked(v)
+
+ @classmethod
+ def from_bytes_wrapping(cls, b: bytes) -> Self:
+ """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE)."""
+ v = int.from_bytes(b, 'big')
+ return cls.from_int_wrapping(v)
+
+ def __str__(self) -> str:
+ """Convert this field element to a 64 character hex string."""
+ return f"{int(self):064x}"
+
+ def __repr__(self) -> str:
+ """Get a string representation of this field element."""
+ return f"{type(self).__qualname__}(0x{int(self):x})"
+
+
+class FE(APrimeFE):
+ SIZE = 2**256 - 2**32 - 977
+
+ def sqrt(self) -> Self | None:
+ # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks
+ # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply
+ # raising the argument to the power (p + 1) / 4.
+
+ # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group,
+ # and thus only half of the non-zero field elements are squares. An element a is
+ # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're
+ # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent
+ # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to
+ # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.
+ v = int(self)
+ s = pow(v, (self.SIZE + 1) // 4, self.SIZE)
+ if s**2 % self.SIZE == v:
+ return type(self)(s)
+ return None
+
+
+class Scalar(APrimeFE):
+ """TODO Docstring"""
+ SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
+
+ @classmethod
+ def from_int_nonzero_checked(cls, v: int) -> Self:
+ """Convert an integer to a scalar (no zero or overflow allowed)."""
+ if not (0 < v < cls.SIZE):
+ raise ValueError
+ return cls(v)
+
+ @classmethod
+ def from_bytes_nonzero_checked(cls, b: bytes) -> Self:
+ """Convert a 32-byte array to a scalar (BE byte order, no zero or overflow allowed)."""
+ v = int.from_bytes(b, 'big')
+ return cls.from_int_nonzero_checked(v)
+
+
+class GE:
+ """Objects of this class represent secp256k1 group elements (curve points or infinity)
+
+ GE objects are immutable.
+
+ Normal points on the curve have fields:
+ * x: the x coordinate (a field element)
+ * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7)
+ * infinity: False
+
+ The point at infinity has field:
+ * infinity: True
+ """
+
+ # TODO The following two class attributes should probably be just getters as
+ # classmethods to enforce immutability. Unfortunately Python makes it hard
+ # to create "classproperties". `G` could then also be just a classmethod.
+
+ # Order of the group (number of points on the curve, plus 1 for infinity)
+ ORDER = Scalar.SIZE
+
+ # Number of valid distinct x coordinates on the curve.
+ ORDER_HALF = ORDER // 2
+
+ @property
+ def infinity(self) -> bool:
+ """Whether the group element is the point at infinity."""
+ return self._infinity
+
+ @property
+ def x(self) -> FE:
+ """The x coordinate (a field element) of a non-infinite group element."""
+ assert not self.infinity
+ return self._x
+
+ @property
+ def y(self) -> FE:
+ """The y coordinate (a field element) of a non-infinite group element."""
+ assert not self.infinity
+ return self._y
+
+ def __init__(self, x: int | FE | None = None, y: int | FE | None = None) -> None:
+ """Initialize a group element with specified x and y coordinates, or infinity."""
+ if x is None:
+ # Initialize as infinity.
+ assert y is None
+ self._infinity = True
+ else:
+ # Initialize as point on the curve (and check that it is).
+ assert x is not None
+ assert y is not None
+ fx = FE(x)
+ fy = FE(y)
+ assert fy**2 == fx**3 + 7
+ self._infinity = False
+ self._x = fx
+ self._y = fy
+
+ def __add__(self, a: GE) -> GE:
+ """Add two group elements together."""
+ # Deal with infinity: a + infinity == infinity + a == a.
+ if self.infinity:
+ return a
+ if a.infinity:
+ return self
+ if self.x == a.x:
+ if self.y != a.y:
+ # A point added to its own negation is infinity.
+ assert self.y + a.y == 0
+ return GE()
+ else:
+ # For identical inputs, use the tangent (doubling formula).
+ lam = (3 * self.x**2) / (2 * self.y)
+ else:
+ # For distinct inputs, use the line through both points (adding formula).
+ lam = (self.y - a.y) / (self.x - a.x)
+ # Determine point opposite to the intersection of that line with the curve.
+ x = lam**2 - (self.x + a.x)
+ y = lam * (self.x - x) - self.y
+ return GE(x, y)
+
+ @staticmethod
+ def sum(*ps: GE) -> GE:
+ """Compute the sum of group elements.
+
+ GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...)."""
+ return sum(ps, start=GE())
+
+ @staticmethod
+ def batch_mul(*aps: tuple[Scalar, GE]) -> GE:
+ """Compute a (batch) scalar group element multiplication.
+
+ GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3,
+ but more efficient."""
+ # Reduce all the scalars modulo order first (so we can deal with negatives etc).
+ naps = [(int(a), p) for a, p in aps]
+ # Start with point at infinity.
+ r = GE()
+ # Iterate over all bit positions, from high to low.
+ for i in range(255, -1, -1):
+ # Double what we have so far.
+ r = r + r
+ # Add then add the points for which the corresponding scalar bit is set.
+ for (a, p) in naps:
+ if (a >> i) & 1:
+ r += p
+ return r
+
+ def __rmul__(self, a: int | Scalar) -> GE:
+ """Multiply an integer or scalar with a group element."""
+ if self == G:
+ return FAST_G.mul(Scalar(a))
+ return GE.batch_mul((Scalar(a), self))
+
+ def __neg__(self) -> GE:
+ """Compute the negation of a group element."""
+ if self.infinity:
+ return self
+ return GE(self.x, -self.y)
+
+ def __sub__(self, a: GE) -> GE:
+ """Subtract a group element from another."""
+ return self + (-a)
+
+ def __eq__(self, a: object) -> bool:
+ """Check if two group elements are equal."""
+ if not isinstance(a, type(self)):
+ return False
+ return (self - a).infinity
+
+ def has_even_y(self) -> bool:
+ """Determine whether a non-infinity group element has an even y coordinate."""
+ assert not self.infinity
+ return self.y.is_even()
+
+ def to_bytes_compressed(self) -> bytes:
+ """Convert a non-infinite group element to 33-byte compressed encoding."""
+ assert not self.infinity
+ return bytes([3 - self.y.is_even()]) + self.x.to_bytes()
+
+ def to_bytes_compressed_with_infinity(self) -> bytes:
+ """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros."""
+ if self.infinity:
+ return 33 * b"\x00"
+ return self.to_bytes_compressed()
+
+ def to_bytes_uncompressed(self) -> bytes:
+ """Convert a non-infinite group element to 65-byte uncompressed encoding."""
+ assert not self.infinity
+ return b'\x04' + self.x.to_bytes() + self.y.to_bytes()
+
+ def to_bytes_xonly(self) -> bytes:
+ """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding."""
+ assert not self.infinity
+ return self.x.to_bytes()
+
+ @staticmethod
+ def lift_x(x: int | FE) -> GE:
+ """Return group element with specified field element as x coordinate (and even y)."""
+ y = (FE(x)**3 + 7).sqrt()
+ if y is None:
+ raise ValueError
+ if not y.is_even():
+ y = -y
+ return GE(x, y)
+
+ @staticmethod
+ def from_bytes_compressed(b: bytes) -> GE:
+ """Convert a compressed to a group element."""
+ assert len(b) == 33
+ if b[0] != 2 and b[0] != 3:
+ raise ValueError
+ x = FE.from_bytes_checked(b[1:])
+ r = GE.lift_x(x)
+ if b[0] == 3:
+ r = -r
+ return r
+
+ @staticmethod
+ def from_bytes_compressed_with_infinity(b: bytes) -> GE:
+ """Convert a compressed to a group element, mapping zeros to infinity."""
+ if b == 33 * b"\x00":
+ return GE()
+ else:
+ return GE.from_bytes_compressed(b)
+
+ @staticmethod
+ def from_bytes_uncompressed(b: bytes) -> GE:
+ """Convert an uncompressed to a group element."""
+ assert len(b) == 65
+ if b[0] != 4:
+ raise ValueError
+ x = FE.from_bytes_checked(b[1:33])
+ y = FE.from_bytes_checked(b[33:])
+ if y**2 != x**3 + 7:
+ raise ValueError
+ return GE(x, y)
+
+ @staticmethod
+ def from_bytes(b: bytes) -> GE:
+ """Convert a compressed or uncompressed encoding to a group element."""
+ assert len(b) in (33, 65)
+ if len(b) == 33:
+ return GE.from_bytes_compressed(b)
+ else:
+ return GE.from_bytes_uncompressed(b)
+
+ @staticmethod
+ def from_bytes_xonly(b: bytes) -> GE:
+ """Convert a point given in xonly encoding to a group element."""
+ assert len(b) == 32
+ x = FE.from_bytes_checked(b)
+ r = GE.lift_x(x)
+ return r
+
+ @staticmethod
+ def is_valid_x(x: int | FE) -> bool:
+ """Determine whether the provided field element is a valid X coordinate."""
+ return (FE(x)**3 + 7).is_square()
+
+ def __str__(self) -> str:
+ """Convert this group element to a string."""
+ if self.infinity:
+ return "(inf)"
+ return f"({self.x},{self.y})"
+
+ def __repr__(self) -> str:
+ """Get a string representation for this group element."""
+ if self.infinity:
+ return "GE()"
+ return f"GE(0x{int(self.x):x},0x{int(self.y):x})"
+
+ def __hash__(self) -> int:
+ """Compute a non-cryptographic hash of the group element."""
+ if self.infinity:
+ return 0 # 0 is not a valid x coordinate
+ return int(self.x)
+
+
+# The secp256k1 generator point
+G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798)
+
+
+class FastGEMul:
+ """Table for fast multiplication with a constant group element.
+
+ Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with
+ its powers of 2:
+
+ table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P]
+
+ During multiplication, the points corresponding to each bit set in the scalar are added up,
+ i.e. on average ~128 point additions take place.
+ """
+
+ def __init__(self, p: GE) -> None:
+ self.table: list[GE] = [p] # table[i] = (2^i) * p
+ for _ in range(255):
+ p = p + p
+ self.table.append(p)
+
+ def mul(self, a: Scalar | int) -> GE:
+ result = GE()
+ a_ = int(a)
+ for bit in range(a_.bit_length()):
+ if a_ & (1 << bit):
+ result += self.table[bit]
+ return result
+
+# Precomputed table with multiples of G for fast multiplication
+FAST_G = FastGEMul(G)
diff --git a/bip-0460/secp256k1lab/src/secp256k1lab/util.py b/bip-0460/secp256k1lab/src/secp256k1lab/util.py
new file mode 100644
index 0000000000..d8c744b795
--- /dev/null
+++ b/bip-0460/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-0460/secp256k1lab/test/__init__.py b/bip-0460/secp256k1lab/test/__init__.py
new file mode 100644
index 0000000000..862ed6e21c
--- /dev/null
+++ b/bip-0460/secp256k1lab/test/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+import sys
+
+# Ensure secp256k1lab is found and can be imported directly
+sys.path.insert(0, str(Path(__file__).parent / "../src/"))
diff --git a/bip-0460/secp256k1lab/test/test_bip340.py b/bip-0460/secp256k1lab/test/test_bip340.py
new file mode 100644
index 0000000000..7fafad54bd
--- /dev/null
+++ b/bip-0460/secp256k1lab/test/test_bip340.py
@@ -0,0 +1,51 @@
+import csv
+from pathlib import Path
+from random import randbytes
+import unittest
+
+from secp256k1lab.bip340 import pubkey_gen, schnorr_sign, schnorr_verify
+
+
+class BIP340Tests(unittest.TestCase):
+ """Test schnorr signatures (BIP 340)."""
+
+ def test_correctness(self):
+ seckey = randbytes(32)
+ pubkey_xonly = pubkey_gen(seckey)
+ aux_rand = randbytes(32)
+ message = b'this is some arbitrary message'
+ signature = schnorr_sign(message, seckey, aux_rand)
+ success = schnorr_verify(message, pubkey_xonly, signature)
+ self.assertTrue(success)
+
+ def test_vectors(self):
+ # Test against vectors from the BIPs repository
+ # [https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv]
+ vectors_file = Path(__file__).parent / "vectors" / "bip340.csv"
+ with open(vectors_file, encoding='utf8') as csvfile:
+ reader = csv.DictReader(csvfile)
+ for row in reader:
+ with self.subTest(i=int(row['index'])):
+ self.subtest_vectors_case(row)
+
+ def subtest_vectors_case(self, row):
+ seckey = bytes.fromhex(row['secret key'])
+ pubkey_xonly = bytes.fromhex(row['public key'])
+ aux_rand = bytes.fromhex(row['aux_rand'])
+ msg = bytes.fromhex(row['message'])
+ sig = bytes.fromhex(row['signature'])
+ result_str = row['verification result']
+ comment = row['comment']
+
+ result = result_str == 'TRUE'
+ assert result or result_str == 'FALSE'
+ if seckey != b'':
+ pubkey_xonly_actual = pubkey_gen(seckey)
+ self.assertEqual(pubkey_xonly.hex(), pubkey_xonly_actual.hex(), f"BIP340 test vector ({comment}): pubkey mismatch")
+ sig_actual = schnorr_sign(msg, seckey, aux_rand)
+ self.assertEqual(sig.hex(), sig_actual.hex(), f"BIP340 test vector ({comment}): sig mismatch")
+ result_actual = schnorr_verify(msg, pubkey_xonly, sig)
+ if result:
+ self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification failed unexpectedly")
+ else:
+ self.assertEqual(result, result_actual, f"BIP340 test vector ({comment}): verification succeeded unexpectedly")
diff --git a/bip-0460/secp256k1lab/test/test_ecdh.py b/bip-0460/secp256k1lab/test/test_ecdh.py
new file mode 100644
index 0000000000..63c9da7a1b
--- /dev/null
+++ b/bip-0460/secp256k1lab/test/test_ecdh.py
@@ -0,0 +1,18 @@
+from random import randbytes
+import unittest
+
+from secp256k1lab.ecdh import ecdh_libsecp256k1
+from secp256k1lab.keys import pubkey_gen_plain
+
+
+class ECDHTests(unittest.TestCase):
+ """Test ECDH module."""
+
+ def test_correctness(self):
+ seckey_alice = randbytes(32)
+ pubkey_alice = pubkey_gen_plain(seckey_alice)
+ seckey_bob = randbytes(32)
+ pubkey_bob = pubkey_gen_plain(seckey_bob)
+ shared_secret1 = ecdh_libsecp256k1(seckey_alice, pubkey_bob)
+ shared_secret2 = ecdh_libsecp256k1(seckey_bob, pubkey_alice)
+ self.assertEqual(shared_secret1, shared_secret2)
diff --git a/bip-0460/secp256k1lab/test/test_secp256k1.py b/bip-0460/secp256k1lab/test/test_secp256k1.py
new file mode 100644
index 0000000000..c6aee19a0a
--- /dev/null
+++ b/bip-0460/secp256k1lab/test/test_secp256k1.py
@@ -0,0 +1,180 @@
+"""Test low-level secp256k1 field and group arithmetic classes."""
+from random import randint
+import unittest
+
+from secp256k1lab.secp256k1 import FE, G, GE, Scalar
+
+
+class PrimeFieldTests(unittest.TestCase):
+ def test_fe_constructors(self):
+ P = FE.SIZE
+ random_fe_valid = randint(0, P-1)
+ random_fe_overflowing = randint(P, 2**256-1)
+
+ # wrapping constructors
+ for init_value in [0, P-1, P, P+1, random_fe_valid, random_fe_overflowing]:
+ fe1 = FE(init_value)
+ fe2 = FE.from_int_wrapping(init_value)
+ fe3 = FE.from_bytes_wrapping(init_value.to_bytes(32, 'big'))
+ reduced_value = init_value % P
+ self.assertEqual(int(fe1), reduced_value)
+ self.assertEqual(int(fe1), int(fe2))
+ self.assertEqual(int(fe2), int(fe3))
+
+ # checking constructors (should throw on overflow)
+ for valid_value in [0, P-1, random_fe_valid]:
+ fe1 = FE.from_int_checked(valid_value)
+ fe2 = FE.from_bytes_checked(valid_value.to_bytes(32, 'big'))
+ self.assertEqual(int(fe1), valid_value)
+ self.assertEqual(int(fe1), int(fe2))
+
+ for overflow_value in [P, P+1, random_fe_overflowing]:
+ with self.assertRaises(ValueError):
+ _ = FE.from_int_checked(overflow_value)
+ with self.assertRaises(ValueError):
+ _ = FE.from_bytes_checked(overflow_value.to_bytes(32, 'big'))
+
+ def test_scalar_constructors(self):
+ N = Scalar.SIZE
+ random_scalar_valid = randint(0, N-1)
+ random_scalar_overflowing = randint(N, 2**256-1)
+
+ # wrapping constructors
+ for init_value in [0, N-1, N, N+1, random_scalar_valid, random_scalar_overflowing]:
+ s1 = Scalar(init_value)
+ s2 = Scalar.from_int_wrapping(init_value)
+ s3 = Scalar.from_bytes_wrapping(init_value.to_bytes(32, 'big'))
+ reduced_value = init_value % N
+ self.assertEqual(int(s1), reduced_value)
+ self.assertEqual(int(s1), int(s2))
+ self.assertEqual(int(s2), int(s3))
+
+ # checking constructors (should throw on overflow)
+ for valid_value in [0, N-1, random_scalar_valid]:
+ s1 = Scalar.from_int_checked(valid_value)
+ s2 = Scalar.from_bytes_checked(valid_value.to_bytes(32, 'big'))
+ self.assertEqual(int(s1), valid_value)
+ self.assertEqual(int(s1), int(s2))
+
+ for overflow_value in [N, N+1, random_scalar_overflowing]:
+ with self.assertRaises(ValueError):
+ _ = Scalar.from_int_checked(overflow_value)
+ with self.assertRaises(ValueError):
+ _ = Scalar.from_bytes_checked(overflow_value.to_bytes(32, 'big'))
+
+ # non-zero checking constructors (should throw on zero or overflow, only for Scalar)
+ random_nonzero_scalar_valid = randint(1, N-1)
+ for valid_value in [1, N-1, random_nonzero_scalar_valid]:
+ s1 = Scalar.from_int_nonzero_checked(valid_value)
+ s2 = Scalar.from_bytes_nonzero_checked(valid_value.to_bytes(32, 'big'))
+ self.assertEqual(int(s1), valid_value)
+ self.assertEqual(int(s1), int(s2))
+
+ for invalid_value in [0, N, random_scalar_overflowing]:
+ with self.assertRaises(ValueError):
+ _ = Scalar.from_int_nonzero_checked(invalid_value)
+ with self.assertRaises(ValueError):
+ _ = Scalar.from_bytes_nonzero_checked(invalid_value.to_bytes(32, 'big'))
+
+
+class GeSerializationTests(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.point_at_infinity = GE()
+ cls.group_elements_on_curve = [
+ # generator point
+ G,
+ # Bitcoin genesis block public key
+ GE(0x678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6,
+ 0x49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f),
+ ]
+ # generate a few random points, to likely cover both even/odd y polarity
+ cls.group_elements_on_curve.extend([randint(1, Scalar.SIZE-1) * G for _ in range(8)])
+ # generate x coordinates that don't have a valid point on the curve
+ # (note that ~50% of all x coordinates are valid, so finding one needs two loop iterations on average)
+ cls.x_coords_not_on_curve = []
+ while len(cls.x_coords_not_on_curve) < 8:
+ x = randint(0, FE.SIZE-1)
+ if not GE.is_valid_x(x):
+ cls.x_coords_not_on_curve.append(x)
+
+ cls.group_elements = [cls.point_at_infinity] + cls.group_elements_on_curve
+
+ def test_infinity_raises(self):
+ with self.assertRaises(AssertionError):
+ _ = self.point_at_infinity.to_bytes_uncompressed()
+ with self.assertRaises(AssertionError):
+ _ = self.point_at_infinity.to_bytes_compressed()
+ with self.assertRaises(AssertionError):
+ _ = self.point_at_infinity.to_bytes_xonly()
+
+ def test_not_on_curve_raises(self):
+ # for compressed and x-only GE deserialization, test with invalid x coordinate
+ for x in self.x_coords_not_on_curve:
+ x_bytes = x.to_bytes(32, 'big')
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_compressed(b'\x02' + x_bytes)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_compressed(b'\x03' + x_bytes)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_compressed_with_infinity(b'\x02' + x_bytes)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_compressed_with_infinity(b'\x03' + x_bytes)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_xonly(x_bytes)
+
+ # for uncompressed GE serialization, test by invalidating either coordinate
+ for ge in self.group_elements_on_curve:
+ valid_x = ge.x
+ valid_y = ge.y
+ invalid_x = ge.x + 1
+ invalid_y = ge.y + 1
+
+ # valid cases (if point (x,y) is on the curve, then point(x,-y) is on the curve as well)
+ _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + valid_y.to_bytes())
+ _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + (-valid_y).to_bytes())
+ # invalid cases (curve equation y**2 = x**3 + 7 doesn't hold)
+ self.assertNotEqual(invalid_y**2, valid_x**3 + 7)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_uncompressed(b'\x04' + valid_x.to_bytes() + invalid_y.to_bytes())
+ self.assertNotEqual(valid_y**2, invalid_x**3 + 7)
+ with self.assertRaises(ValueError):
+ _ = GE.from_bytes_uncompressed(b'\x04' + invalid_x.to_bytes() + valid_y.to_bytes())
+
+ def test_affine(self):
+ # GE serialization and parsing round-trip (variants that only support serializing points on the curve)
+ for ge_orig in self.group_elements_on_curve:
+ # uncompressed serialization: 65 bytes, starts with 0x04
+ ge_ser = ge_orig.to_bytes_uncompressed()
+ self.assertEqual(len(ge_ser), 65)
+ self.assertEqual(ge_ser[0], 0x04)
+ ge_deser = GE.from_bytes_uncompressed(ge_ser)
+ self.assertEqual(ge_deser, ge_orig)
+
+ # compressed serialization: 33 bytes, starts with 0x02 (if y is even) or 0x03 (if y is odd)
+ ge_ser = ge_orig.to_bytes_compressed()
+ self.assertEqual(len(ge_ser), 33)
+ self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03)
+ ge_deser = GE.from_bytes_compressed(ge_ser)
+ self.assertEqual(ge_deser, ge_orig)
+
+ # x-only serialization: 32 bytes
+ ge_ser = ge_orig.to_bytes_xonly()
+ self.assertEqual(len(ge_ser), 32)
+ ge_deser = GE.from_bytes_xonly(ge_ser)
+ if not ge_orig.has_even_y(): # x-only implies even y, so flip if necessary
+ ge_deser = -ge_deser
+ self.assertEqual(ge_deser, ge_orig)
+
+ def test_affine_with_infinity(self):
+ # GE serialization and parsing round-trip (variants that also support serializing the point at infinity)
+ for ge_orig in self.group_elements:
+ # compressed serialization: 33 bytes, all-zeros for point at infinity
+ ge_ser = ge_orig.to_bytes_compressed_with_infinity()
+ self.assertEqual(len(ge_ser), 33)
+ if ge_orig.infinity:
+ self.assertEqual(ge_ser, b'\x00'*33)
+ else:
+ self.assertEqual(ge_ser[0], 0x02 if ge_orig.has_even_y() else 0x03)
+ ge_deser = GE.from_bytes_compressed_with_infinity(ge_ser)
+ self.assertEqual(ge_deser, ge_orig)
diff --git a/bip-0460/secp256k1lab/test/vectors/bip340.csv b/bip-0460/secp256k1lab/test/vectors/bip340.csv
new file mode 100644
index 0000000000..aa317a3b3d
--- /dev/null
+++ b/bip-0460/secp256k1lab/test/vectors/bip340.csv
@@ -0,0 +1,20 @@
+index,secret key,public key,aux_rand,message,signature,verification result,comment
+0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE,
+1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE,
+2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE,
+3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n
+4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE,
+5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve
+6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false
+7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message
+8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value
+9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0
+10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1
+11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve
+12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
+13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
+14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
+15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12)
+16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12)
+17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12)
+18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12)
diff --git a/bip-0460/test-vectors.py b/bip-0460/test-vectors.py
new file mode 100644
index 0000000000..cfbd8cf788
--- /dev/null
+++ b/bip-0460/test-vectors.py
@@ -0,0 +1,1266 @@
+#!/usr/bin/env python3
+"""
+Test vector generator for the CISA Taproot key path BIP (witness v2).
+
+Generates two files:
+ wallet-test-vectors.json scriptPubKey/address derivation, sighash
+ computation, and witness construction
+ consensus-test-vectors.json transaction level valid/invalid cases
+
+WARNING: All keys and nonces in this file are deterministic and publicly
+known. They exist only to make the vectors reproducible. Never use this
+code or these values in production.
+"""
+
+import hashlib
+import json
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
+sys.path.insert(0, str(Path(__file__).parent))
+
+from secp256k1lab.secp256k1 import G, GE, Scalar
+from secp256k1lab.bip340 import schnorr_sign, schnorr_verify
+from secp256k1lab.util import tagged_hash
+
+import halfagg
+import fullagg
+
+# Marker bytes
+MARKER_OPTOUT = 0xBB
+MARKER_HALFAGG = 0xBC
+MARKER_FULLAGG = 0xBD
+
+# Sighash epoch for witness v2 signature messages
+SIGHASH_EPOCH = 0x01
+
+SIGHASH_DEFAULT = 0x00
+SIGHASH_ALL = 0x01
+SIGHASH_NONE = 0x02
+SIGHASH_SINGLE = 0x03
+SIGHASH_ANYONECANPAY = 0x80
+
+AUX_ZERO = bytes(32)
+
+
+# ---------------------------------------------------------------------------
+# Serialization helpers
+# ---------------------------------------------------------------------------
+
+def sha256(b):
+ return hashlib.sha256(b).digest()
+
+
+def ser_compact_size(n):
+ if n < 253:
+ return bytes([n])
+ if n < 0x10000:
+ return b"\xfd" + n.to_bytes(2, "little")
+ if n < 0x100000000:
+ return b"\xfe" + n.to_bytes(4, "little")
+ return b"\xff" + n.to_bytes(8, "little")
+
+
+class TxIn:
+ def __init__(self, txid, vout, sequence=0xFFFFFFFF):
+ self.txid = txid # 32 bytes, internal byte order
+ self.vout = vout
+ self.sequence = sequence
+
+ def outpoint(self):
+ return self.txid + self.vout.to_bytes(4, "little")
+
+
+class TxOut:
+ def __init__(self, amount, script_pubkey):
+ self.amount = amount
+ self.script_pubkey = script_pubkey
+
+ def serialize(self):
+ return self.amount.to_bytes(8, "little") + ser_compact_size(
+ len(self.script_pubkey)
+ ) + self.script_pubkey
+
+
+class Tx:
+ def __init__(self, vin, vout, version=2, locktime=0):
+ self.version = version
+ self.locktime = locktime
+ self.vin = vin
+ self.vout = vout
+ self.witnesses = [[] for _ in vin]
+
+ def serialize_unsigned(self):
+ out = self.version.to_bytes(4, "little")
+ out += ser_compact_size(len(self.vin))
+ for txin in self.vin:
+ out += txin.outpoint() + b"\x00" + txin.sequence.to_bytes(4, "little")
+ out += ser_compact_size(len(self.vout))
+ for txout in self.vout:
+ out += txout.serialize()
+ out += self.locktime.to_bytes(4, "little")
+ return out
+
+ def serialize_signed(self):
+ out = self.version.to_bytes(4, "little")
+ out += b"\x00\x01"
+ out += ser_compact_size(len(self.vin))
+ for txin in self.vin:
+ out += txin.outpoint() + b"\x00" + txin.sequence.to_bytes(4, "little")
+ out += ser_compact_size(len(self.vout))
+ for txout in self.vout:
+ out += txout.serialize()
+ for witness in self.witnesses:
+ out += ser_compact_size(len(witness))
+ for element in witness:
+ out += ser_compact_size(len(element)) + element
+ out += self.locktime.to_bytes(4, "little")
+ return out
+
+
+# ---------------------------------------------------------------------------
+# Taproot style key derivation
+# ---------------------------------------------------------------------------
+
+def tweak_keypair(seckey32):
+ d0 = Scalar.from_bytes_checked(seckey32)
+ P = d0 * G
+ d = d0 if P.has_even_y() else -d0
+ internal_pubkey = P.to_bytes_xonly()
+ t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_pubkey))
+ Q = (d + t) * G
+ tweaked_seckey = (d + t).to_bytes()
+ return internal_pubkey, t.to_bytes(), tweaked_seckey, Q.to_bytes_xonly()
+
+
+def v2_script_pubkey(output_key32):
+ return bytes([0x52, 0x20]) + output_key32
+
+
+# ---------------------------------------------------------------------------
+# Bech32m (adapted from the BIP 350 reference implementation)
+# ---------------------------------------------------------------------------
+
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+BECH32M_CONST = 0x2BC830A3
+
+
+def bech32_polymod(values):
+ generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1FFFFFF) << 5 ^ value
+ for i in range(5):
+ chk ^= generator[i] if ((top >> i) & 1) else 0
+ return chk
+
+
+def bech32_hrp_expand(hrp):
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def bech32m_create_checksum(hrp, data):
+ values = bech32_hrp_expand(hrp) + data
+ polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ BECH32M_CONST
+ return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
+
+
+def convertbits(data, frombits, tobits, pad=True):
+ acc = 0
+ bits = 0
+ ret = []
+ maxv = (1 << tobits) - 1
+ for value in data:
+ acc = (acc << frombits) | value
+ bits += frombits
+ while bits >= tobits:
+ bits -= tobits
+ ret.append((acc >> bits) & maxv)
+ if pad and bits:
+ ret.append((acc << (tobits - bits)) & maxv)
+ return ret
+
+
+def v2_address(output_key32, hrp="bc"):
+ data = [2] + convertbits(output_key32, 8, 5)
+ combined = data + bech32m_create_checksum(hrp, data)
+ return hrp + "1" + "".join([CHARSET[d] for d in combined])
+
+
+# ---------------------------------------------------------------------------
+# Witness v2 common signature message
+# ---------------------------------------------------------------------------
+
+def sigmsg_common(tx, spent_utxos, input_index, hash_type, ext_flag, annex=None):
+ """Compute SigMsg(hash_type, ext_flag) as defined in BIP 341."""
+ assert len(spent_utxos) == len(tx.vin)
+ anyonecanpay = bool(hash_type & SIGHASH_ANYONECANPAY)
+ base_type = hash_type & 3
+
+ msg = bytes([hash_type])
+ msg += tx.version.to_bytes(4, "little")
+ msg += tx.locktime.to_bytes(4, "little")
+ if not anyonecanpay:
+ msg += sha256(b"".join(txin.outpoint() for txin in tx.vin))
+ msg += sha256(b"".join(u.amount.to_bytes(8, "little") for u in spent_utxos))
+ msg += sha256(
+ b"".join(
+ ser_compact_size(len(u.script_pubkey)) + u.script_pubkey
+ for u in spent_utxos
+ )
+ )
+ msg += sha256(
+ b"".join(txin.sequence.to_bytes(4, "little") for txin in tx.vin)
+ )
+ if base_type not in (SIGHASH_NONE, SIGHASH_SINGLE):
+ msg += sha256(b"".join(txout.serialize() for txout in tx.vout))
+ spend_type = 2 * ext_flag + (1 if annex is not None else 0)
+ msg += bytes([spend_type])
+ if anyonecanpay:
+ txin = tx.vin[input_index]
+ utxo = spent_utxos[input_index]
+ msg += txin.outpoint()
+ msg += utxo.amount.to_bytes(8, "little")
+ msg += ser_compact_size(len(utxo.script_pubkey)) + utxo.script_pubkey
+ msg += txin.sequence.to_bytes(4, "little")
+ else:
+ msg += input_index.to_bytes(4, "little")
+ if annex is not None:
+ msg += sha256(ser_compact_size(len(annex)) + annex)
+ if base_type == SIGHASH_SINGLE:
+ msg += sha256(tx.vout[input_index].serialize())
+ return msg
+
+
+def sigmsg_v2(tx, spent_utxos, input_index, hash_type, agg_mode, annex=None):
+ """Compute hash_TapSighash(0x01 || agg_mode || SigMsg(hash_type, 0))."""
+ msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 0, annex)
+ return tagged_hash("TapSighash", bytes([SIGHASH_EPOCH, agg_mode]) + msg)
+
+
+def sigmsg_tapscript(tx, spent_utxos, input_index, hash_type, tapleaf_hash,
+ annex=None):
+ """Compute the unchanged BIP 342 tapscript signature message,
+ hash_TapSighash(0x00 || SigMsg(hash_type, 1) || ext)."""
+ msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 1, annex)
+ ext = tapleaf_hash + b"\x00" + b"\xff\xff\xff\xff"
+ return tagged_hash("TapSighash", bytes([0x00]) + msg + ext)
+
+
+def sigmsg_v1(tx, spent_utxos, input_index, hash_type, annex=None):
+ """Compute the unchanged BIP 341 key path signature message,
+ hash_TapSighash(0x00 || SigMsg(hash_type, 0))."""
+ msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 0, annex)
+ return tagged_hash("TapSighash", bytes([0x00]) + msg)
+
+
+# ---------------------------------------------------------------------------
+# Witness element construction
+# ---------------------------------------------------------------------------
+
+def marker_element(marker, hash_type=SIGHASH_DEFAULT, sig=b""):
+ element = bytes([marker])
+ if hash_type != SIGHASH_DEFAULT:
+ element += bytes([hash_type])
+ return element + sig
+
+
+# ---------------------------------------------------------------------------
+# Deterministic test data
+# ---------------------------------------------------------------------------
+
+def test_seckey(i):
+ return tagged_hash("CISA/test/key", bytes([i]))
+
+
+def test_prevout_txid(i):
+ return sha256(b"CISA vector prevout " + bytes([i]))
+
+
+def test_fullagg_secnonce(i):
+ # Test-only deterministic nonces. Nonces must be fresh uniform
+ # randomness in any real signing session.
+ r1 = Scalar.from_bytes_wrapping(tagged_hash("CISA/test/nonce", bytes([i, 0])))
+ r2 = Scalar.from_bytes_wrapping(tagged_hash("CISA/test/nonce", bytes([i, 1])))
+ return (r1, r2), (r1 * G, r2 * G)
+
+
+def test_garbage(label, length=32):
+ out = b""
+ i = 0
+ while len(out) < length:
+ out += tagged_hash("CISA/test/garbage", label + bytes([i]))
+ i += 1
+ return out[:length]
+
+
+def test_offcurve_x(label):
+ # Deterministically find 32 bytes that are not a valid x coordinate.
+ i = 0
+ while True:
+ cand = tagged_hash("CISA/test/offcurve", label + bytes([i]))
+ try:
+ GE.from_bytes_xonly(cand)
+ except ValueError:
+ return cand
+ i += 1
+
+
+# ---------------------------------------------------------------------------
+# Signing helpers
+# ---------------------------------------------------------------------------
+
+def sign_optout(tx, utxos, idx, tweaked_seckey, hash_type):
+ m = sigmsg_v2(tx, utxos, idx, hash_type, MARKER_OPTOUT)
+ sig = schnorr_sign(m, tweaked_seckey, AUX_ZERO)
+ return m, sig
+
+
+def sign_halfagg_group(tx, utxos, members):
+ """members: list of (input_index, tweaked_seckey, hash_type).
+ Returns (msgs, plain_sigs, aggsig)."""
+ triples = []
+ msgs = []
+ for idx, sk, ht in members:
+ m = sigmsg_v2(tx, utxos, idx, ht, MARKER_HALFAGG)
+ sig = schnorr_sign(m, sk, AUX_ZERO)
+ pk = utxos[idx].script_pubkey[2:]
+ triples.append((pk, m, sig))
+ msgs.append(m)
+ aggsig = halfagg.Aggregate(triples)
+ assert halfagg.VerifyAggregate(aggsig, [(pk, m) for pk, m, _ in triples])
+ return msgs, [t[2] for t in triples], aggsig
+
+
+def sign_fullagg_group(tx, utxos, members, msg_mode=MARKER_FULLAGG,
+ nonce_offset=0):
+ """members: list of (input_index, tweaked_seckey, hash_type).
+ msg_mode overrides the marker committed in the signature messages,
+ used by negative vectors exercising the mode commitment.
+ nonce_offset varies the deterministic test nonces between sessions.
+ Returns (msgs, secnonces, pubnonces, sig64)."""
+ pks = []
+ msgs = []
+ secnonces = []
+ pubnonces = []
+ for i, (idx, sk, ht) in enumerate(members):
+ m = sigmsg_v2(tx, utxos, idx, ht, msg_mode)
+ msgs.append(m)
+ pks.append(GE.from_bytes_xonly(utxos[idx].script_pubkey[2:]))
+ secnonce, pubnonce = test_fullagg_secnonce(i + nonce_offset)
+ secnonces.append(secnonce)
+ pubnonces.append(pubnonce)
+ aggnonce = fullagg.NonceAgg(pubnonces)
+ psigs = []
+ for i, (idx, sk, ht) in enumerate(members):
+ d = Scalar.from_bytes_checked(sk)
+ psig = fullagg.Sign(secnonces[i], d, msgs[i], aggnonce, pks, msgs, pubnonces)
+ psigs.append(psig)
+ R, s = fullagg.SigAgg(aggnonce, pks, msgs, pubnonces, psigs)
+ assert fullagg.Verify(pks, msgs, (R, s))
+ sig64 = R.to_bytes_xonly() + s.to_bytes()
+ return msgs, secnonces, pubnonces, sig64
+
+
+# ---------------------------------------------------------------------------
+# Vector construction
+# ---------------------------------------------------------------------------
+
+def hexlify(b):
+ return b.hex()
+
+
+def make_wallet_vectors():
+ vectors = {"scriptPubKey": [], "keyPathSpending": []}
+
+ # scriptPubKey and address derivation
+ for i in range(2):
+ seckey = test_seckey(i)
+ internal_pubkey, tweak, tweaked_seckey, output_key = tweak_keypair(seckey)
+ spk = v2_script_pubkey(output_key)
+ vectors["scriptPubKey"].append(
+ {
+ "given": {"internalPubkey": hexlify(internal_pubkey)},
+ "intermediary": {
+ "tweak": hexlify(tweak),
+ "tweakedPubkey": hexlify(output_key),
+ },
+ "expected": {
+ "scriptPubKey": hexlify(spk),
+ "address": v2_address(output_key),
+ },
+ }
+ )
+
+ # Case 1: opted-out input plus a two-member half-aggregation group
+ keys = [tweak_keypair(test_seckey(i)) for i in range(3)]
+ utxos = [
+ TxOut(100_000_000 + i * 1_000_000, v2_script_pubkey(k[3]))
+ for i, k in enumerate(keys)
+ ]
+ tx = Tx(
+ vin=[TxIn(test_prevout_txid(i), 0) for i in range(3)],
+ vout=[TxOut(299_000_000, v2_script_pubkey(keys[0][3]))],
+ )
+ m0, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_ALL)
+ msgs, _, aggsig = sign_halfagg_group(
+ tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT, SIGHASH_ALL, sig0)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG, sig=aggsig)]
+
+ case1 = {
+ "description": "Opted-out input with SIGHASH_ALL alongside a "
+ "two-member half-aggregation group with SIGHASH_DEFAULT",
+ "given": {
+ "rawUnsignedTx": hexlify(tx.serialize_unsigned()),
+ "utxosSpent": [
+ {"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
+ for u in utxos
+ ],
+ },
+ "inputSpending": [],
+ "auxiliary": {"aggregateSignature": hexlify(aggsig)},
+ "expected": {"rawSignedTx": hexlify(tx.serialize_signed())},
+ }
+ input_data = [
+ (0, MARKER_OPTOUT, SIGHASH_ALL, m0),
+ (1, MARKER_HALFAGG, SIGHASH_DEFAULT, msgs[0]),
+ (2, MARKER_HALFAGG, SIGHASH_DEFAULT, msgs[1]),
+ ]
+ for idx, marker, ht, m in input_data:
+ case1["inputSpending"].append(
+ {
+ "given": {
+ "txinIndex": idx,
+ "internalPrivkey": hexlify(test_seckey(idx)),
+ "marker": f"0x{marker:02x}",
+ "hashType": ht,
+ },
+ "intermediary": {
+ "internalPubkey": hexlify(keys[idx][0]),
+ "tweak": hexlify(keys[idx][1]),
+ "tweakedPrivkey": hexlify(keys[idx][2]),
+ "sigHash": hexlify(m),
+ },
+ "expected": {"witness": [hexlify(e) for e in tx.witnesses[idx]]},
+ }
+ )
+ vectors["keyPathSpending"].append(case1)
+
+ # Case 2: two-member full-aggregation group
+ keys = [tweak_keypair(test_seckey(i + 3)) for i in range(2)]
+ utxos = [
+ TxOut(50_000_000, v2_script_pubkey(keys[0][3])),
+ TxOut(70_000_000, v2_script_pubkey(keys[1][3])),
+ ]
+ tx = Tx(
+ vin=[TxIn(test_prevout_txid(i + 3), 1) for i in range(2)],
+ vout=[TxOut(119_000_000, v2_script_pubkey(keys[0][3]))],
+ )
+ msgs, secnonces, pubnonces, sig64 = sign_fullagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_ALL)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL, sig64)]
+
+ case2 = {
+ "description": "Two-member full-aggregation group with SIGHASH_ALL "
+ "(using deterministic nonces)",
+ "given": {
+ "rawUnsignedTx": hexlify(tx.serialize_unsigned()),
+ "utxosSpent": [
+ {"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
+ for u in utxos
+ ],
+ },
+ "inputSpending": [],
+ "auxiliary": {
+ "secnonces": [
+ hexlify(r1.to_bytes() + r2.to_bytes()) for r1, r2 in secnonces
+ ],
+ "pubnonces": [
+ hexlify(R1.to_bytes_compressed() + R2.to_bytes_compressed())
+ for R1, R2 in pubnonces
+ ],
+ "aggregateSignature": hexlify(sig64),
+ },
+ "expected": {"rawSignedTx": hexlify(tx.serialize_signed())},
+ }
+ for idx in range(2):
+ case2["inputSpending"].append(
+ {
+ "given": {
+ "txinIndex": idx,
+ "internalPrivkey": hexlify(test_seckey(idx + 3)),
+ "marker": f"0x{MARKER_FULLAGG:02x}",
+ "hashType": SIGHASH_ALL,
+ },
+ "intermediary": {
+ "internalPubkey": hexlify(keys[idx][0]),
+ "tweak": hexlify(keys[idx][1]),
+ "tweakedPrivkey": hexlify(keys[idx][2]),
+ "sigHash": hexlify(msgs[idx]),
+ },
+ "expected": {"witness": [hexlify(e) for e in tx.witnesses[idx]]},
+ }
+ )
+ vectors["keyPathSpending"].append(case2)
+
+ # Case 3: script path spend of a witness v2 output (BIP 341/342 rules
+ # unchanged). Same leaf and keys as the consensus scriptpath-valid
+ # case, so the two files cross-check.
+ script_seckey = test_seckey(20)
+ script_pk = (Scalar.from_bytes_checked(script_seckey) * G).to_bytes_xonly()
+ script = bytes([0x20]) + script_pk + bytes([0xAC]) # OP_CHECKSIG
+ tapleaf_hash = tagged_hash(
+ "TapLeaf", bytes([0xC0]) + ser_compact_size(len(script)) + script
+ )
+ internal_seckey = test_seckey(21)
+ internal_x = (Scalar.from_bytes_checked(internal_seckey) * G).to_bytes_xonly()
+ t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x + tapleaf_hash))
+ Q = GE.from_bytes_xonly(internal_x) + t * G
+ control = bytes([0xC0 | (0 if Q.has_even_y() else 1)]) + internal_x
+ spk = v2_script_pubkey(Q.to_bytes_xonly())
+ utxos = [TxOut(100_000_000, spk)]
+ dest_key = tweak_keypair(test_seckey(0))[3]
+ tx = Tx(
+ vin=[TxIn(test_prevout_txid(33), 0)],
+ vout=[TxOut(99_000_000, v2_script_pubkey(dest_key))],
+ )
+ m_script = sigmsg_tapscript(tx, utxos, 0, SIGHASH_DEFAULT, tapleaf_hash)
+ sig_script = schnorr_sign(m_script, script_seckey, AUX_ZERO)
+ assert schnorr_verify(m_script, script_pk, sig_script)
+ tx.witnesses[0] = [sig_script, script, control]
+
+ vectors["scriptPathSpending"] = [
+ {
+ "description": "Script path spend of a witness v2 output with a "
+ "single CHECKSIG leaf, following BIP 341/342 unchanged",
+ "given": {
+ "rawUnsignedTx": hexlify(tx.serialize_unsigned()),
+ "utxosSpent": [
+ {"scriptPubKey": hexlify(u.script_pubkey),
+ "amountSats": u.amount}
+ for u in utxos
+ ],
+ "internalPubkey": hexlify(internal_x),
+ "script": hexlify(script),
+ "leafVersion": "0xc0",
+ "hashType": SIGHASH_DEFAULT,
+ },
+ "intermediary": {
+ "leafHash": hexlify(tapleaf_hash),
+ "tweak": hexlify(t.to_bytes()),
+ "tweakedPubkey": hexlify(Q.to_bytes_xonly()),
+ "controlBlock": hexlify(control),
+ "sigHash": hexlify(m_script),
+ },
+ "expected": {
+ "scriptPubKey": hexlify(spk),
+ "address": v2_address(Q.to_bytes_xonly()),
+ "witness": [hexlify(e) for e in tx.witnesses[0]],
+ "rawSignedTx": hexlify(tx.serialize_signed()),
+ },
+ }
+ ]
+
+ return vectors
+
+
+def make_consensus_vectors():
+ cases = []
+
+ def add_case(case_id, description, tx, utxos, valid, failure_reason=None):
+ case = {
+ "id": case_id,
+ "description": description,
+ "prevouts": [
+ {"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
+ for u in utxos
+ ],
+ "tx": hexlify(tx.serialize_signed()),
+ "valid": valid,
+ }
+ if failure_reason is not None:
+ case["failureReason"] = failure_reason
+ cases.append(case)
+
+ def fresh_setup(num_inputs, key_offset=0):
+ keys = [tweak_keypair(test_seckey(i + key_offset)) for i in range(num_inputs)]
+ utxos = [
+ TxOut(100_000_000, v2_script_pubkey(k[3])) for k in keys
+ ]
+ tx = Tx(
+ vin=[TxIn(test_prevout_txid(i + key_offset), 0) for i in range(num_inputs)],
+ vout=[TxOut(num_inputs * 100_000_000 - 1_000_000,
+ v2_script_pubkey(keys[0][3]))],
+ )
+ return keys, utxos, tx
+
+ # 1: valid two-member half-aggregation group
+ keys, utxos, tx = fresh_setup(2)
+ _, _, aggsig = sign_halfagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig)]
+ add_case("halfagg-valid", "Two-member half-aggregation group, SIGHASH_DEFAULT",
+ tx, utxos, True)
+ halfagg_base = (keys, utxos, tx, aggsig)
+
+ # 2: valid two-member full-aggregation group
+ keys, utxos, tx = fresh_setup(2, key_offset=3)
+ _, _, _, sig64 = sign_fullagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_ALL)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL, sig64)]
+ add_case("fullagg-valid", "Two-member full-aggregation group, SIGHASH_ALL",
+ tx, utxos, True)
+ fullagg_base = (keys, utxos, tx, sig64)
+
+ # 3: valid mix of opted-out input and half-aggregation group
+ keys, utxos, tx = fresh_setup(3, key_offset=6)
+ _, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
+ _, _, aggsig3 = sign_halfagg_group(
+ tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT, sig=sig0)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG, sig=aggsig3)]
+ add_case("mixed-valid",
+ "Opted-out input alongside a two-member half-aggregation group",
+ tx, utxos, True)
+
+ # 4: invalid, undefined marker byte
+ keys, utxos, tx = fresh_setup(3, key_offset=6)
+ tx.witnesses[0] = [bytes([0xBE]) + sig0]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG, sig=aggsig3)]
+ add_case("marker-undefined", "Witness element starts with the undefined "
+ "marker byte 0xbe", tx, utxos, False,
+ "undefined marker byte")
+
+ # 5: invalid, explicit 0x00 sighash byte
+ keys, utxos, tx = fresh_setup(3, key_offset=6)
+ tx.witnesses[0] = [bytes([MARKER_OPTOUT, 0x00]) + sig0]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG, sig=aggsig3)]
+ add_case("sighash-explicit-default", "Opted-out input encodes "
+ "SIGHASH_DEFAULT explicitly as a 0x00 sighash byte",
+ tx, utxos, False, "explicit 0x00 sighash byte")
+
+ # 6: invalid, aggregation group without final signature
+ keys, utxos, tx = fresh_setup(2)
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ add_case("group-no-final", "Half-aggregation markers without an input "
+ "carrying the aggregate signature", tx, utxos, False,
+ "aggregation group has no final input")
+
+ # 7: invalid, marker after the group final
+ keys, utxos, tx = fresh_setup(3, key_offset=9)
+ _, _, aggsig7 = sign_halfagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig7)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG)]
+ add_case("marker-after-final", "Half-aggregation marker appears after "
+ "the group's final input", tx, utxos, False,
+ "marker after group final")
+
+ # 8: invalid, aggregate signature size mismatch
+ # fresh_setup(2) recreates the transaction from case 1 deterministically,
+ # so the aggregate signature from that case corresponds to this tx.
+ _, _, _, aggsig = halfagg_base
+ keys, utxos, tx = fresh_setup(2)
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig + bytes(32))]
+ add_case("aggsig-size-mismatch", "Aggregate signature is 128 bytes for a "
+ "two-member half-aggregation group (expected 96)", tx, utxos,
+ False, "aggregate signature size mismatch")
+
+ # 9: invalid, cross-mode fold (mode commitment)
+ # Signatures are created over the opt-out flavored message and then
+ # packaged as a half-aggregation group. The group structure is well
+ # formed but aggregate verification fails because the signature
+ # message commits to the aggregation mode.
+ keys, utxos, tx = fresh_setup(2, key_offset=12)
+ triples = []
+ for idx in range(2):
+ m_optout = sigmsg_v2(tx, utxos, idx, SIGHASH_DEFAULT, MARKER_OPTOUT)
+ sig = schnorr_sign(m_optout, keys[idx][2], AUX_ZERO)
+ pk = utxos[idx].script_pubkey[2:]
+ triples.append((pk, m_optout, sig))
+ aggsig9 = halfagg.Aggregate(triples)
+ consensus_msgs = [
+ (utxos[i].script_pubkey[2:],
+ sigmsg_v2(tx, utxos, i, SIGHASH_DEFAULT, MARKER_HALFAGG))
+ for i in range(2)
+ ]
+ assert not halfagg.VerifyAggregate(aggsig9, consensus_msgs)
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig9)]
+ add_case("cross-mode-fold", "Opt-out flavored signatures folded into a "
+ "half-aggregation group by a third party", tx, utxos, False,
+ "aggregate signature invalid, messages commit to opt-out mode")
+
+ # 10: invalid, empty witness
+ keys, utxos, tx = fresh_setup(2)
+ _, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = []
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1)]
+ add_case("empty-witness", "Witness v2 input with an empty witness",
+ tx, utxos, False, "empty witness")
+
+ # 11: invalid, opt-out marker without a signature
+ keys, utxos, tx = fresh_setup(2)
+ _, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([MARKER_OPTOUT])]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1)]
+ add_case("optout-no-sig", "Opt-out marker as a 1-byte witness element "
+ "without a signature", tx, utxos, False,
+ "opt-out marker without signature")
+
+ # 12: invalid, opt-out marker with sighash byte but without a signature
+ keys, utxos, tx = fresh_setup(2)
+ _, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([MARKER_OPTOUT, SIGHASH_ALL])]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1)]
+ add_case("optout-sighash-no-sig", "Opt-out marker with an explicit "
+ "sighash byte but without a signature", tx, utxos, False,
+ "opt-out marker without signature")
+
+ # 13: valid, half-aggregation group member carrying a signed annex
+ keys, utxos, tx = fresh_setup(2, key_offset=15)
+ annex = bytes([0x50]) + b"CISA annex test"
+ m0 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG, annex=annex)
+ m1 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ pk0 = utxos[0].script_pubkey[2:]
+ pk1 = utxos[1].script_pubkey[2:]
+ sig0 = schnorr_sign(m0, keys[0][2], AUX_ZERO)
+ sig1 = schnorr_sign(m1, keys[1][2], AUX_ZERO)
+ aggsig_annex = halfagg.Aggregate([(pk0, m0, sig0), (pk1, m1, sig1)])
+ assert halfagg.VerifyAggregate(aggsig_annex, [(pk0, m0), (pk1, m1)])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG), annex]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig_annex)]
+ add_case("annex-committed-valid", "Half-aggregation group whose first "
+ "member carries an annex that is committed in its signature "
+ "message", tx, utxos, True)
+
+ # 14: invalid, annex stripped from the transaction of case 13
+ keys, utxos, tx = fresh_setup(2, key_offset=15)
+ m0_stripped = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ assert not halfagg.VerifyAggregate(
+ aggsig_annex, [(pk0, m0_stripped), (pk1, m1)]
+ )
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=aggsig_annex)]
+ add_case("annex-stripped", "Annex removed by a third party from the "
+ "transaction of annex-committed-valid", tx, utxos, False,
+ "signature message committed to the annex")
+
+ # 15: valid, witness v2 script path spend (BIP 341/342 rules unchanged)
+ script_seckey = test_seckey(20)
+ script_pk = (Scalar.from_bytes_checked(script_seckey) * G).to_bytes_xonly()
+ script = bytes([0x20]) + script_pk + bytes([0xAC]) # OP_CHECKSIG
+ tapleaf_hash = tagged_hash(
+ "TapLeaf", bytes([0xC0]) + ser_compact_size(len(script)) + script
+ )
+ internal_seckey = test_seckey(21)
+ d0 = Scalar.from_bytes_checked(internal_seckey)
+ P_int = d0 * G
+ internal_x = P_int.to_bytes_xonly()
+ t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x + tapleaf_hash))
+ Q = GE.from_bytes_xonly(internal_x) + t * G
+ control = bytes([0xC0 | (0 if Q.has_even_y() else 1)]) + internal_x
+ keys, utxos, tx = fresh_setup(2, key_offset=18)
+ utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q.to_bytes_xonly()))
+ m_script = sigmsg_tapscript(tx, utxos, 0, SIGHASH_DEFAULT, tapleaf_hash)
+ sig_script = schnorr_sign(m_script, script_seckey, AUX_ZERO)
+ assert schnorr_verify(m_script, script_pk, sig_script)
+ _, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [sig_script, script, control]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1)]
+ add_case("scriptpath-valid", "Witness v2 script path spend of a single "
+ "CHECKSIG leaf under BIP 341/342 rules, alongside an opted-out "
+ "key path input", tx, utxos, True)
+
+ # 16: invalid, cross-mode messages in a full-aggregation group
+ # The signers run a full-aggregation session over opt-out flavored
+ # messages and the result is placed in a 0xbd witness. Unlike case 9
+ # there is no public fold operation for full-agg, this checks that
+ # verifiers derive the messages with the 0xbd marker on this path too.
+ keys, utxos, tx = fresh_setup(2, key_offset=22)
+ _, _, _, sig64_16 = sign_fullagg_group(
+ tx, utxos,
+ [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)],
+ msg_mode=MARKER_OPTOUT,
+ )
+ pks16 = [GE.from_bytes_xonly(u.script_pubkey[2:]) for u in utxos]
+ consensus_msgs16 = [
+ sigmsg_v2(tx, utxos, i, SIGHASH_DEFAULT, MARKER_FULLAGG) for i in range(2)
+ ]
+ R16 = GE.from_bytes_xonly(sig64_16[:32])
+ s16 = Scalar.from_bytes_checked(sig64_16[32:])
+ assert not fullagg.Verify(pks16, consensus_msgs16, (R16, s16))
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64_16)]
+ add_case("cross-mode-fullagg", "Full-aggregation signature created over "
+ "opt-out flavored messages, placed in a full-aggregation group",
+ tx, utxos, False,
+ "aggregate signature invalid, messages commit to opt-out mode")
+
+ # 17: invalid, 97-byte 0xbd element (half-agg sized aggregate in the
+ # full-agg final slot). Marker 0xbd admits lengths 1, 2, 65 and 66
+ # only, so parsing fails in pass 1 regardless of the content.
+ # fresh_setup(2, key_offset=3) recreates the transaction from case 2
+ # deterministically, so the aggregate signature from that case
+ # corresponds to this tx.
+ _, _, _, sig64 = fullagg_base
+ keys, utxos, tx = fresh_setup(2, key_offset=3)
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL)]
+ tx.witnesses[1] = [bytes([MARKER_FULLAGG]) + sig64 + bytes(32)]
+ add_case("fullagg-size-mismatch", "Full-aggregation final input carries "
+ "96 bytes after the marker (97-byte 0xbd element, matches no "
+ "defined witness structure)", tx, utxos, False,
+ "marker and length match no defined structure")
+
+ # 18: valid, mixed transaction mirroring Example 4 of the BIP: two
+ # opted-out inputs with explicit sighash types, a half-aggregation
+ # group and a full-aggregation group with interleaved indices. Also
+ # covers a valid SIGHASH_SINGLE member with a corresponding output.
+ keys, utxos, tx = fresh_setup(7, key_offset=24)
+ tx.vout = [
+ TxOut(350_000_000, v2_script_pubkey(keys[0][3])),
+ TxOut(349_000_000, v2_script_pubkey(keys[1][3])),
+ ]
+ _, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
+ _, sig5 = sign_optout(tx, utxos, 5, keys[5][2],
+ SIGHASH_NONE | SIGHASH_ANYONECANPAY)
+ _, _, aggsig18 = sign_halfagg_group(
+ tx, utxos,
+ [(1, keys[1][2], SIGHASH_SINGLE), (4, keys[4][2], SIGHASH_DEFAULT)],
+ )
+ _, _, _, sig64_18 = sign_fullagg_group(
+ tx, utxos,
+ [(2, keys[2][2], SIGHASH_DEFAULT), (3, keys[3][2], SIGHASH_DEFAULT),
+ (6, keys[6][2], SIGHASH_DEFAULT)],
+ )
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT, sig=sig0)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, SIGHASH_SINGLE)]
+ tx.witnesses[2] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[3] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[4] = [marker_element(MARKER_HALFAGG, sig=aggsig18)]
+ tx.witnesses[5] = [marker_element(MARKER_OPTOUT,
+ SIGHASH_NONE | SIGHASH_ANYONECANPAY,
+ sig5)]
+ tx.witnesses[6] = [marker_element(MARKER_FULLAGG, sig=sig64_18)]
+ add_case("two-groups-valid", "Mixed transaction mirroring Example 4 of "
+ "the BIP: two opted-out inputs, a half-aggregation group with "
+ "a SIGHASH_SINGLE member, and a full-aggregation group, with "
+ "interleaved input indices", tx, utxos, True)
+
+ # 19: invalid, SIGHASH_SINGLE without a corresponding output. Input 1
+ # signals SIGHASH_SINGLE but the transaction has only one output, so
+ # computing SigMsg fails per the BIP 341 failure conditions, which are
+ # incorporated by reference. The witness is structurally well formed
+ # and carries a syntactically valid signature (created over the
+ # SIGHASH_ALL flavored message), so the only reason for rejection is
+ # the missing corresponding output.
+ keys, utxos, tx = fresh_setup(2, key_offset=31)
+ _, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
+ m_all = sigmsg_v2(tx, utxos, 1, SIGHASH_ALL, MARKER_OPTOUT)
+ sig1_19 = schnorr_sign(m_all, keys[1][2], AUX_ZERO)
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT, sig=sig0)]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, SIGHASH_SINGLE, sig1_19)]
+ add_case("sighash-single-no-output", "Opted-out input uses "
+ "SIGHASH_SINGLE at input index 1 of a single-output "
+ "transaction", tx, utxos, False,
+ "SIGHASH_SINGLE input without corresponding output")
+
+ # 20: invalid, two final inputs in one half-aggregation group. Each
+ # element carries a valid 64-byte single-member aggregate, so the
+ # failure is purely structural. Distinct from case 7, where the
+ # element after the final is a placeholder.
+ keys, utxos, tx = fresh_setup(2, key_offset=34)
+ _, _, agg_a = sign_halfagg_group(tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)])
+ _, _, agg_b = sign_halfagg_group(tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG, sig=agg_a)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg_b)]
+ add_case("halfagg-two-finals", "Two inputs each carrying a 64-byte "
+ "half-aggregation final element", tx, utxos, False,
+ "more than one final input in a group")
+
+ # 21: invalid, two final inputs in one full-aggregation group
+ keys, utxos, tx = fresh_setup(2, key_offset=36)
+ _, _, _, sig_a = sign_fullagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)], nonce_offset=10)
+ _, _, _, sig_b = sign_fullagg_group(
+ tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)], nonce_offset=11)
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG, sig=sig_a)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig_b)]
+ add_case("fullagg-two-finals", "Two inputs each carrying a 64-byte "
+ "full-aggregation final element", tx, utxos, False,
+ "more than one final input in a group")
+
+ # 22: invalid, full-aggregation markers without a final input
+ # (full-agg analog of case 6)
+ keys, utxos, tx = fresh_setup(2, key_offset=38)
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG)]
+ add_case("fullagg-no-final", "Full-aggregation markers without an input "
+ "carrying the aggregate signature", tx, utxos, False,
+ "aggregation group has no final input")
+
+ # 23: invalid, 0xbd marker after the full-agg final
+ # (full-agg analog of case 7)
+ keys, utxos, tx = fresh_setup(3, key_offset=40)
+ _, _, _, sig64_23 = sign_fullagg_group(
+ tx, utxos,
+ [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)],
+ nonce_offset=12)
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64_23)]
+ tx.witnesses[2] = [marker_element(MARKER_FULLAGG)]
+ add_case("fullagg-marker-after-final", "Full-aggregation marker appears "
+ "after the group's final input", tx, utxos, False,
+ "marker after group final")
+
+ # 24: valid, single-member half-aggregation group. The aggregate is
+ # (1+1)*32 = 64 bytes, the same size as an opted-out signature.
+ keys, utxos, tx = fresh_setup(1, key_offset=43)
+ _, _, aggsig24 = sign_halfagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG, sig=aggsig24)]
+ add_case("halfagg-single-member", "Half-aggregation group with a single "
+ "member carrying a 64-byte aggregate", tx, utxos, True)
+
+ # 25: valid, single-member full-aggregation group
+ keys, utxos, tx = fresh_setup(1, key_offset=44)
+ _, _, _, sig64_25 = sign_fullagg_group(
+ tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)], nonce_offset=13)
+ tx.witnesses[0] = [marker_element(MARKER_FULLAGG, sig=sig64_25)]
+ add_case("fullagg-single-member", "Full-aggregation group with a single "
+ "member", tx, utxos, True)
+
+ # 26: invalid, single witness element starting with the annex prefix.
+ # An annex requires at least two witness elements, so this parses as
+ # a key path element with the undefined marker byte 0x50.
+ keys, utxos, tx = fresh_setup(2, key_offset=45)
+ _, sig1_26 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([0x50]) + b"CISA annex test"]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_26)]
+ add_case("annex-lookalike", "Single witness element starting with the "
+ "annex prefix 0x50", tx, utxos, False,
+ "undefined marker byte, a single element is never an annex")
+
+ # 27: valid, annex on the final input of a group. The annex must be
+ # stripped before the final element is interpreted. Complements case
+ # 13, which has the annex on a placeholder input.
+ keys, utxos, tx = fresh_setup(2, key_offset=47)
+ annex27 = bytes([0x50]) + b"CISA annex test"
+ m0_27 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ m1_27 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG,
+ annex=annex27)
+ pk0 = utxos[0].script_pubkey[2:]
+ pk1 = utxos[1].script_pubkey[2:]
+ sig0_27 = schnorr_sign(m0_27, keys[0][2], AUX_ZERO)
+ sig1_27 = schnorr_sign(m1_27, keys[1][2], AUX_ZERO)
+ agg27 = halfagg.Aggregate([(pk0, m0_27, sig0_27), (pk1, m1_27, sig1_27)])
+ assert halfagg.VerifyAggregate(agg27, [(pk0, m0_27), (pk1, m1_27)])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg27), annex27]
+ add_case("annex-on-final", "Half-aggregation group whose final input "
+ "carries an annex that is committed in its signature message",
+ tx, utxos, True)
+
+ # 28: invalid, undefined sighash value. Case 5 covers the explicit
+ # 0x00 encoding rule, this covers a value outside the defined set.
+ # The signature bytes are irrelevant, parsing fails first.
+ keys, utxos, tx = fresh_setup(2, key_offset=49)
+ m0_28 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_OPTOUT)
+ sig0_28 = schnorr_sign(m0_28, keys[0][2], AUX_ZERO)
+ _, sig1_28 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([MARKER_OPTOUT, 0x04]) + sig0_28]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_28)]
+ add_case("sighash-value-undefined", "Opted-out input with the undefined "
+ "sighash value 0x04", tx, utxos, False,
+ "undefined sighash type")
+
+ # 29: invalid, 33-byte 0xbc element. Half-aggregation finals are at
+ # least 65 bytes since a group has at least one member, so this
+ # matches no defined structure. Complements case 8, where the
+ # aggregate is too large.
+ keys, utxos, tx = fresh_setup(2, key_offset=51)
+ _, sig0_29 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
+ _, _, agg29 = sign_halfagg_group(
+ tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)])
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT, sig=sig0_29)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg29[:32])]
+ add_case("halfagg-final-too-small", "33-byte 0xbc element, below the "
+ "65-byte minimum for a half-aggregation final", tx, utxos,
+ False, "marker and length match no defined structure")
+
+ # 30: valid, witness v2 output whose witness program is not 32 bytes.
+ # Such outputs remain unencumbered by the BIP, so the witness content
+ # is unconstrained, even an undefined marker byte.
+ keys, utxos, tx = fresh_setup(2, key_offset=53)
+ program33 = test_garbage(b"33-byte program", 33)
+ utxos[0] = TxOut(100_000_000, bytes([0x52, 0x21]) + program33)
+ _, sig1_30 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([0xBE])]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_30)]
+ add_case("program-not-32-bytes", "Witness v2 output with a 33-byte "
+ "witness program spent with an arbitrary witness", tx, utxos,
+ True)
+
+ # 31: invalid, opted-out witness program is not a valid x coordinate.
+ # BIP 340 verification fails at lift_x.
+ keys, utxos, tx = fresh_setup(2, key_offset=55)
+ utxos[0] = TxOut(100_000_000,
+ bytes([0x52, 0x20]) + test_offcurve_x(b"optout"))
+ _, sig1_31 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [marker_element(MARKER_OPTOUT,
+ sig=test_garbage(b"offcurve sig", 64))]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_31)]
+ add_case("program-offcurve-optout", "Opted-out input whose witness "
+ "program is not a valid x-only public key", tx, utxos, False,
+ "witness program is not a valid x-only public key")
+
+ # 32: invalid, group member witness program is not a valid x
+ # coordinate. VerifyAggregate fails at lift_x of the first pubkey, so
+ # the aggregate content is irrelevant.
+ keys, utxos, tx = fresh_setup(2, key_offset=57)
+ utxos[0] = TxOut(100_000_000,
+ bytes([0x52, 0x20]) + test_offcurve_x(b"group"))
+ m1_32 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ sig1_32 = schnorr_sign(m1_32, keys[1][2], AUX_ZERO)
+ agg32 = test_garbage(b"offcurve r0", 32) + sig1_32
+ assert not halfagg.VerifyAggregate(agg32, [
+ (utxos[0].script_pubkey[2:],
+ sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)),
+ (utxos[1].script_pubkey[2:], m1_32),
+ ])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg32)]
+ add_case("program-offcurve-in-group", "Half-aggregation group member "
+ "whose witness program is not a valid x-only public key",
+ tx, utxos, False,
+ "witness program is not a valid x-only public key")
+
+ # 33: valid, script path spend with an unknown leaf version (0xc2).
+ # Unknown leaf versions succeed unconditionally under BIP 341,
+ # preserving the upgrade hook in witness v2.
+ keys, utxos, tx = fresh_setup(2, key_offset=59)
+ internal_seckey33 = test_seckey(200)
+ internal_x33 = (Scalar.from_bytes_checked(internal_seckey33) * G).to_bytes_xonly()
+ script33 = bytes([0x51])
+ leaf33 = tagged_hash(
+ "TapLeaf", bytes([0xC2]) + ser_compact_size(len(script33)) + script33)
+ t33 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x33 + leaf33))
+ Q33 = GE.from_bytes_xonly(internal_x33) + t33 * G
+ control33 = bytes([0xC2 | (0 if Q33.has_even_y() else 1)]) + internal_x33
+ utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q33.to_bytes_xonly()))
+ _, sig1_33 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [script33, control33]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_33)]
+ add_case("scriptpath-unknown-leaf-version", "Witness v2 script path "
+ "spend with unknown leaf version 0xc2, which succeeds "
+ "unconditionally per BIP 341", tx, utxos, True)
+
+ # 34: valid, script path leaf containing an OP_SUCCESS opcode. The
+ # script byte 0xbb is OP_SUCCESS187 in tapscript and has no marker
+ # meaning inside a script.
+ keys, utxos, tx = fresh_setup(2, key_offset=61)
+ internal_seckey34 = test_seckey(201)
+ internal_x34 = (Scalar.from_bytes_checked(internal_seckey34) * G).to_bytes_xonly()
+ script34 = bytes([0xBB])
+ leaf34 = tagged_hash(
+ "TapLeaf", bytes([0xC0]) + ser_compact_size(len(script34)) + script34)
+ t34 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x34 + leaf34))
+ Q34 = GE.from_bytes_xonly(internal_x34) + t34 * G
+ control34 = bytes([0xC0 | (0 if Q34.has_even_y() else 1)]) + internal_x34
+ utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q34.to_bytes_xonly()))
+ _, sig1_34 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [script34, control34]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_34)]
+ add_case("scriptpath-op-success", "Witness v2 script path spend whose "
+ "leaf script is OP_SUCCESS187 (byte 0xbb), which succeeds "
+ "unconditionally per BIP 342", tx, utxos, True)
+
+ # 35: valid, mixed witness v1 and v2 transaction mirroring Example 3
+ # of the BIP. Input 0 spends a taproot output with an unchanged
+ # BIP 341 key path signature (epoch 0x00, no marker).
+ keys, utxos, tx = fresh_setup(3, key_offset=63)
+ utxos[0] = TxOut(100_000_000, bytes([0x51, 0x20]) + keys[0][3])
+ m0_35 = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
+ sig0_35 = schnorr_sign(m0_35, keys[0][2], AUX_ZERO)
+ _, _, agg35 = sign_halfagg_group(
+ tx, utxos,
+ [(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)])
+ tx.witnesses[0] = [sig0_35]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[2] = [marker_element(MARKER_HALFAGG, sig=agg35)]
+ add_case("v1-v2-mixed", "Witness v1 key path input alongside a "
+ "two-member witness v2 half-aggregation group, mirroring "
+ "Example 3 of the BIP", tx, utxos, True)
+
+ # 36: invalid, explicit sighash byte stripped by a third party. Input
+ # 0 signed with explicit SIGHASH_ALL, the witness is malleated to the
+ # 1-byte form, so the verifier derives the SIGHASH_DEFAULT flavored
+ # message. Complements case 14, which strips the annex.
+ keys, utxos, tx = fresh_setup(2, key_offset=66)
+ msgs36, _, agg36 = sign_halfagg_group(
+ tx, utxos,
+ [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_DEFAULT)])
+ m0_default = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ assert not halfagg.VerifyAggregate(agg36, [
+ (utxos[0].script_pubkey[2:], m0_default),
+ (utxos[1].script_pubkey[2:], msgs36[1]),
+ ])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg36)]
+ add_case("sighash-byte-stripped", "Explicit SIGHASH_ALL byte removed "
+ "from a group member by a third party", tx, utxos, False,
+ "signature message committed to the sighash type")
+
+ # 37: invalid, empty witness element. Distinct from case 10, where
+ # the witness stack itself is empty.
+ keys, utxos, tx = fresh_setup(2, key_offset=68)
+ _, sig1_37 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [b""]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_37)]
+ add_case("empty-witness-element", "Witness v2 input whose only witness "
+ "element is empty", tx, utxos, False, "empty witness element")
+
+ # 38: invalid, SIGHASH_SINGLE group member without a corresponding
+ # output. Complements case 19, which covers the opted-out path. The
+ # witness is structurally well formed (98-byte element for n = 2) and
+ # the signature bytes are created over the SIGHASH_ALL flavored
+ # message, so the only reason for rejection is the missing output.
+ keys, utxos, tx = fresh_setup(2, key_offset=70)
+ pk0 = utxos[0].script_pubkey[2:]
+ pk1 = utxos[1].script_pubkey[2:]
+ m0_38 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
+ m1_38 = sigmsg_v2(tx, utxos, 1, SIGHASH_ALL, MARKER_HALFAGG)
+ sig0_38 = schnorr_sign(m0_38, keys[0][2], AUX_ZERO)
+ sig1_38 = schnorr_sign(m1_38, keys[1][2], AUX_ZERO)
+ agg38 = halfagg.Aggregate([(pk0, m0_38, sig0_38), (pk1, m1_38, sig1_38)])
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, SIGHASH_SINGLE, agg38)]
+ add_case("sighash-single-no-output-group", "Half-aggregation group "
+ "member uses SIGHASH_SINGLE at input index 1 of a "
+ "single-output transaction", tx, utxos, False,
+ "SIGHASH_SINGLE input without corresponding output")
+
+ # 39: invalid, two witness elements where the last is not an annex.
+ # With two elements and no annex prefix this is a script path spend,
+ # and the 1-byte last element is not a valid control block.
+ keys, utxos, tx = fresh_setup(2, key_offset=72)
+ m0_39 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_OPTOUT)
+ sig0_39 = schnorr_sign(m0_39, keys[0][2], AUX_ZERO)
+ _, sig1_39 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([MARKER_OPTOUT]) + sig0_39, bytes([0x60])]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_39)]
+ add_case("two-elements-no-annex", "Keypath-looking element followed by "
+ "a second element that is not an annex, making this a script "
+ "path spend with an invalid control block", tx, utxos, False,
+ "invalid control block size")
+
+ # 40: invalid, marker-less v1-style spend of a witness v2 output. The
+ # witness is a bare 64-byte signature over the BIP 341 witness v1
+ # message, as produced by a wallet that treats v2 outputs like Taproot.
+ # The first byte of the signature is not a defined marker.
+ keys, utxos, tx = fresh_setup(2, key_offset=74)
+ m0_40 = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
+ sig0_40 = schnorr_sign(m0_40, keys[0][2], AUX_ZERO)
+ assert sig0_40[0] not in (MARKER_OPTOUT, MARKER_HALFAGG, MARKER_FULLAGG)
+ _, sig1_40 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [sig0_40]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_40)]
+ add_case("v1-style-spend", "Witness v2 output spent like a Taproot "
+ "output with a bare 64-byte signature and no marker", tx,
+ utxos, False, "undefined marker byte")
+
+ # 41: valid, 8-member half-aggregation group. The aggregate is
+ # (8+1)*32 = 288 bytes and the final witness element is 289 bytes,
+ # exercising the multi-byte compact size length prefix required for
+ # elements over 252 bytes (any group of seven or more members crosses
+ # this boundary).
+ keys, utxos, tx = fresh_setup(8, key_offset=76)
+ members41 = [(i, keys[i][2], SIGHASH_DEFAULT) for i in range(8)]
+ _, _, agg41 = sign_halfagg_group(tx, utxos, members41)
+ for i in range(7):
+ tx.witnesses[i] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[7] = [marker_element(MARKER_HALFAGG, sig=agg41)]
+ add_case("halfagg-large-group", "8-member half-aggregation group with "
+ "a 289-byte final witness element, crossing the compact size "
+ "boundary for witness elements", tx, utxos, True)
+
+ # 42: valid, script path spend whose first stack element is a 1-byte
+ # 0xbc marker lookalike. With three witness elements this is a script
+ # path spend, so marker interpretation does not apply. The leaf
+ # script drops the element and pushes true.
+ keys, utxos, tx = fresh_setup(2, key_offset=84)
+ internal_seckey42 = test_seckey(202)
+ internal_x42 = (Scalar.from_bytes_checked(internal_seckey42) * G).to_bytes_xonly()
+ script42 = bytes([0x75, 0x51]) # OP_DROP OP_1
+ leaf42 = tagged_hash(
+ "TapLeaf", bytes([0xC0]) + ser_compact_size(len(script42)) + script42)
+ t42 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x42 + leaf42))
+ Q42 = GE.from_bytes_xonly(internal_x42) + t42 * G
+ control42 = bytes([0xC0 | (0 if Q42.has_even_y() else 1)]) + internal_x42
+ utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q42.to_bytes_xonly()))
+ _, sig1_42 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
+ tx.witnesses[0] = [bytes([MARKER_HALFAGG]), script42, control42]
+ tx.witnesses[1] = [marker_element(MARKER_OPTOUT, sig=sig1_42)]
+ add_case("scriptpath-marker-lookalike", "Script path spend whose first "
+ "stack element is a 1-byte 0xbc marker lookalike, which has "
+ "no marker meaning outside key path spends", tx, utxos, True)
+
+ # 43: valid, both group members spend outputs with the same witness
+ # program (address reuse). The messages differ by input index and
+ # neither scheme deduplicates keys.
+ keys, utxos, tx = fresh_setup(4, key_offset=86)
+ utxos[1] = TxOut(100_000_000, v2_script_pubkey(keys[0][3]))
+ utxos[3] = TxOut(100_000_000, v2_script_pubkey(keys[2][3]))
+ _, _, agg43 = sign_halfagg_group(
+ tx, utxos,
+ [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[0][2], SIGHASH_DEFAULT)])
+ _, _, _, sig64_43 = sign_fullagg_group(
+ tx, utxos,
+ [(2, keys[2][2], SIGHASH_DEFAULT), (3, keys[2][2], SIGHASH_DEFAULT)],
+ nonce_offset=20)
+ tx.witnesses[0] = [marker_element(MARKER_HALFAGG)]
+ tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg43)]
+ tx.witnesses[2] = [marker_element(MARKER_FULLAGG)]
+ tx.witnesses[3] = [marker_element(MARKER_FULLAGG, sig=sig64_43)]
+ add_case("duplicate-key-groups", "Half-aggregation and full-aggregation "
+ "groups whose two members each spend outputs with the same "
+ "witness program (address reuse)", tx, utxos, True)
+
+ return {"testCases": cases}
+
+
+def main():
+ out_dir = Path(__file__).parent
+ wallet = make_wallet_vectors()
+ consensus = make_consensus_vectors()
+ with open(out_dir / "wallet-test-vectors.json", "w") as f:
+ json.dump(wallet, f, indent=2)
+ f.write("\n")
+ with open(out_dir / "consensus-test-vectors.json", "w") as f:
+ json.dump(consensus, f, indent=2)
+ f.write("\n")
+ print(f"wrote {out_dir / 'wallet-test-vectors.json'}")
+ print(f"wrote {out_dir / 'consensus-test-vectors.json'}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bip-0460/wallet-test-vectors.json b/bip-0460/wallet-test-vectors.json
new file mode 100644
index 0000000000..db50281b29
--- /dev/null
+++ b/bip-0460/wallet-test-vectors.json
@@ -0,0 +1,222 @@
+{
+ "scriptPubKey": [
+ {
+ "given": {
+ "internalPubkey": "34b703e82bfdedfbab012da7a34767456c3f85524171663df7c0aaf227276901"
+ },
+ "intermediary": {
+ "tweak": "4821522480fda0d8eeab11f09a642cae24605302c3ccda3f590e323f81d6fc0b",
+ "tweakedPubkey": "4049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b"
+ },
+ "expected": {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "address": "bc1zgpyuugjdfdjq06w8se8g3kqzpxttavzxv0x3t94lflu2kv6mc4as88u8yc"
+ }
+ },
+ {
+ "given": {
+ "internalPubkey": "cca0578cd53156e37b6e87c90b2d03cb38b046587a7c4a9a2986e24d0c85c120"
+ },
+ "intermediary": {
+ "tweak": "d18eb46c3f1105be40607ff7c064a3af23cb7cce36938f7cf73dbfd2e694a950",
+ "tweakedPubkey": "cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5"
+ },
+ "expected": {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "address": "bc1zejf5zh4d6tknfhrx3v69aaya6jfw4t6uq43ystfe8ytev6ndhl2sx2w44g"
+ }
+ }
+ ],
+ "keyPathSpending": [
+ {
+ "description": "Opted-out input with SIGHASH_ALL alongside a two-member half-aggregation group with SIGHASH_DEFAULT",
+ "given": {
+ "rawUnsignedTx": "0200000003375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff8e89080fe122cee9ffa6c0b2128cc14be4ca148c2629d0040f6037cfd0f816190000000000ffffffff01c060d211000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b00000000",
+ "utxosSpent": [
+ {
+ "scriptPubKey": "52204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b",
+ "amountSats": 100000000
+ },
+ {
+ "scriptPubKey": "5220cc93415eadd2ed34dc668b345ef49dd492eaaf5c0562482d393917966a6dbfd5",
+ "amountSats": 101000000
+ },
+ {
+ "scriptPubKey": "52208877c408836c8fac26214cd6ce52200ef5c44f8466b08d889cc671b7fa7dbcbd",
+ "amountSats": 102000000
+ }
+ ]
+ },
+ "inputSpending": [
+ {
+ "given": {
+ "txinIndex": 0,
+ "internalPrivkey": "50dbc446357ea19a9f032c65541a3d69d7fce0f909c67671ab92f0fdf75bc75b",
+ "marker": "0xbb",
+ "hashType": 1
+ },
+ "intermediary": {
+ "internalPubkey": "34b703e82bfdedfbab012da7a34767456c3f85524171663df7c0aaf227276901",
+ "tweak": "4821522480fda0d8eeab11f09a642cae24605302c3ccda3f590e323f81d6fc0b",
+ "tweakedPrivkey": "98fd166ab67c42738dae3e55ee7e6a17fc5d33fbcd9350b104a1233d7932c366",
+ "sigHash": "f0bbb40db0b9244c8e789552e18a806c4aefe6a6510b9accdcb46da0c4976c2a"
+ },
+ "expected": {
+ "witness": [
+ "bb01e7036e9e8afd0ce3e8274e258d868ecbcc0dd5ea890458e44b7d2749d1f5506bcd182c409c31a13e4c0937179863f1980d6051752f3e1ed8099eed96ad862b0f"
+ ]
+ }
+ },
+ {
+ "given": {
+ "txinIndex": 1,
+ "internalPrivkey": "26cc4c0d15b25be1e2137c638a99c9dbce9cfe8b74d2f1612731d9e620648870",
+ "marker": "0xbc",
+ "hashType": 0
+ },
+ "intermediary": {
+ "internalPubkey": "cca0578cd53156e37b6e87c90b2d03cb38b046587a7c4a9a2986e24d0c85c120",
+ "tweak": "d18eb46c3f1105be40607ff7c064a3af23cb7cce36938f7cf73dbfd2e694a950",
+ "tweakedPrivkey": "f85b007954c361a02273fc5b4afe6d8af2687b59ab6680de1e6f99b906f931c0",
+ "sigHash": "b4790a58d8405ea70b8758143fd245301d725b23af4a60a2d2393686b79122af"
+ },
+ "expected": {
+ "witness": [
+ "bc"
+ ]
+ }
+ },
+ {
+ "given": {
+ "txinIndex": 2,
+ "internalPrivkey": "213ac0b0d8b50089762f10ee2851872255c673e87da343375d89060cbec74e4a",
+ "marker": "0xbc",
+ "hashType": 0
+ },
+ "intermediary": {
+ "internalPubkey": "49d40eb515a74c614f7b231d3b2957b5dd4267f34b0c99f3da86116d1974670c",
+ "tweak": "88e8cdaa41ee11c3a85fcb9b1321fcd82d4c79ba143265d6df49ed76127071ca",
+ "tweakedPrivkey": "67ae0cf96939113a3230baacead075b5d78605d1968f229f81c0e76953a92380",
+ "sigHash": "d8562e8cf18da05da50f2bc3f00fc8b93585bcc3c07e398b85a68b068c15247c"
+ },
+ "expected": {
+ "witness": [
+ "bc5dc80989a7bcfb1fce2602935d5b70052b7eb030c50f4a9f08b49c08cb20ef3f9e0f4cac3b04d5360ca75e857091040adbc8e35d7e72d6ba1e4d4d5f407018ebdb694f9f26ae5b34c24c2123e76cd5924da9d7236b71d5cc87c16303e1f353bc"
+ ]
+ }
+ }
+ ],
+ "auxiliary": {
+ "aggregateSignature": "5dc80989a7bcfb1fce2602935d5b70052b7eb030c50f4a9f08b49c08cb20ef3f9e0f4cac3b04d5360ca75e857091040adbc8e35d7e72d6ba1e4d4d5f407018ebdb694f9f26ae5b34c24c2123e76cd5924da9d7236b71d5cc87c16303e1f353bc"
+ },
+ "expected": {
+ "rawSignedTx": "02000000000103375412079d92555f10368dc54e08189023c8a6bfe0318933dda280bb8c96e76a0000000000ffffffff2dea8db84ee1bcea688026b1bda8fea0667b8f3b24d914e2afe0f5dffae5086d0000000000ffffffff8e89080fe122cee9ffa6c0b2128cc14be4ca148c2629d0040f6037cfd0f816190000000000ffffffff01c060d211000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b0142bb01e7036e9e8afd0ce3e8274e258d868ecbcc0dd5ea890458e44b7d2749d1f5506bcd182c409c31a13e4c0937179863f1980d6051752f3e1ed8099eed96ad862b0f0101bc0161bc5dc80989a7bcfb1fce2602935d5b70052b7eb030c50f4a9f08b49c08cb20ef3f9e0f4cac3b04d5360ca75e857091040adbc8e35d7e72d6ba1e4d4d5f407018ebdb694f9f26ae5b34c24c2123e76cd5924da9d7236b71d5cc87c16303e1f353bc00000000"
+ }
+ },
+ {
+ "description": "Two-member full-aggregation group with SIGHASH_ALL (using deterministic nonces)",
+ "given": {
+ "rawUnsignedTx": "020000000254d36a0897340110cc34b526d73b206175ae4f5db4d25fe7bdeef3a8a5e89afd0100000000ffffffff835a02826d69e61b563ec713020bd2b7c3bc3848223cda2b3c52bbdcd3e6267a0100000000ffffffff01c0cb1707000000002252208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e400000000",
+ "utxosSpent": [
+ {
+ "scriptPubKey": "52208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e4",
+ "amountSats": 50000000
+ },
+ {
+ "scriptPubKey": "522018b6491469cc78b764b01669b0a5a4e892f57e8c1b20b89bc1ca6dce3e881b51",
+ "amountSats": 70000000
+ }
+ ]
+ },
+ "inputSpending": [
+ {
+ "given": {
+ "txinIndex": 0,
+ "internalPrivkey": "edc07892ebcc0812db6aea0fbc967eed825fedecf70db83d4c69fbe574b97361",
+ "marker": "0xbd",
+ "hashType": 1
+ },
+ "intermediary": {
+ "internalPubkey": "020be368e697e135d54cd88e86b42c0b5e3e72d4b0c77632a8a4d0441203ded5",
+ "tweak": "22b1442d7532c6cdbe4d9854a90b8f7a5820c559af2ca6f7d375e6fc3bfd8a09",
+ "tweakedPrivkey": "1071bcc060fecee099b8826465a20e691fd1d65ff6f1bef9600d8454e080bc29",
+ "sigHash": "9339267f5f6715d1b7e1b26bd69e13689b3872be5fa0c50a9d90daf9f2ae46cc"
+ },
+ "expected": {
+ "witness": [
+ "bd01"
+ ]
+ }
+ },
+ {
+ "given": {
+ "txinIndex": 1,
+ "internalPrivkey": "80f4d8f499868a546ca66497352d4df0aa1c7e99cd83faeba3eeb11ecafec708",
+ "marker": "0xbd",
+ "hashType": 1
+ },
+ "intermediary": {
+ "internalPubkey": "0fef9c578d084fb5f7c8fe48a570dc644609cc012a356fdd0c056f674f9455c0",
+ "tweak": "85fb96a1a8a6c3a93ebfaf308aecc9837eceb6cfda6eb735cf8586e989a87563",
+ "tweakedPrivkey": "0506bdad0f203954d2194a9955bf7b92d4b238360ceabc4a2b96d5cabea9ae5b",
+ "sigHash": "d2982e6665e1e964ecefa2a747b19f17ad46d6765e284d639c293647c9349857"
+ },
+ "expected": {
+ "witness": [
+ "bd01f00c224cc4e1bb038b1f35d71c28608510236327924d350d75520eb6295eb1908a38b68c0a7b484bc4ac7330ae2f5e909bf25b53fa60131f7d6799d5017311d8"
+ ]
+ }
+ }
+ ],
+ "auxiliary": {
+ "secnonces": [
+ "87c33dcedc7db5994d68278a81a4693ee961d08ef7e3fdeab611ab8061e92dd4517ac39af981926ed1124224dfe721c6dc0769a8ea24d2e2fc2636c3b1e9ca15",
+ "3c4b7832dd37bbdb955ebf900769d133d55161e5c6ad9e09c2efa4ace1ba2e153d007745e9f74e6b8a47651a672111b07589261401674263a40e286340bcefea"
+ ],
+ "pubnonces": [
+ "021be5111aa027a4b7368d88cb0f5419f7b530e17e41af6fe7a611a03abc9bab63020e72955a3c53f163a1373ac5233ad30ff8bb4ef69dad424d91529c0ef6329ac6",
+ "0367b7d091e6b58f7e94e9228393bc6b777e9fbe0186e9fce56248c9177c96f65e03592137dea3431dbb0fbe1287c35f8e44a30d68e62486969d21e90d2b4c68f05e"
+ ],
+ "aggregateSignature": "f00c224cc4e1bb038b1f35d71c28608510236327924d350d75520eb6295eb1908a38b68c0a7b484bc4ac7330ae2f5e909bf25b53fa60131f7d6799d5017311d8"
+ },
+ "expected": {
+ "rawSignedTx": "0200000000010254d36a0897340110cc34b526d73b206175ae4f5db4d25fe7bdeef3a8a5e89afd0100000000ffffffff835a02826d69e61b563ec713020bd2b7c3bc3848223cda2b3c52bbdcd3e6267a0100000000ffffffff01c0cb1707000000002252208640c68f13b51a1e63dfc5cc41202cca21c18f1f9d7037c2cbec3f03f63e52e40102bd010142bd01f00c224cc4e1bb038b1f35d71c28608510236327924d350d75520eb6295eb1908a38b68c0a7b484bc4ac7330ae2f5e909bf25b53fa60131f7d6799d5017311d800000000"
+ }
+ }
+ ],
+ "scriptPathSpending": [
+ {
+ "description": "Script path spend of a witness v2 output with a single CHECKSIG leaf, following BIP 341/342 unchanged",
+ "given": {
+ "rawUnsignedTx": "02000000019d159d40bef848f021458aee62eecb1006f6e987d41b8af985dcb13cf97b47d60000000000ffffffff01c09ee605000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b00000000",
+ "utxosSpent": [
+ {
+ "scriptPubKey": "5220e8605ff15e754192a9d6c053969ae935932c82ceaeea0fed9a152f0c6b4b424b",
+ "amountSats": 100000000
+ }
+ ],
+ "internalPubkey": "39920c3a54000624c1923187eebbbd3fcdc409302757139c1a1b003bc9276da4",
+ "script": "2038c01dda6433ce79606dda3f70ee5dd525104bbbba5b0fd96f56d6d634e92f71ac",
+ "leafVersion": "0xc0",
+ "hashType": 0
+ },
+ "intermediary": {
+ "leafHash": "d888fa5463776e32e8b0fddf14df8b3862673078395cbc592c10912056af7c52",
+ "tweak": "543da6eae6544c2af0aa1c684f67bf908059153e077714caa4af34da3bd72ae6",
+ "tweakedPubkey": "e8605ff15e754192a9d6c053969ae935932c82ceaeea0fed9a152f0c6b4b424b",
+ "controlBlock": "c139920c3a54000624c1923187eebbbd3fcdc409302757139c1a1b003bc9276da4",
+ "sigHash": "ea23ae9afeeb7f6e5e6aa6ca278eee5071aa9f20ca843a590b28f0faf9df058c"
+ },
+ "expected": {
+ "scriptPubKey": "5220e8605ff15e754192a9d6c053969ae935932c82ceaeea0fed9a152f0c6b4b424b",
+ "address": "bc1zaps9lu27w4qe92wkcpfedxhfxkfjeqkw4m4qlmv6z5hsc66tgf9sg52494",
+ "witness": [
+ "3f748f6e85b7064aa161a2e35ed5d17f540a2de4975c690739a04af70adf372b6daa1b3a27d553c38e12547289803aeb2d726187cdf6faec419e5cd864391460",
+ "2038c01dda6433ce79606dda3f70ee5dd525104bbbba5b0fd96f56d6d634e92f71ac",
+ "c139920c3a54000624c1923187eebbbd3fcdc409302757139c1a1b003bc9276da4"
+ ],
+ "rawSignedTx": "020000000001019d159d40bef848f021458aee62eecb1006f6e987d41b8af985dcb13cf97b47d60000000000ffffffff01c09ee605000000002252204049ce224d4b6407e9c7864e88d8020996beb04663cd1596bf4ff8ab335bc57b03403f748f6e85b7064aa161a2e35ed5d17f540a2de4975c690739a04af70adf372b6daa1b3a27d553c38e12547289803aeb2d726187cdf6faec419e5cd864391460222038c01dda6433ce79606dda3f70ee5dd525104bbbba5b0fd96f56d6d634e92f71ac21c139920c3a54000624c1923187eebbbd3fcdc409302757139c1a1b003bc9276da400000000"
+ }
+ }
+ ]
+}