From 652b940f9ed73472d4ee9e1dbd4cfb24695058b3 Mon Sep 17 00:00:00 2001 From: Lucia Date: Sat, 9 May 2026 21:33:21 +1200 Subject: [PATCH 1/5] This fills in the remaining TODO sections in BIP-376 --- bip-0376.mediawiki | 22 +++- bip-0376/reference.py | 223 +++++++++++++++++++++++++++++++++++++ bip-0376/test-vectors.json | 65 +++++++++++ 3 files changed, 308 insertions(+), 2 deletions(-) create mode 100755 bip-0376/reference.py create mode 100644 bip-0376/test-vectors.json diff --git a/bip-0376.mediawiki b/bip-0376.mediawiki index a005f08ec0..12987ee72f 100644 --- a/bip-0376.mediawiki +++ b/bip-0376.mediawiki @@ -146,11 +146,29 @@ These are new fields added to the existing PSBT format. Because PSBT is designed == Reference implementation == -'''''TODO''''' +A Python reference implementation is provided in [[bip-0376/reference.py|bip-0376/reference.py]]. + +It demonstrates the Signer behavior specified in this BIP: + +* Key derivation using ''d = (bspend + tweak) mod n''. +* Key negation when ''d·G'' has odd y-coordinate. +* Verification that the resulting x-only public key matches the output key ''P''. +* BIP 340 signing with the derived key. === Test vectors === -'''''TODO''''' +Machine-readable test vectors are provided in [[bip-0376/test-vectors.json|bip-0376/test-vectors.json]]. + +The vector set includes: + +* Valid cases with and without key negation. +* Invalid cases for output-key mismatch, zero tweaked key, and out-of-range spend key. + +The reference implementation can be run against the vectors with: + +
+./bip-0376/reference.py bip-0376/test-vectors.json
+
== Appendix == diff --git a/bip-0376/reference.py b/bip-0376/reference.py new file mode 100755 index 0000000000..d5b63fad9c --- /dev/null +++ b/bip-0376/reference.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""BIP-0376 reference implementation and test vector runner. + +Run: + ./bip-0376/reference.py bip-0376/test-vectors.json +""" + +import json +import sys +import hashlib +from pathlib import Path +from typing import Optional, Tuple + +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +G = ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, +) + +Point = Tuple[int, int] + + +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode("utf-8")).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def int_from_bytes(data: bytes) -> int: + return int.from_bytes(data, byteorder="big") + + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + + +def has_even_y(P: Point) -> bool: + return (P[1] % 2) == 0 + + +def bytes_from_point(P: Point) -> bytes: + return bytes_from_int(P[0]) + + +def xor_bytes(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(a, b)) + + +def lift_x(x_coord: int) -> Optional[Point]: + if x_coord >= p: + return None + y_sq = (pow(x_coord, 3, p) + 7) % p + y_coord = pow(y_sq, (p + 1) // 4, p) + if pow(y_coord, 2, p) != y_sq: + return None + return (x_coord, y_coord if (y_coord % 2) == 0 else p - y_coord) + + +def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: + if P1 is None: + return P2 + if P2 is None: + return P1 + if (P1[0] == P2[0]) and (P1[1] != P2[1]): + return None + if P1 == P2: + lam = (3 * P1[0] * P1[0] * pow(2 * P1[1], p - 2, p)) % p + else: + lam = ((P2[1] - P1[1]) * pow(P2[0] - P1[0], p - 2, p)) % p + x3 = (lam * lam - P1[0] - P2[0]) % p + y3 = (lam * (P1[0] - x3) - P1[1]) % p + return (x3, y3) + + +def point_mul(P: Optional[Point], scalar: int) -> Optional[Point]: + R = None + for i in range(256): + if (scalar >> i) & 1: + R = point_add(R, P) + P = point_add(P, P) + return R + + +def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: + if len(pubkey) != 32 or len(sig) != 64: + return False + P = lift_x(int_from_bytes(pubkey)) + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if P is None or r >= p or s >= n: + return False + e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n + R = point_add(point_mul(G, s), point_mul(P, n - e)) + if R is None: + return False + return has_even_y(R) and (R[0] == r) + + +def schnorr_sign(msg: bytes, seckey: bytes, aux_rand: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= n - 1): + raise ValueError("The secret key must be in the range 1..n-1.") + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes.") + P = point_mul(G, d0) + assert P is not None + d = d0 if has_even_y(P) else n - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash("BIP0340/aux", aux_rand)) + k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n + if k0 == 0: + raise RuntimeError("Failure. This happens only with negligible probability.") + R = point_mul(G, k0) + assert R is not None + k = k0 if has_even_y(R) else n - k0 + e = int_from_bytes(tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)) % n + sig = bytes_from_point(R) + bytes_from_int((k + e * d) % n) + if not schnorr_verify(msg, bytes_from_point(P), sig): + raise RuntimeError("The created signature does not pass verification.") + return sig + + +def parse_hex(data: str, expected_len: int, field_name: str) -> bytes: + raw = bytes.fromhex(data) + if len(raw) != expected_len: + raise ValueError(f"{field_name} must be {expected_len} bytes.") + return raw + + +def derive_signing_key(spend_seckey: bytes, tweak: bytes, output_pubkey: bytes) -> Tuple[int, int, bool]: + b_spend = int_from_bytes(spend_seckey) + if not (1 <= b_spend <= n - 1): + raise ValueError("spend key out of range") + + tweak_int = int_from_bytes(tweak) + d_raw = (b_spend + tweak_int) % n + if d_raw == 0: + raise ValueError("tweaked private key is zero") + + Q = point_mul(G, d_raw) + assert Q is not None + negated = not has_even_y(Q) + d = d_raw if not negated else n - d_raw + + Q_even = point_mul(G, d) + assert Q_even is not None + if bytes_from_point(Q_even) != output_pubkey: + raise ValueError("tweaked key does not match output key") + + return d_raw, d, negated + + +def run_test_vectors(path: Path) -> bool: + vectors = json.loads(path.read_text(encoding="utf-8")) + all_passed = True + + valid_vectors = vectors.get("valid", []) + invalid_vectors = vectors.get("invalid", []) + + print(f"Running {len(valid_vectors)} valid vectors") + for index, vector in enumerate(valid_vectors): + description = vector["description"] + given = vector["given"] + expected = vector["expected"] + print(f"- valid[{index}] {description}") + try: + spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") + tweak = parse_hex(given["tweak"], 32, "tweak") + output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") + message = parse_hex(given["message"], 32, "message") + aux_rand = parse_hex(given["aux_rand"], 32, "aux_rand") + + d_raw, d, negated = derive_signing_key(spend_seckey, tweak, output_pubkey) + signature = schnorr_sign(message, bytes_from_int(d), aux_rand) + + assert bytes_from_int(d_raw).hex() == expected["raw_tweaked_seckey"] + assert negated == expected["negated"] + assert bytes_from_int(d).hex() == expected["final_seckey"] + assert signature.hex() == expected["signature"] + except Exception as exc: + all_passed = False + print(f" FAILED: {exc}") + + print(f"Running {len(invalid_vectors)} invalid vectors") + for index, vector in enumerate(invalid_vectors): + description = vector["description"] + given = vector["given"] + error_substr = vector["error_substr"] + print(f"- invalid[{index}] {description}") + try: + spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") + tweak = parse_hex(given["tweak"], 32, "tweak") + output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") + derive_signing_key(spend_seckey, tweak, output_pubkey) + all_passed = False + print(" FAILED: expected an exception") + except Exception as exc: + if error_substr not in str(exc): + all_passed = False + print(f" FAILED: wrong error, got: {exc}") + + print("All test vectors passed." if all_passed else "Some test vectors failed.") + return all_passed + + +def main() -> int: + if len(sys.argv) > 2: + print(f"Usage: {sys.argv[0]} [test-vectors.json]") + return 1 + + if len(sys.argv) == 2: + vector_path = Path(sys.argv[1]) + else: + vector_path = Path(__file__).with_name("test-vectors.json") + + if not vector_path.is_file(): + print(f"Vector file not found: {vector_path}") + return 1 + + return 0 if run_test_vectors(vector_path) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bip-0376/test-vectors.json b/bip-0376/test-vectors.json new file mode 100644 index 0000000000..5014b37f89 --- /dev/null +++ b/bip-0376/test-vectors.json @@ -0,0 +1,65 @@ +{ + "valid": [ + { + "description": "No negation required; tweaked key directly matches output key", + "given": { + "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", + "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", + "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028", + "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4", + "aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10" + }, + "expected": { + "raw_tweaked_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b", + "negated": false, + "final_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b", + "signature": "d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f9417778" + } + }, + { + "description": "Negation required because (b_spend + tweak)G has odd Y", + "given": { + "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", + "tweak": "58e2385eb96d1c906bbd807eafd1fddb80fb2f43026a16386a400e6832644cbc", + "output_pubkey": "db0edc417c73c567add118de8d138b2d0b64083f0a1bd8e876936415de7edc46", + "message": "a78521e49048b6e0d368d3fba417fc20c7546272dafa78a8a173fcca6c81233b", + "aux_rand": "6b31977a8ac73ede3f3653ea0d96bc3656242461e31d771985a0b17084d3cf91" + }, + "expected": { + "raw_tweaked_seckey": "7fa902ec0eabdbe69f8853746e9f5e664c94eef0e36a688d1499cab7f5d6e516", + "negated": true, + "final_seckey": "8056fd13f15424196077ac8b9160a1986e19edf5cbde37aeab3893d4da5f5c2b", + "signature": "3df67213afd895a833bc046e9455c77a7e40165638ad489669a5c498d4d71ab565a9c54abda2e23e931f7a0f78a9f151bba07b8400b7b96d24f857c8ba65c022" + } + } + ], + "invalid": [ + { + "description": "Tweaked key does not match output key", + "given": { + "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", + "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", + "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da029" + }, + "error_substr": "tweaked key does not match output key" + }, + { + "description": "Tweaked private key is zero", + "given": { + "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", + "tweak": "d9393572aac140a9cc352d0a41329f73ef151d38ce484de71578a23d0cc3a8e7", + "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" + }, + "error_substr": "tweaked private key is zero" + }, + { + "description": "Spend key out of range", + "given": { + "spend_seckey": "0000000000000000000000000000000000000000000000000000000000000000", + "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", + "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" + }, + "error_substr": "spend key out of range" + } + ] +} From b0b2cf47a99226029928cc22763a09da3271f37c Mon Sep 17 00:00:00 2001 From: Lucia Date: Sat, 9 May 2026 21:33:21 +1200 Subject: [PATCH 2/5] BIP-0376: use PSBT vectors for signer tests --- bip-0376.mediawiki | 3 +- bip-0376/psbt_bip376.py | 103 +++++++++++++++++++++ bip-0376/reference.py | 180 +++++++++++-------------------------- bip-0376/test-vectors.json | 16 +--- 4 files changed, 161 insertions(+), 141 deletions(-) create mode 100644 bip-0376/psbt_bip376.py diff --git a/bip-0376.mediawiki b/bip-0376.mediawiki index 12987ee72f..57777df2db 100644 --- a/bip-0376.mediawiki +++ b/bip-0376.mediawiki @@ -147,6 +147,7 @@ These are new fields added to the existing PSBT format. Because PSBT is designed == Reference implementation == A Python reference implementation is provided in [[bip-0376/reference.py|bip-0376/reference.py]]. +It uses the vendored bitcoin_test PSBT components and secp256k1lab test-only secp256k1 implementation from BIP 375. It demonstrates the Signer behavior specified in this BIP: @@ -161,7 +162,7 @@ Machine-readable test vectors are provided in [[bip-0376/test-vectors.json| Optional[bytes]: + if key_data == b"": + return self.map.get(key_type) + return self.map.get(bytes([key_type]) + key_data) + + def set_by_key(self, key_type: int, value_data: bytes, key_data: bytes = b"") -> None: + if key_data == b"": + self.map[key_type] = value_data + else: + self.map[bytes([key_type]) + key_data] = value_data + + +class BIP376PSBT(PSBT): + """PSBT that deserializes maps as BIP376PSBTMap instances.""" + + def deserialize(self, f): + assert f.read(5) == b"psbt\xff" + self.g = from_binary(BIP376PSBTMap, f) + + self.version = 0 + if PSBT_GLOBAL_VERSION in self.g.map: + assert PSBT_GLOBAL_INPUT_COUNT in self.g.map + assert PSBT_GLOBAL_OUTPUT_COUNT in self.g.map + self.version = struct.unpack(" bytes: + witness_utxo = input_map.get(PSBT_IN_WITNESS_UTXO) + if witness_utxo is None: + raise ValueError("missing PSBT_IN_WITNESS_UTXO") + + txout = from_binary(CTxOut, witness_utxo) + script_pubkey = txout.scriptPubKey + if len(script_pubkey) != 34 or script_pubkey[:2] != b"\x51\x20": + raise ValueError("PSBT_IN_WITNESS_UTXO is not a P2TR output") + return script_pubkey[2:] + + +def get_sp_tweak(input_map: BIP376PSBTMap) -> bytes: + tweak = input_map.get(PSBT_IN_SP_TWEAK) + if tweak is None: + raise ValueError("missing PSBT_IN_SP_TWEAK") + if len(tweak) != 32: + raise ValueError("PSBT_IN_SP_TWEAK must be 32 bytes") + return tweak + + +def set_tap_key_sig(input_map: BIP376PSBTMap, signature: bytes) -> None: + if len(signature) not in (64, 65): + raise ValueError("PSBT_IN_TAP_KEY_SIG must be 64 or 65 bytes") + input_map.set_by_key(PSBT_IN_TAP_KEY_SIG, signature) diff --git a/bip-0376/reference.py b/bip-0376/reference.py index d5b63fad9c..a02728435c 100755 --- a/bip-0376/reference.py +++ b/bip-0376/reference.py @@ -7,117 +7,25 @@ import json import sys -import hashlib from pathlib import Path -from typing import Optional, Tuple -p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -G = ( - 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, - 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, +BIP375_DIR = Path(__file__).resolve().parents[1] / "bip-0375" +DEPS_DIR = BIP375_DIR / "deps" +SECP256K1LAB_DIR = DEPS_DIR / "secp256k1lab/src" +for dependency_path in (BIP375_DIR, DEPS_DIR, SECP256K1LAB_DIR): + sys.path.insert(0, str(dependency_path)) + +from secp256k1lab.bip340 import schnorr_sign +from secp256k1lab.secp256k1 import G, Scalar + +from psbt_bip376 import ( + BIP376PSBT, + PSBT_IN_SP_TWEAK, + get_p2tr_witness_utxo_output_key, + get_sp_tweak, + set_tap_key_sig, ) -Point = Tuple[int, int] - - -def tagged_hash(tag: str, msg: bytes) -> bytes: - tag_hash = hashlib.sha256(tag.encode("utf-8")).digest() - return hashlib.sha256(tag_hash + tag_hash + msg).digest() - - -def int_from_bytes(data: bytes) -> int: - return int.from_bytes(data, byteorder="big") - - -def bytes_from_int(x: int) -> bytes: - return x.to_bytes(32, byteorder="big") - - -def has_even_y(P: Point) -> bool: - return (P[1] % 2) == 0 - - -def bytes_from_point(P: Point) -> bytes: - return bytes_from_int(P[0]) - - -def xor_bytes(a: bytes, b: bytes) -> bytes: - return bytes(x ^ y for (x, y) in zip(a, b)) - - -def lift_x(x_coord: int) -> Optional[Point]: - if x_coord >= p: - return None - y_sq = (pow(x_coord, 3, p) + 7) % p - y_coord = pow(y_sq, (p + 1) // 4, p) - if pow(y_coord, 2, p) != y_sq: - return None - return (x_coord, y_coord if (y_coord % 2) == 0 else p - y_coord) - - -def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: - if P1 is None: - return P2 - if P2 is None: - return P1 - if (P1[0] == P2[0]) and (P1[1] != P2[1]): - return None - if P1 == P2: - lam = (3 * P1[0] * P1[0] * pow(2 * P1[1], p - 2, p)) % p - else: - lam = ((P2[1] - P1[1]) * pow(P2[0] - P1[0], p - 2, p)) % p - x3 = (lam * lam - P1[0] - P2[0]) % p - y3 = (lam * (P1[0] - x3) - P1[1]) % p - return (x3, y3) - - -def point_mul(P: Optional[Point], scalar: int) -> Optional[Point]: - R = None - for i in range(256): - if (scalar >> i) & 1: - R = point_add(R, P) - P = point_add(P, P) - return R - - -def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: - if len(pubkey) != 32 or len(sig) != 64: - return False - P = lift_x(int_from_bytes(pubkey)) - r = int_from_bytes(sig[0:32]) - s = int_from_bytes(sig[32:64]) - if P is None or r >= p or s >= n: - return False - e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n - R = point_add(point_mul(G, s), point_mul(P, n - e)) - if R is None: - return False - return has_even_y(R) and (R[0] == r) - - -def schnorr_sign(msg: bytes, seckey: bytes, aux_rand: bytes) -> bytes: - d0 = int_from_bytes(seckey) - if not (1 <= d0 <= n - 1): - raise ValueError("The secret key must be in the range 1..n-1.") - if len(aux_rand) != 32: - raise ValueError("aux_rand must be 32 bytes.") - P = point_mul(G, d0) - assert P is not None - d = d0 if has_even_y(P) else n - d0 - t = xor_bytes(bytes_from_int(d), tagged_hash("BIP0340/aux", aux_rand)) - k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n - if k0 == 0: - raise RuntimeError("Failure. This happens only with negligible probability.") - R = point_mul(G, k0) - assert R is not None - k = k0 if has_even_y(R) else n - k0 - e = int_from_bytes(tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)) % n - sig = bytes_from_point(R) + bytes_from_int((k + e * d) % n) - if not schnorr_verify(msg, bytes_from_point(P), sig): - raise RuntimeError("The created signature does not pass verification.") - return sig - def parse_hex(data: str, expected_len: int, field_name: str) -> bytes: raw = bytes.fromhex(data) @@ -126,29 +34,45 @@ def parse_hex(data: str, expected_len: int, field_name: str) -> bytes: return raw -def derive_signing_key(spend_seckey: bytes, tweak: bytes, output_pubkey: bytes) -> Tuple[int, int, bool]: - b_spend = int_from_bytes(spend_seckey) - if not (1 <= b_spend <= n - 1): +def derive_signing_key( + spend_seckey: bytes, tweak: bytes, output_pubkey: bytes +) -> tuple[Scalar, Scalar, bool]: + try: + b_spend = Scalar.from_bytes_checked(spend_seckey) + except ValueError as exc: + raise ValueError("spend key out of range") from exc + if b_spend == 0: raise ValueError("spend key out of range") - tweak_int = int_from_bytes(tweak) - d_raw = (b_spend + tweak_int) % n + d_raw = b_spend + Scalar.from_bytes_wrapping(tweak) if d_raw == 0: raise ValueError("tweaked private key is zero") - Q = point_mul(G, d_raw) - assert Q is not None - negated = not has_even_y(Q) - d = d_raw if not negated else n - d_raw + Q = d_raw * G + assert not Q.infinity + negated = not Q.has_even_y() + d = d_raw if not negated else -d_raw - Q_even = point_mul(G, d) - assert Q_even is not None - if bytes_from_point(Q_even) != output_pubkey: + Q_even = d * G + assert not Q_even.infinity + if Q_even.to_bytes_xonly() != output_pubkey: raise ValueError("tweaked key does not match output key") return d_raw, d, negated +def sign_psbt(psbt_data: str, spend_seckey: bytes, message: bytes, aux_rand: bytes) -> str: + psbt = BIP376PSBT.from_base64(psbt_data) + for input_map in psbt.i: + if input_map.get(PSBT_IN_SP_TWEAK) is None: + continue + tweak = get_sp_tweak(input_map) + output_pubkey = get_p2tr_witness_utxo_output_key(input_map) + _, d, _ = derive_signing_key(spend_seckey, tweak, output_pubkey) + set_tap_key_sig(input_map, schnorr_sign(message, d.to_bytes(), aux_rand)) + return psbt.to_base64() + + def run_test_vectors(path: Path) -> bool: vectors = json.loads(path.read_text(encoding="utf-8")) all_passed = True @@ -164,18 +88,18 @@ def run_test_vectors(path: Path) -> bool: print(f"- valid[{index}] {description}") try: spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") - tweak = parse_hex(given["tweak"], 32, "tweak") - output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") message = parse_hex(given["message"], 32, "message") aux_rand = parse_hex(given["aux_rand"], 32, "aux_rand") - d_raw, d, negated = derive_signing_key(spend_seckey, tweak, output_pubkey) - signature = schnorr_sign(message, bytes_from_int(d), aux_rand) - - assert bytes_from_int(d_raw).hex() == expected["raw_tweaked_seckey"] - assert negated == expected["negated"] - assert bytes_from_int(d).hex() == expected["final_seckey"] - assert signature.hex() == expected["signature"] + if "psbt" in given: + signed_psbt = sign_psbt(given["psbt"], spend_seckey, message, aux_rand) + assert signed_psbt == expected["psbt"] + else: + tweak = parse_hex(given["tweak"], 32, "tweak") + output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") + _, d, _ = derive_signing_key(spend_seckey, tweak, output_pubkey) + signature = schnorr_sign(message, d.to_bytes(), aux_rand) + assert signature.hex() == expected["signature"] except Exception as exc: all_passed = False print(f" FAILED: {exc}") diff --git a/bip-0376/test-vectors.json b/bip-0376/test-vectors.json index 5014b37f89..a28d9a54fa 100644 --- a/bip-0376/test-vectors.json +++ b/bip-0376/test-vectors.json @@ -4,32 +4,24 @@ "description": "No negation required; tweaked key directly matches output key", "given": { "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", - "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028", + "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEAAQMIhAMAAAAAAAABBAFqAA==", "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4", "aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10" }, "expected": { - "raw_tweaked_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b", - "negated": false, - "final_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b", - "signature": "d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f9417778" + "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEBE0DQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd4AAEDCIQDAAAAAAAAAQQBagA=" } }, { "description": "Negation required because (b_spend + tweak)G has odd Y", "given": { "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "tweak": "58e2385eb96d1c906bbd807eafd1fddb80fb2f43026a16386a400e6832644cbc", - "output_pubkey": "db0edc417c73c567add118de8d138b2d0b64083f0a1bd8e876936415de7edc46", + "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgWOI4XrltHJBrvYB+r9H924D7L0MCahY4akAOaDJkTLwAAQMIhQMAAAAAAAABBAFqAA==", "message": "a78521e49048b6e0d368d3fba417fc20c7546272dafa78a8a173fcca6c81233b", "aux_rand": "6b31977a8ac73ede3f3653ea0d96bc3656242461e31d771985a0b17084d3cf91" }, "expected": { - "raw_tweaked_seckey": "7fa902ec0eabdbe69f8853746e9f5e664c94eef0e36a688d1499cab7f5d6e516", - "negated": true, - "final_seckey": "8056fd13f15424196077ac8b9160a1986e19edf5cbde37aeab3893d4da5f5c2b", - "signature": "3df67213afd895a833bc046e9455c77a7e40165638ad489669a5c498d4d71ab565a9c54abda2e23e931f7a0f78a9f151bba07b8400b7b96d24f857c8ba65c022" + "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgWOI4XrltHJBrvYB+r9H924D7L0MCahY4akAOaDJkTLwBE0A99nITr9iVqDO8BG6UVcd6fkAWVjitSJZppcSY1NcatWWpxUq9ouI+kx96D3ip8VG7oHuEALe5bST4V8i6ZcAiAAEDCIUDAAAAAAAAAQQBagA=" } } ], From 9ebb27f651e92993959575087de1fa5cae8533bf Mon Sep 17 00:00:00 2001 From: Lucia Date: Sat, 9 May 2026 21:33:22 +1200 Subject: [PATCH 3/5] bip-0376: remove error message checks from invalid vectors --- bip-0376/reference.py | 7 ++----- bip-0376/test-vectors.json | 9 +++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/bip-0376/reference.py b/bip-0376/reference.py index a02728435c..b5bb793396 100755 --- a/bip-0376/reference.py +++ b/bip-0376/reference.py @@ -108,7 +108,6 @@ def run_test_vectors(path: Path) -> bool: for index, vector in enumerate(invalid_vectors): description = vector["description"] given = vector["given"] - error_substr = vector["error_substr"] print(f"- invalid[{index}] {description}") try: spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") @@ -117,10 +116,8 @@ def run_test_vectors(path: Path) -> bool: derive_signing_key(spend_seckey, tweak, output_pubkey) all_passed = False print(" FAILED: expected an exception") - except Exception as exc: - if error_substr not in str(exc): - all_passed = False - print(f" FAILED: wrong error, got: {exc}") + except Exception: + pass print("All test vectors passed." if all_passed else "Some test vectors failed.") return all_passed diff --git a/bip-0376/test-vectors.json b/bip-0376/test-vectors.json index a28d9a54fa..af899fab65 100644 --- a/bip-0376/test-vectors.json +++ b/bip-0376/test-vectors.json @@ -32,8 +32,7 @@ "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da029" - }, - "error_substr": "tweaked key does not match output key" + } }, { "description": "Tweaked private key is zero", @@ -41,8 +40,7 @@ "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", "tweak": "d9393572aac140a9cc352d0a41329f73ef151d38ce484de71578a23d0cc3a8e7", "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" - }, - "error_substr": "tweaked private key is zero" + } }, { "description": "Spend key out of range", @@ -50,8 +48,7 @@ "spend_seckey": "0000000000000000000000000000000000000000000000000000000000000000", "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" - }, - "error_substr": "spend key out of range" + } } ] } From 58a43d40f2035735f814b4b83f1aaa72f12c8f87 Mon Sep 17 00:00:00 2001 From: Lucia Date: Sat, 9 May 2026 21:36:11 +1200 Subject: [PATCH 4/5] BIP-0376: use PSBT vectors for signer tests From b718f2e61098a5043da4d9d2b6d9d376938cd704 Mon Sep 17 00:00:00 2001 From: Lucia Date: Sat, 9 May 2026 21:33:26 +1200 Subject: [PATCH 5/5] bip-0376: vendor test deps and add PSBTv2 vectors --- bip-0376/deps/bitcoin_test/messages.py | 449 +++++++++++++++++ bip-0376/deps/bitcoin_test/psbt.py | 197 ++++++++ .../secp256k1lab/.github/workflows/main.yml | 17 + bip-0376/deps/secp256k1lab/.python-version | 1 + bip-0376/deps/secp256k1lab/CHANGELOG.md | 10 + bip-0376/deps/secp256k1lab/COPYING | 23 + bip-0376/deps/secp256k1lab/README.md | 13 + bip-0376/deps/secp256k1lab/pyproject.toml | 34 ++ .../secp256k1lab/src/secp256k1lab/__init__.py | 0 .../secp256k1lab/src/secp256k1lab/bip340.py | 73 +++ .../secp256k1lab/src/secp256k1lab/ecdh.py | 16 + .../secp256k1lab/src/secp256k1lab/keys.py | 15 + .../secp256k1lab/src/secp256k1lab/py.typed | 0 .../src/secp256k1lab/secp256k1.py | 454 ++++++++++++++++++ .../secp256k1lab/src/secp256k1lab/util.py | 24 + bip-0376/psbt_bip376.py | 62 ++- bip-0376/reference.py | 157 ++++-- bip-0376/test-vectors.json | 143 +++++- 18 files changed, 1597 insertions(+), 91 deletions(-) create mode 100644 bip-0376/deps/bitcoin_test/messages.py create mode 100644 bip-0376/deps/bitcoin_test/psbt.py create mode 100644 bip-0376/deps/secp256k1lab/.github/workflows/main.yml create mode 100644 bip-0376/deps/secp256k1lab/.python-version create mode 100644 bip-0376/deps/secp256k1lab/CHANGELOG.md create mode 100644 bip-0376/deps/secp256k1lab/COPYING create mode 100644 bip-0376/deps/secp256k1lab/README.md create mode 100644 bip-0376/deps/secp256k1lab/pyproject.toml create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/__init__.py create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/bip340.py create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/ecdh.py create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/keys.py create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/py.typed create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/secp256k1.py create mode 100644 bip-0376/deps/secp256k1lab/src/secp256k1lab/util.py diff --git a/bip-0376/deps/bitcoin_test/messages.py b/bip-0376/deps/bitcoin_test/messages.py new file mode 100644 index 0000000000..90d2630301 --- /dev/null +++ b/bip-0376/deps/bitcoin_test/messages.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Bitcoin test framework primitive and message structures + +CBlock, CTransaction, CBlockHeader, CTxIn, CTxOut, etc....: + data structures that should map to corresponding structures in + bitcoin/primitives + +msg_block, msg_tx, msg_headers, etc.: + data structures that represent network messages + +ser_*, deser_*: functions that handle serialization/deserialization. + +Classes use __slots__ to ensure extraneous attributes aren't accidentally added +by tests, compromising their intended effect. +""" + +######################################################################## +# Adapted from Bitcoin Core test framework messages.py +# for BIP-375 PSBT validation tests. +######################################################################## + +import copy +import hashlib +import math +from io import BytesIO + +COIN = 100000000 # 1 btc in satoshis +WITNESS_SCALE_FACTOR = 4 + +# ============================================================================ +# Serialization utilities +# ============================================================================ + +def hash160(s: bytes) -> bytes: + return hashlib.new("ripemd160", sha256(s)).digest() + + +def sha256(s: bytes) -> bytes: + return hashlib.sha256(s).digest() + + +def hash256(s: bytes) -> bytes: + return sha256(sha256(s)) + + +def ser_compact_size(l): + r = b"" + if l < 253: + r = l.to_bytes(1, "little") + elif l < 0x10000: + r = (253).to_bytes(1, "little") + l.to_bytes(2, "little") + elif l < 0x100000000: + r = (254).to_bytes(1, "little") + l.to_bytes(4, "little") + else: + r = (255).to_bytes(1, "little") + l.to_bytes(8, "little") + return r + + +def deser_compact_size(f): + nit = int.from_bytes(f.read(1), "little") + if nit == 253: + nit = int.from_bytes(f.read(2), "little") + elif nit == 254: + nit = int.from_bytes(f.read(4), "little") + elif nit == 255: + nit = int.from_bytes(f.read(8), "little") + return nit + + +def ser_varint(l): + r = b"" + while True: + r = bytes([(l & 0x7f) | (0x80 if len(r) > 0 else 0x00)]) + r + if l <= 0x7f: + return r + l = (l >> 7) - 1 + + +def deser_varint(f): + n = 0 + while True: + dat = f.read(1)[0] + n = (n << 7) | (dat & 0x7f) + if (dat & 0x80) > 0: + n += 1 + else: + return n + + +def deser_string(f): + nit = deser_compact_size(f) + return f.read(nit) + + +def ser_string(s): + return ser_compact_size(len(s)) + s + + +def deser_uint256(f): + return int.from_bytes(f.read(32), 'little') + + +def ser_uint256(u): + return u.to_bytes(32, 'little') + + +def uint256_from_str(s): + return int.from_bytes(s[:32], 'little') + + +def uint256_from_compact(c): + nbytes = (c >> 24) & 0xFF + v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) + return v + + +# deser_function_name: Allow for an alternate deserialization function on the +# entries in the vector. +def deser_vector(f, c, deser_function_name=None): + nit = deser_compact_size(f) + r = [] + for _ in range(nit): + t = c() + if deser_function_name: + getattr(t, deser_function_name)(f) + else: + t.deserialize(f) + r.append(t) + return r + + +# ser_function_name: Allow for an alternate serialization function on the +# entries in the vector (we use this for serializing the vector of transactions +# for a witness block). +def ser_vector(l, ser_function_name=None): + r = ser_compact_size(len(l)) + for i in l: + if ser_function_name: + r += getattr(i, ser_function_name)() + else: + r += i.serialize() + return r + + +def deser_uint256_vector(f): + nit = deser_compact_size(f) + r = [] + for _ in range(nit): + t = deser_uint256(f) + r.append(t) + return r + + +def ser_uint256_vector(l): + r = ser_compact_size(len(l)) + for i in l: + r += ser_uint256(i) + return r + + +def deser_string_vector(f): + nit = deser_compact_size(f) + r = [] + for _ in range(nit): + t = deser_string(f) + r.append(t) + return r + + +def ser_string_vector(l): + r = ser_compact_size(len(l)) + for sv in l: + r += ser_string(sv) + return r + +# like from_hex, but without the hex part +def from_binary(cls, stream): + """deserialize a binary stream (or bytes object) into an object""" + # handle bytes object by turning it into a stream + was_bytes = isinstance(stream, bytes) + if was_bytes: + stream = BytesIO(stream) + obj = cls() + obj.deserialize(stream) + if was_bytes: + assert len(stream.read()) == 0 + return obj + + +# ============================================================================ +# Transaction data structures +# ============================================================================ + +class COutPoint: + __slots__ = ("hash", "n") + + def __init__(self, hash=0, n=0): + self.hash = hash + self.n = n + + def deserialize(self, f): + self.hash = deser_uint256(f) + self.n = int.from_bytes(f.read(4), "little") + + def serialize(self): + r = b"" + r += ser_uint256(self.hash) + r += self.n.to_bytes(4, "little") + return r + + def __repr__(self): + return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) + +class CTxIn: + __slots__ = ("nSequence", "prevout", "scriptSig") + + def __init__(self, outpoint=None, scriptSig=b"", nSequence=0): + if outpoint is None: + self.prevout = COutPoint() + else: + self.prevout = outpoint + self.scriptSig = scriptSig + self.nSequence = nSequence + + def deserialize(self, f): + self.prevout = COutPoint() + self.prevout.deserialize(f) + self.scriptSig = deser_string(f) + self.nSequence = int.from_bytes(f.read(4), "little") + + def serialize(self): + r = b"" + r += self.prevout.serialize() + r += ser_string(self.scriptSig) + r += self.nSequence.to_bytes(4, "little") + return r + + def __repr__(self): + return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ + % (repr(self.prevout), self.scriptSig.hex(), + self.nSequence) + + +class CTxOut: + __slots__ = ("nValue", "scriptPubKey") + + def __init__(self, nValue=0, scriptPubKey=b""): + self.nValue = nValue + self.scriptPubKey = scriptPubKey + + def deserialize(self, f): + self.nValue = int.from_bytes(f.read(8), "little", signed=True) + self.scriptPubKey = deser_string(f) + + def serialize(self): + r = b"" + r += self.nValue.to_bytes(8, "little", signed=True) + r += ser_string(self.scriptPubKey) + return r + + def __repr__(self): + return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ + % (self.nValue // COIN, self.nValue % COIN, + self.scriptPubKey.hex()) + + +class CScriptWitness: + __slots__ = ("stack",) + + def __init__(self): + # stack is a vector of strings + self.stack = [] + + def __repr__(self): + return "CScriptWitness(%s)" % \ + (",".join([x.hex() for x in self.stack])) + + def is_null(self): + if self.stack: + return False + return True + + +class CTxInWitness: + __slots__ = ("scriptWitness",) + + def __init__(self): + self.scriptWitness = CScriptWitness() + + def deserialize(self, f): + self.scriptWitness.stack = deser_string_vector(f) + + def serialize(self): + return ser_string_vector(self.scriptWitness.stack) + + def __repr__(self): + return repr(self.scriptWitness) + + def is_null(self): + return self.scriptWitness.is_null() + + +class CTxWitness: + __slots__ = ("vtxinwit",) + + def __init__(self): + self.vtxinwit = [] + + def deserialize(self, f): + for i in range(len(self.vtxinwit)): + self.vtxinwit[i].deserialize(f) + + def serialize(self): + r = b"" + # This is different than the usual vector serialization -- + # we omit the length of the vector, which is required to be + # the same length as the transaction's vin vector. + for x in self.vtxinwit: + r += x.serialize() + return r + + def __repr__(self): + return "CTxWitness(%s)" % \ + (';'.join([repr(x) for x in self.vtxinwit])) + + def is_null(self): + for x in self.vtxinwit: + if not x.is_null(): + return False + return True + + +class CTransaction: + __slots__ = ("nLockTime", "version", "vin", "vout", "wit") + + def __init__(self, tx=None): + if tx is None: + self.version = 2 + self.vin = [] + self.vout = [] + self.wit = CTxWitness() + self.nLockTime = 0 + else: + self.version = tx.version + self.vin = copy.deepcopy(tx.vin) + self.vout = copy.deepcopy(tx.vout) + self.nLockTime = tx.nLockTime + self.wit = copy.deepcopy(tx.wit) + + def deserialize(self, f): + self.version = int.from_bytes(f.read(4), "little") + self.vin = deser_vector(f, CTxIn) + flags = 0 + if len(self.vin) == 0: + flags = int.from_bytes(f.read(1), "little") + # Not sure why flags can't be zero, but this + # matches the implementation in bitcoind + if (flags != 0): + self.vin = deser_vector(f, CTxIn) + self.vout = deser_vector(f, CTxOut) + else: + self.vout = deser_vector(f, CTxOut) + if flags != 0: + self.wit.vtxinwit = [CTxInWitness() for _ in range(len(self.vin))] + self.wit.deserialize(f) + else: + self.wit = CTxWitness() + self.nLockTime = int.from_bytes(f.read(4), "little") + + def serialize_without_witness(self): + r = b"" + r += self.version.to_bytes(4, "little") + r += ser_vector(self.vin) + r += ser_vector(self.vout) + r += self.nLockTime.to_bytes(4, "little") + return r + + # Only serialize with witness when explicitly called for + def serialize_with_witness(self): + flags = 0 + if not self.wit.is_null(): + flags |= 1 + r = b"" + r += self.version.to_bytes(4, "little") + if flags: + dummy = [] + r += ser_vector(dummy) + r += flags.to_bytes(1, "little") + r += ser_vector(self.vin) + r += ser_vector(self.vout) + if flags & 1: + if (len(self.wit.vtxinwit) != len(self.vin)): + # vtxinwit must have the same length as vin + self.wit.vtxinwit = self.wit.vtxinwit[:len(self.vin)] + for _ in range(len(self.wit.vtxinwit), len(self.vin)): + self.wit.vtxinwit.append(CTxInWitness()) + r += self.wit.serialize() + r += self.nLockTime.to_bytes(4, "little") + return r + + # Regular serialization is with witness -- must explicitly + # call serialize_without_witness to exclude witness data. + def serialize(self): + return self.serialize_with_witness() + + @property + def wtxid_hex(self): + """Return wtxid (transaction hash with witness) as hex string.""" + return hash256(self.serialize())[::-1].hex() + + @property + def wtxid_int(self): + """Return wtxid (transaction hash with witness) as integer.""" + return uint256_from_str(hash256(self.serialize_with_witness())) + + @property + def txid_hex(self): + """Return txid (transaction hash without witness) as hex string.""" + return hash256(self.serialize_without_witness())[::-1].hex() + + @property + def txid_int(self): + """Return txid (transaction hash without witness) as integer.""" + return uint256_from_str(hash256(self.serialize_without_witness())) + + def is_valid(self): + for tout in self.vout: + if tout.nValue < 0 or tout.nValue > 21000000 * COIN: + return False + return True + + # Calculate the transaction weight using witness and non-witness + # serialization size (does NOT use sigops). + def get_weight(self): + with_witness_size = len(self.serialize_with_witness()) + without_witness_size = len(self.serialize_without_witness()) + return (WITNESS_SCALE_FACTOR - 1) * without_witness_size + with_witness_size + + def get_vsize(self): + return math.ceil(self.get_weight() / WITNESS_SCALE_FACTOR) + + def __repr__(self): + return "CTransaction(version=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ + % (self.version, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) \ No newline at end of file diff --git a/bip-0376/deps/bitcoin_test/psbt.py b/bip-0376/deps/bitcoin_test/psbt.py new file mode 100644 index 0000000000..2820430865 --- /dev/null +++ b/bip-0376/deps/bitcoin_test/psbt.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +######################################################################## +# Adapted from Bitcoin Core test framework psbt.py +# for BIP-375 PSBT validation tests. +######################################################################## + +import base64 +import struct + +from io import BytesIO + +from .messages import ( + CTransaction, + deser_string, + deser_compact_size, + from_binary, + ser_compact_size, +) + + +# global types +PSBT_GLOBAL_UNSIGNED_TX = 0x00 +PSBT_GLOBAL_XPUB = 0x01 +PSBT_GLOBAL_TX_VERSION = 0x02 +PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03 +PSBT_GLOBAL_INPUT_COUNT = 0x04 +PSBT_GLOBAL_OUTPUT_COUNT = 0x05 +PSBT_GLOBAL_TX_MODIFIABLE = 0x06 +PSBT_GLOBAL_VERSION = 0xfb +PSBT_GLOBAL_PROPRIETARY = 0xfc + +# per-input types +PSBT_IN_NON_WITNESS_UTXO = 0x00 +PSBT_IN_WITNESS_UTXO = 0x01 +PSBT_IN_PARTIAL_SIG = 0x02 +PSBT_IN_SIGHASH_TYPE = 0x03 +PSBT_IN_REDEEM_SCRIPT = 0x04 +PSBT_IN_WITNESS_SCRIPT = 0x05 +PSBT_IN_BIP32_DERIVATION = 0x06 +PSBT_IN_FINAL_SCRIPTSIG = 0x07 +PSBT_IN_FINAL_SCRIPTWITNESS = 0x08 +PSBT_IN_POR_COMMITMENT = 0x09 +PSBT_IN_RIPEMD160 = 0x0a +PSBT_IN_SHA256 = 0x0b +PSBT_IN_HASH160 = 0x0c +PSBT_IN_HASH256 = 0x0d +PSBT_IN_PREVIOUS_TXID = 0x0e +PSBT_IN_OUTPUT_INDEX = 0x0f +PSBT_IN_SEQUENCE = 0x10 +PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11 +PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12 +PSBT_IN_TAP_KEY_SIG = 0x13 +PSBT_IN_TAP_SCRIPT_SIG = 0x14 +PSBT_IN_TAP_LEAF_SCRIPT = 0x15 +PSBT_IN_TAP_BIP32_DERIVATION = 0x16 +PSBT_IN_TAP_INTERNAL_KEY = 0x17 +PSBT_IN_TAP_MERKLE_ROOT = 0x18 +PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a +PSBT_IN_MUSIG2_PUB_NONCE = 0x1b +PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c +PSBT_IN_PROPRIETARY = 0xfc + +# per-output types +PSBT_OUT_REDEEM_SCRIPT = 0x00 +PSBT_OUT_WITNESS_SCRIPT = 0x01 +PSBT_OUT_BIP32_DERIVATION = 0x02 +PSBT_OUT_AMOUNT = 0x03 +PSBT_OUT_SCRIPT = 0x04 +PSBT_OUT_TAP_INTERNAL_KEY = 0x05 +PSBT_OUT_TAP_TREE = 0x06 +PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 +PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 +PSBT_OUT_PROPRIETARY = 0xfc + + +class PSBTMap: + """Class for serializing and deserializing PSBT maps""" + + def __init__(self, map=None): + self.map = map if map is not None else {} + + def deserialize(self, f): + m = {} + while True: + k = deser_string(f) + if len(k) == 0: + break + v = deser_string(f) + if len(k) == 1: + k = k[0] + assert k not in m + m[k] = v + self.map = m + + def serialize(self): + m = b"" + for k,v in self.map.items(): + if isinstance(k, int) and 0 <= k and k <= 255: + k = bytes([k]) + if isinstance(v, list): + assert all(type(elem) is bytes for elem in v) + v = b"".join(v) # simply concatenate the byte-strings w/o size prefixes + m += ser_compact_size(len(k)) + k + m += ser_compact_size(len(v)) + v + m += b"\x00" + return m + +class PSBT: + """Class for serializing and deserializing PSBTs""" + + def __init__(self, *, g=None, i=None, o=None): + self.g = g if g is not None else PSBTMap() + self.i = i if i is not None else [] + self.o = o if o is not None else [] + self.in_count = len(i) if i is not None else None + self.out_count = len(o) if o is not None else None + self.version = None + + def deserialize(self, f): + assert f.read(5) == b"psbt\xff" + self.g = from_binary(PSBTMap, f) + + self.version = 0 + if PSBT_GLOBAL_VERSION in self.g.map: + assert PSBT_GLOBAL_INPUT_COUNT in self.g.map + assert PSBT_GLOBAL_OUTPUT_COUNT in self.g.map + self.version = struct.unpack(" 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-0376/deps/secp256k1lab/src/secp256k1lab/ecdh.py b/bip-0376/deps/secp256k1lab/src/secp256k1lab/ecdh.py new file mode 100644 index 0000000000..73f47fa1a7 --- /dev/null +++ b/bip-0376/deps/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-0376/deps/secp256k1lab/src/secp256k1lab/keys.py b/bip-0376/deps/secp256k1lab/src/secp256k1lab/keys.py new file mode 100644 index 0000000000..3e28897e99 --- /dev/null +++ b/bip-0376/deps/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-0376/deps/secp256k1lab/src/secp256k1lab/py.typed b/bip-0376/deps/secp256k1lab/src/secp256k1lab/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bip-0376/deps/secp256k1lab/src/secp256k1lab/secp256k1.py b/bip-0376/deps/secp256k1lab/src/secp256k1lab/secp256k1.py new file mode 100644 index 0000000000..6e262bf51e --- /dev/null +++ b/bip-0376/deps/secp256k1lab/src/secp256k1lab/secp256k1.py @@ -0,0 +1,454 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +# TODO Docstrings of methods still say "field element" +class APrimeFE: + """Objects of this class represent elements of a prime field. + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE: int + + def __init__(self, a=0, b=1): + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, type(self)): + num = a._num + den = a._den + else: + num = a % self.SIZE + den = 1 + if isinstance(b, type(self)): + den = (den * b._num) % self.SIZE + num = (num * b._den) % self.SIZE + else: + den = (den * b) % self.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num = num + self._den = den + + def __add__(self, a): + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den + self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num + self._den * a, self._den) + return NotImplemented + + def __radd__(self, a): + """Compute the sum of an integer and a field element.""" + return type(self)(a) + self + + @classmethod + # REVIEW This should be + # def sum(cls, *es: Iterable[Self]) -> Self: + # but Self needs the typing_extension package on Python <= 3.12. + def sum(cls, *es): + """Compute the sum of field elements. + + sum(a, b, c, ...) is identical to (0 + a + b + c + ...).""" + return sum(es, start=cls(0)) + + def __sub__(self, a): + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._den - self._den * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num - self._den * a, self._den) + return NotImplemented + + def __rsub__(self, a): + """Compute the difference of an integer and a field element.""" + return type(self)(a) - self + + def __mul__(self, a): + """Compute the product of two field elements (second may be int).""" + if isinstance(a, type(self)): + return type(self)(self._num * a._num, self._den * a._den) + if isinstance(a, int): + return type(self)(self._num * a, self._den) + return NotImplemented + + def __rmul__(self, a): + """Compute the product of an integer with a field element.""" + return type(self)(a) * self + + def __truediv__(self, a): + """Compute the ratio of two field elements (second may be int).""" + if isinstance(a, type(self)) or isinstance(a, int): + return type(self)(self, a) + return NotImplemented + + def __pow__(self, a): + """Raise a field element to an integer power.""" + return type(self)(pow(self._num, a, self.SIZE), pow(self._den, a, self.SIZE)) + + def __neg__(self): + """Negate a field element.""" + return type(self)(-self._num, self._den) + + def __int__(self): + """Convert a field element to an integer in range 0..SIZE-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, self.SIZE)) % self.SIZE + self._den = 1 + return self._num + + def sqrt(self): + """Compute the square root of a field element if it exists (None otherwise).""" + raise NotImplementedError + + def is_square(self): + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self): + """Determine whether this field element, represented as integer in 0..SIZE-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a): + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, type(self)): + return (self._num * a._den - self._den * a._num) % self.SIZE == 0 + return (self._num - self._den * a) % self.SIZE == 0 + + def to_bytes(self): + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @classmethod + def from_int_checked(cls, v): + """Convert an integer to a field element (no overflow allowed).""" + if v >= cls.SIZE: + raise ValueError + return cls(v) + + @classmethod + def from_int_wrapping(cls, v): + """Convert an integer to a field element (reduced modulo SIZE).""" + return cls(v % cls.SIZE) + + @classmethod + def from_bytes_checked(cls, b): + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + return cls.from_int_checked(v) + + @classmethod + def from_bytes_wrapping(cls, b): + """Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE).""" + v = int.from_bytes(b, 'big') + return cls.from_int_wrapping(v) + + def __str__(self): + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self): + """Get a string representation of this field element.""" + return f"{type(self).__qualname__}(0x{int(self):x})" + + +class FE(APrimeFE): + SIZE = 2**256 - 2**32 - 977 + + def sqrt(self): + # Due to the fact that our modulus p is of the form (p % 4) == 3, the Tonelli-Shanks + # algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + # raising the argument to the power (p + 1) / 4. + + # To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + # and thus only half of the non-zero field elements are squares. An element a is + # a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + # looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + # to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + # x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p. + v = int(self) + s = pow(v, (self.SIZE + 1) // 4, self.SIZE) + if s**2 % self.SIZE == v: + return type(self)(s) + return None + + +class Scalar(APrimeFE): + """TODO Docstring""" + SIZE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + GE objects are immutable. + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # TODO The following two class attributes should probably be just getters as + # classmethods to enforce immutability. Unfortunately Python makes it hard + # to create "classproperties". `G` could then also be just a classmethod. + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = Scalar.SIZE + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + @property + def infinity(self): + """Whether the group element is the point at infinity.""" + return self._infinity + + @property + def x(self): + """The x coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._x + + @property + def y(self): + """The y coordinate (a field element) of a non-infinite group element.""" + assert not self.infinity + return self._y + + def __init__(self, x=None, y=None): + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self._infinity = True + else: + # Initialize as point on the curve (and check that it is). + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self._infinity = False + self._x = fx + self._y = fy + + def __add__(self, a): + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def sum(*ps): + """Compute the sum of group elements. + + GE.sum(a, b, c, ...) is identical to (GE() + a + b + c + ...).""" + return sum(ps, start=GE()) + + @staticmethod + def batch_mul(*aps): + """Compute a (batch) scalar group element multiplication. + + GE.batch_mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(int(a), p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a): + """Multiply an integer with a group element.""" + if self == G: + return FAST_G.mul(Scalar(a)) + return GE.batch_mul((Scalar(a), self)) + + def __neg__(self): + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def __sub__(self, a): + """Subtract a group element from another.""" + return self + (-a) + + def __eq__(self, a): + """Check if two group elements are equal.""" + return (self - a).infinity + + def has_even_y(self): + """Determine whether a non-infinity group element has an even y coordinate.""" + assert not self.infinity + return self.y.is_even() + + def to_bytes_compressed(self): + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_compressed_with_infinity(self): + """Convert a group element to 33-byte compressed encoding, mapping infinity to zeros.""" + if self.infinity: + return 33 * b"\x00" + return self.to_bytes_compressed() + + def to_bytes_uncompressed(self): + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self): + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x): + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + raise ValueError + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes_compressed(b): + """Convert a compressed to a group element.""" + assert len(b) == 33 + if b[0] != 2 and b[0] != 3: + raise ValueError + x = FE.from_bytes_checked(b[1:]) + r = GE.lift_x(x) + if b[0] == 3: + r = -r + return r + + @staticmethod + def from_bytes_uncompressed(b): + """Convert an uncompressed to a group element.""" + assert len(b) == 65 + if b[0] != 4: + raise ValueError + x = FE.from_bytes_checked(b[1:33]) + y = FE.from_bytes_checked(b[33:]) + if y**2 != x**3 + 7: + raise ValueError + return GE(x, y) + + @staticmethod + def from_bytes(b): + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + return GE.from_bytes_compressed(b) + else: + return GE.from_bytes_uncompressed(b) + + @staticmethod + def from_bytes_xonly(b): + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes_checked(b) + r = GE.lift_x(x) + return r + + @staticmethod + def is_valid_x(x): + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self): + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self): + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + + def __hash__(self): + """Compute a non-cryptographic hash of the group element.""" + if self.infinity: + return 0 # 0 is not a valid x coordinate + return int(self.x) + + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p): + self.table = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a): + result = GE() + a = int(a) + for bit in range(a.bit_length()): + if a & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/bip-0376/deps/secp256k1lab/src/secp256k1lab/util.py b/bip-0376/deps/secp256k1lab/src/secp256k1lab/util.py new file mode 100644 index 0000000000..d8c744b795 --- /dev/null +++ b/bip-0376/deps/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-0376/psbt_bip376.py b/bip-0376/psbt_bip376.py index 086aeedb77..d0beda15c1 100644 --- a/bip-0376/psbt_bip376.py +++ b/bip-0376/psbt_bip376.py @@ -5,14 +5,14 @@ import struct from typing import Optional -from deps.bitcoin_test.messages import CTransaction, CTxOut, deser_compact_size, from_binary +from deps.bitcoin_test.messages import CTxOut, deser_compact_size, from_binary from deps.bitcoin_test.psbt import ( PSBT, PSBTMap, PSBT_GLOBAL_INPUT_COUNT, PSBT_GLOBAL_OUTPUT_COUNT, - PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_VERSION, + PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_IN_TAP_KEY_SIG, PSBT_IN_WITNESS_UTXO, ) @@ -46,35 +46,38 @@ def set_by_key(self, key_type: int, value_data: bytes, key_data: bytes = b"") -> class BIP376PSBT(PSBT): - """PSBT that deserializes maps as BIP376PSBTMap instances.""" + """PSBTv2 that deserializes maps as BIP376PSBTMap instances.""" def deserialize(self, f): assert f.read(5) == b"psbt\xff" self.g = from_binary(BIP376PSBTMap, f) - self.version = 0 - if PSBT_GLOBAL_VERSION in self.g.map: - assert PSBT_GLOBAL_INPUT_COUNT in self.g.map - assert PSBT_GLOBAL_OUTPUT_COUNT in self.g.map - self.version = struct.unpack(" bytes: witness_utxo = input_map.get(PSBT_IN_WITNESS_UTXO) @@ -101,3 +104,20 @@ def set_tap_key_sig(input_map: BIP376PSBTMap, signature: bytes) -> None: if len(signature) not in (64, 65): raise ValueError("PSBT_IN_TAP_KEY_SIG must be 64 or 65 bytes") input_map.set_by_key(PSBT_IN_TAP_KEY_SIG, signature) + + +def remove_sp_finalized_fields(input_map: BIP376PSBTMap) -> None: + if input_map.get(PSBT_IN_FINAL_SCRIPTWITNESS) is None: + return + + for key in ( + PSBT_IN_SP_TWEAK, + PSBT_IN_TAP_KEY_SIG, + PSBT_IN_WITNESS_UTXO, + ): + input_map.map.pop(key, None) + + derivation_prefix = bytes([PSBT_IN_SP_SPEND_BIP32_DERIVATION]) + for key in list(input_map.map): + if isinstance(key, bytes) and key.startswith(derivation_prefix): + input_map.map.pop(key, None) diff --git a/bip-0376/reference.py b/bip-0376/reference.py index b5bb793396..351396bef0 100755 --- a/bip-0376/reference.py +++ b/bip-0376/reference.py @@ -9,20 +9,27 @@ import sys from pathlib import Path -BIP375_DIR = Path(__file__).resolve().parents[1] / "bip-0375" -DEPS_DIR = BIP375_DIR / "deps" +PROJECT_DIR = Path(__file__).resolve().parent +DEPS_DIR = PROJECT_DIR / "deps" SECP256K1LAB_DIR = DEPS_DIR / "secp256k1lab/src" -for dependency_path in (BIP375_DIR, DEPS_DIR, SECP256K1LAB_DIR): +for dependency_path in (PROJECT_DIR, DEPS_DIR, SECP256K1LAB_DIR): sys.path.insert(0, str(dependency_path)) -from secp256k1lab.bip340 import schnorr_sign +from secp256k1lab.bip340 import schnorr_sign, schnorr_verify from secp256k1lab.secp256k1 import G, Scalar +from deps.bitcoin_test.messages import ser_string_vector +from deps.bitcoin_test.psbt import ( + PSBT_IN_FINAL_SCRIPTWITNESS, + PSBT_IN_TAP_KEY_SIG, +) from psbt_bip376 import ( BIP376PSBT, + PSBT_IN_SP_SPEND_BIP32_DERIVATION, PSBT_IN_SP_TWEAK, get_p2tr_witness_utxo_output_key, get_sp_tweak, + remove_sp_finalized_fields, set_tap_key_sig, ) @@ -34,6 +41,21 @@ def parse_hex(data: str, expected_len: int, field_name: str) -> bytes: return raw +def load_psbt(psbt_data: dict) -> BIP376PSBT: + if "hex" in psbt_data: + return BIP376PSBT.from_hex(psbt_data["hex"]) + if "base64" in psbt_data: + return BIP376PSBT.from_base64(psbt_data["base64"]) + raise ValueError("psbt must contain hex or base64") + + +def encode_psbt(psbt: BIP376PSBT) -> dict: + return { + "hex": psbt.to_hex(), + "base64": psbt.to_base64(), + } + + def derive_signing_key( spend_seckey: bytes, tweak: bytes, output_pubkey: bytes ) -> tuple[Scalar, Scalar, bool]: @@ -61,8 +83,31 @@ def derive_signing_key( return d_raw, d, negated -def sign_psbt(psbt_data: str, spend_seckey: bytes, message: bytes, aux_rand: bytes) -> str: - psbt = BIP376PSBT.from_base64(psbt_data) +def update_psbt(psbt_data: dict, supplementary: dict) -> dict: + psbt = load_psbt(psbt_data) + input_index = supplementary.get("input_index", 0) + input_map = psbt.i[input_index] + tweak = parse_hex(supplementary["tweak"], 32, "tweak") + + if "spend_pubkey" in supplementary: + spend_pubkey = parse_hex(supplementary["spend_pubkey"], 33, "spend_pubkey") + derivation = bytes.fromhex( + supplementary.get("spend_bip32_derivation", "00000000") + ) + if len(derivation) < 4 or len(derivation) % 4 != 0: + raise ValueError( + "spend_bip32_derivation must be fingerprint plus path elements" + ) + input_map.set_by_key( + PSBT_IN_SP_SPEND_BIP32_DERIVATION, derivation, spend_pubkey + ) + input_map.set_by_key(PSBT_IN_SP_TWEAK, tweak) + + return encode_psbt(psbt) + + +def sign_psbt(psbt_data: dict, spend_seckey: bytes, message: bytes, aux_rand: bytes) -> dict: + psbt = load_psbt(psbt_data) for input_map in psbt.i: if input_map.get(PSBT_IN_SP_TWEAK) is None: continue @@ -70,55 +115,79 @@ def sign_psbt(psbt_data: str, spend_seckey: bytes, message: bytes, aux_rand: byt output_pubkey = get_p2tr_witness_utxo_output_key(input_map) _, d, _ = derive_signing_key(spend_seckey, tweak, output_pubkey) set_tap_key_sig(input_map, schnorr_sign(message, d.to_bytes(), aux_rand)) - return psbt.to_base64() + return encode_psbt(psbt) + + +def finalize_psbt(psbt_data: dict, message: bytes) -> dict: + psbt = load_psbt(psbt_data) + for input_map in psbt.i: + if input_map.get(PSBT_IN_SP_TWEAK) is None: + continue + signature = input_map.get(PSBT_IN_TAP_KEY_SIG) + if signature is None: + raise ValueError("missing PSBT_IN_TAP_KEY_SIG") + + output_pubkey = get_p2tr_witness_utxo_output_key(input_map) + if not schnorr_verify(message, output_pubkey, signature[:64]): + raise ValueError("invalid PSBT_IN_TAP_KEY_SIG") + + if input_map.get(PSBT_IN_FINAL_SCRIPTWITNESS) is None: + input_map.set_by_key( + PSBT_IN_FINAL_SCRIPTWITNESS, ser_string_vector([signature]) + ) + remove_sp_finalized_fields(input_map) + return encode_psbt(psbt) + + +def run_case(case: dict) -> dict: + supplementary = case.get("supplementary", {}) + task = supplementary["task"] + psbt_data = case["psbt"] + + if task == "update": + return update_psbt(psbt_data, supplementary) + + if task in ("sign", "fail_sign"): + spend_seckey = parse_hex(supplementary["spend_seckey"], 32, "spend_seckey") + message = parse_hex(supplementary["message"], 32, "message") + aux_rand = parse_hex(supplementary["aux_rand"], 32, "aux_rand") + return sign_psbt(psbt_data, spend_seckey, message, aux_rand) + + if task in ("finalize", "fail"): + message = parse_hex(supplementary["message"], 32, "message") + return finalize_psbt(psbt_data, message) + + raise ValueError(f"unknown task: {task}") def run_test_vectors(path: Path) -> bool: vectors = json.loads(path.read_text(encoding="utf-8")) all_passed = True - valid_vectors = vectors.get("valid", []) - invalid_vectors = vectors.get("invalid", []) - - print(f"Running {len(valid_vectors)} valid vectors") - for index, vector in enumerate(valid_vectors): - description = vector["description"] - given = vector["given"] - expected = vector["expected"] - print(f"- valid[{index}] {description}") + cases = vectors.get("cases", []) + print(f"Description: {vectors.get('description', 'N/A')}") + print(f"Version: {vectors.get('version', 'N/A')}") + print(f"Running {len(cases)} cases") + for index, case in enumerate(cases): + description = case["description"] + task = case.get("supplementary", {})["task"] + print(f"- cases[{index}] {description}") try: - spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") - message = parse_hex(given["message"], 32, "message") - aux_rand = parse_hex(given["aux_rand"], 32, "aux_rand") - - if "psbt" in given: - signed_psbt = sign_psbt(given["psbt"], spend_seckey, message, aux_rand) - assert signed_psbt == expected["psbt"] - else: - tweak = parse_hex(given["tweak"], 32, "tweak") - output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") - _, d, _ = derive_signing_key(spend_seckey, tweak, output_pubkey) - signature = schnorr_sign(message, d.to_bytes(), aux_rand) - assert signature.hex() == expected["signature"] + result = run_case(case) + if task in ("fail", "fail_sign"): + all_passed = False + print(" FAILED: expected an exception") + continue + expected = case["expected"]["psbt"] + if result != expected: + all_passed = False + print(" FAILED: PSBT mismatch") except Exception as exc: + if task in ("fail", "fail_sign"): + continue all_passed = False print(f" FAILED: {exc}") - print(f"Running {len(invalid_vectors)} invalid vectors") - for index, vector in enumerate(invalid_vectors): - description = vector["description"] - given = vector["given"] - print(f"- invalid[{index}] {description}") - try: - spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey") - tweak = parse_hex(given["tweak"], 32, "tweak") - output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey") - derive_signing_key(spend_seckey, tweak, output_pubkey) - all_passed = False - print(" FAILED: expected an exception") - except Exception: - pass - print("All test vectors passed." if all_passed else "Some test vectors failed.") return all_passed diff --git a/bip-0376/test-vectors.json b/bip-0376/test-vectors.json index af899fab65..c7a1e75790 100644 --- a/bip-0376/test-vectors.json +++ b/bip-0376/test-vectors.json @@ -1,53 +1,144 @@ { - "valid": [ + "description": "BIP 376 test vectors", + "version": "0.1.0", + "notes": [ + "PSBT encodings are provided as hex and base64 for convenience.", + "The supplementary.task field selects the role operation: update, sign, fail_sign, finalize, or fail.", + "Supplementary fields provide deterministic signing material for vector validation only." + ], + "cases": [ + { + "description": "Updater adds PSBT_IN_SP_TWEAK", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da0280001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgAAQMIhAMAAAAAAAABBAFqAA==" + }, + "supplementary": { + "task": "update", + "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221" + }, + "expected": { + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee2210001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgBICCsew0EIPDUpWfZq8uKUuAs+uIWkP2NLVk0Nw3MWq7iIQABAwiEAwAAAAAAAAEEAWoA" + } + } + }, + { + "description": "Updater adds PSBT_IN_SP_TWEAK and PSBT_IN_SP_SPEND_BIP32_DERIVATION", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da0280001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgAAQMIhAMAAAAAAAABBAFqAA==" + }, + "supplementary": { + "task": "update", + "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", + "spend_pubkey": "02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef8", + "spend_bip32_derivation": "00000000" + }, + "expected": { + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee2210001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEAAQMIhAMAAAAAAAABBAFqAA==" + } + } + }, + { + "description": "Signer sets PSBT_IN_TAP_KEY_SIG with expected signature for input with PSBT_IN_SP_TWEAK set using PSBT_IN_SP_SPEND_BIP32_DERIVATION", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee2210001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEAAQMIhAMAAAAAAAABBAFqAA==" + }, + "supplementary": { + "task": "sign", + "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", + "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4", + "aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10" + }, + "expected": { + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221011340d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177780001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEBE0DQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd4AAEDCIQDAAAAAAAAAQQBagA=" + } + } + }, { - "description": "No negation required; tweaked key directly matches output key", - "given": { + "description": "Signer sets PSBT_IN_TAP_KEY_SIG with expected signature for input with PSBT_IN_SP_TWEAK without PSBT_IN_SP_SPEND_BIP32_DERIVATION field", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee2210001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgBICCsew0EIPDUpWfZq8uKUuAs+uIWkP2NLVk0Nw3MWq7iIQABAwiEAwAAAAAAAAEEAWoA" + }, + "supplementary": { + "task": "sign", "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEAAQMIhAMAAAAAAAABBAFqAA==", "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4", "aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10" }, "expected": { - "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEBE0DQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd4AAEDCIQDAAAAAAAAAQQBagA=" + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221011340d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177780001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgBICCsew0EIPDUpWfZq8uKUuAs+uIWkP2NLVk0Nw3MWq7iIQETQNDE9e43aMA6jY4SBLjlLGpN7R9FbQ8XB+eEGSiUXFpFvBwLxnHXlhLvHGelS9UNZTzj0zwf2WbOmpHgU/lBd3gAAQMIhAMAAAAAAAABBAFqAA==" + } } }, { - "description": "Negation required because (b_spend + tweak)G has odd Y", - "given": { + "description": "Signer sets PSBT_IN_TAP_KEY_SIG with expected signature with d.G odd, for input with PSBT_IN_SP_TWEAK set", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201212121212121212121212121212121212121212121212121212121212121212010f040000000001012be903000000000000225120db0edc417c73c567add118de8d138b2d0b64083f0a1bd8e876936415de7edc4601202058e2385eb96d1c906bbd807eafd1fddb80fb2f43026a16386a400e6832644cbc0001030885030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYBICBY4jheuW0ckGu9gH6v0f3bgPsvQwJqFjhqQA5oMmRMvAABAwiFAwAAAAAAAAEEAWoA" + }, + "supplementary": { + "task": "sign", "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgWOI4XrltHJBrvYB+r9H924D7L0MCahY4akAOaDJkTLwAAQMIhQMAAAAAAAABBAFqAA==", "message": "a78521e49048b6e0d368d3fba417fc20c7546272dafa78a8a173fcca6c81233b", "aux_rand": "6b31977a8ac73ede3f3653ea0d96bc3656242461e31d771985a0b17084d3cf91" }, "expected": { - "psbt": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgWOI4XrltHJBrvYB+r9H924D7L0MCahY4akAOaDJkTLwBE0A99nITr9iVqDO8BG6UVcd6fkAWVjitSJZppcSY1NcatWWpxUq9ouI+kx96D3ip8VG7oHuEALe5bST4V8i6ZcAiAAEDCIUDAAAAAAAAAQQBagA=" + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201212121212121212121212121212121212121212121212121212121212121212010f040000000001012be903000000000000225120db0edc417c73c567add118de8d138b2d0b64083f0a1bd8e876936415de7edc4601202058e2385eb96d1c906bbd807eafd1fddb80fb2f43026a16386a400e6832644cbc0113403df67213afd895a833bc046e9455c77a7e40165638ad489669a5c498d4d71ab565a9c54abda2e23e931f7a0f78a9f151bba07b8400b7b96d24f857c8ba65c0220001030885030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAQ8EAAAAAAEBK+kDAAAAAAAAIlEg2w7cQXxzxWet0RjejROLLQtkCD8KG9jodpNkFd5+3EYBICBY4jheuW0ckGu9gH6v0f3bgPsvQwJqFjhqQA5oMmRMvAETQD32chOv2JWoM7wEbpRVx3p+QBZWOK1IlmmlxJjU1xq1ZanFSr2i4j6TH3oPeKnxUbuge4QAt7ltJPhXyLplwCIAAQMIhQMAAAAAAAABBAFqAA==" + } } - } - ], - "invalid": [ + }, { - "description": "Tweaked key does not match output key", - "given": { + "description": "Signer fails if the x-coordinate of d.G does not equal the output key in the P2TR output script", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da029221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee2210001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCkiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEAAQMIhAMAAAAAAAABBAFqAA==" + }, + "supplementary": { + "task": "fail_sign", "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", - "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da029" + "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4", + "aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10" } }, { - "description": "Tweaked private key is zero", - "given": { - "spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a", - "tweak": "d9393572aac140a9cc352d0a41329f73ef151d38ce484de71578a23d0cc3a8e7", - "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" + "description": "Input finalizer fails to verify PSBT_IN_TAP_KEY_SIG for PSBT_IN_SP_TWEAK", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221011340d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177790001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEBE0DQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd5AAEDCIQDAAAAAAAAAQQBagA=" + }, + "supplementary": { + "task": "fail", + "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4" } }, { - "description": "Spend key out of range", - "given": { - "spend_seckey": "0000000000000000000000000000000000000000000000000000000000000000", - "tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221", - "output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028" + "description": "Input finalizer removes PSBT_IN_SP_TWEAK, PSBT_IN_SP_SPEND_BIP32_DERIVATION, PSBT_IN_TAP_KEY_SIG, and PSBT_IN_WITNESS_UTXO fields for a finalized input", + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f040000000001012be803000000000000225120528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028221f02812c369c23c4185755813b40f4edf0cedffbc2496f7802932513ead763ffaef80400000000012020ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221011340d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177780108420140d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177780001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEBK+gDAAAAAAAAIlEgUot1KW+mRqzs8/y3x2l/kvdkXqDkHm7opmVUc50toCgiHwKBLDacI8QYV1WBO0D07fDO3/vCSW94ApMlE+rXY/+u+AQAAAAAASAgrHsNBCDw1KVn2avLilLgLPriFpD9jS1ZNDcNzFqu4iEBE0DQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd4AQhCAUDQxPXuN2jAOo2OEgS45SxqTe0fRW0PFwfnhBkolFxaRbwcC8Zx15YS7xxnpUvVDWU849M8H9lmzpqR4FP5QXd4AAEDCIQDAAAAAAAAAQQBagA=" + }, + "supplementary": { + "task": "finalize", + "message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4" + }, + "expected": { + "psbt": { + "hex": "70736274ff01020402000000010401010105010101fb040200000000010e201111111111111111111111111111111111111111111111111111111111111111010f04000000000108420140d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f94177780001030884030000000000000104016a00", + "base64": "cHNidP8BAgQCAAAAAQQBAQEFAQEB+wQCAAAAAAEOIBERERERERERERERERERERERERERERERERERERERERERAQ8EAAAAAAEIQgFA0MT17jdowDqNjhIEuOUsak3tH0VtDxcH54QZKJRcWkW8HAvGcdeWEu8cZ6VL1Q1lPOPTPB/ZZs6akeBT+UF3eAABAwiEAwAAAAAAAAEEAWoA" + } } } ]