From ad4b24588485ea959155a6487488f980b5195ab0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 26 Aug 2024 08:47:38 -0500 Subject: [PATCH 01/15] Initial BIP-352 support --- src/embit/bip352.py | 25 +++++++++++++++++++++ tests/tests/test_bip352.py | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/embit/bip352.py create mode 100644 tests/tests/test_bip352.py diff --git a/src/embit/bip352.py b/src/embit/bip352.py new file mode 100644 index 00000000..0ed009d1 --- /dev/null +++ b/src/embit/bip352.py @@ -0,0 +1,25 @@ +""" +BIP-352: Silent Payments +see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki + +TODO: +* Add support for SP labels. +* Implement deriving a destination addr for a given output and recipient SP address. +* Implement check to determine if a given output is an SP output for a given SP address. +* Implement signing SP spends (once psbt format is settled). +""" + +from embit import bech32, ec + + +def generate_silent_payment_address(B_scan: ec.PublicKey, B_m: ec.PublicKey, network: str = "main", version: int = 0) -> str: + """ + Adapted from https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py + + Generates the recipient's reusable silent payment address for a given: + * scanning pubkey `B_scan` + * spending pubkey `B_m` + """ + data = bech32.convertbits(B_scan.sec() + B_m.sec(), 8, 5) + hrp = "sp" if network == "main" else "tsp" + return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) diff --git a/tests/tests/test_bip352.py b/tests/tests/test_bip352.py new file mode 100644 index 00000000..e0d73ce5 --- /dev/null +++ b/tests/tests/test_bip352.py @@ -0,0 +1,45 @@ +""" +BIP-352 test vectors: +https://github.com/bitcoin/bips/blob/master/bip-0352/send_and_receive_test_vectors.json +""" + +from binascii import unhexlify +from unittest import TestCase +from embit import bip352 +from embit.ec import PrivateKey +from embit.networks import NETWORKS + + +BASIC_TEST_VECTORS = [ + { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "sp_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + }, + { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002", + "sp_address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + } +] + + +class BIP352Test(TestCase): + def test_generate_silent_payment_address(self): + """ Should generate the expected silent payment address """ + for test_vector in BASIC_TEST_VECTORS: + spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) + sp_address = bip352.generate_silent_payment_address(scan_priv_key.get_public_key(), spend_priv_key.get_public_key()) + assert sp_address == test_vector["sp_address"] + + + def test_generate_silent_payment_address_for_network(self): + """ Test network silent payment addrs should start with "tsp" """ + test_networks = [k for k in NETWORKS.keys() if k != "main"] + scan_pubkey = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"])).get_public_key() + spend_pubkey = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])).get_public_key() + + for network in test_networks: + payment_addr = bip352.generate_silent_payment_address(scan_pubkey, spend_pubkey, network=network) + assert payment_addr.startswith("tsp") From 575e80b6f2b8cac989bfd7733f69f3cd46cc69af Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 31 Dec 2024 13:38:35 -0600 Subject: [PATCH 02/15] Add labeled SP addrs --- src/embit/bip352.py | 36 +++++++++++++++++++++++++-------- tests/tests/test_bip352.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/embit/bip352.py b/src/embit/bip352.py index 0ed009d1..0a33bef2 100644 --- a/src/embit/bip352.py +++ b/src/embit/bip352.py @@ -3,23 +3,43 @@ see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki TODO: -* Add support for SP labels. * Implement deriving a destination addr for a given output and recipient SP address. * Implement check to determine if a given output is an SP output for a given SP address. * Implement signing SP spends (once psbt format is settled). """ - from embit import bech32, ec +from embit.util import secp256k1 +from embit.hashes import tagged_hash + -def generate_silent_payment_address(B_scan: ec.PublicKey, B_m: ec.PublicKey, network: str = "main", version: int = 0) -> str: +def generate_silent_payment_address(B_scan: ec.PublicKey, B_spend: ec.PublicKey, network: str = "main", version: int = 0) -> str: """ Adapted from https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py - - Generates the recipient's reusable silent payment address for a given: - * scanning pubkey `B_scan` - * spending pubkey `B_m` """ - data = bech32.convertbits(B_scan.sec() + B_m.sec(), 8, 5) + data = bech32.convertbits(B_scan.sec() + B_spend.sec(), 8, 5) hrp = "sp" if network == "main" else "tsp" return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) + + + +def generate_labeled_silent_payment_address(b_scan: ec.PrivateKey, B_spend: ec.PublicKey, label, network: str = "main", version: int = 0) -> str: + """ + The spending key is tweaked with the label to generate a labeled silent payment address. + see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding + + `label` must be an int, str, or bytes. + """ + if isinstance(label, int): + label_bytes = label.to_bytes(4, "big") + elif isinstance(label, str): + label_bytes = label.encode() + elif isinstance(label, bytes): + label_bytes = label + else: + raise Exception("Label must be an int, str, or bytes.") + + tweak = tagged_hash("BIP0352/Label", b_scan.secret + label_bytes) + label_pubkey = ec.PublicKey(secp256k1.ec_pubkey_add(secp256k1.ec_pubkey_parse(B_spend.sec()), tweak)) + + return generate_silent_payment_address(b_scan.get_public_key(), label_pubkey, network=network, version=version) diff --git a/tests/tests/test_bip352.py b/tests/tests/test_bip352.py index e0d73ce5..f613a199 100644 --- a/tests/tests/test_bip352.py +++ b/tests/tests/test_bip352.py @@ -5,6 +5,8 @@ from binascii import unhexlify from unittest import TestCase + +import pytest from embit import bip352 from embit.ec import PrivateKey from embit.networks import NETWORKS @@ -24,6 +26,22 @@ ] +LABEL_TEST_VECTORS = { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "labels": [ + 2, + 3, + 1001337 + ], + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ] +} + + class BIP352Test(TestCase): def test_generate_silent_payment_address(self): """ Should generate the expected silent payment address """ @@ -43,3 +61,26 @@ def test_generate_silent_payment_address_for_network(self): for network in test_networks: payment_addr = bip352.generate_silent_payment_address(scan_pubkey, spend_pubkey, network=network) assert payment_addr.startswith("tsp") + + + def test_generate_labeled_silent_payment_address(self): + """ Should generate the expected labeled silent payment addresses """ + spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) + for label, address in zip(LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"]): + sp_address = bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label) + assert sp_address == address + + # Label may also be a string, but the bip does not provide any test vectors + bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label="tenant 6102") + + # Label may also be passed in as bytes + bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label="I am bytes".encode()) + + with pytest.raises(Exception): + # Label is required + bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key()) + + with pytest.raises(Exception): + # Label must be an int, str, or bytes + bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label=1.0) From d91a969dca91fffd4135a935446fcebb6b22b7af Mon Sep 17 00:00:00 2001 From: odudex Date: Fri, 29 May 2026 09:04:13 -0300 Subject: [PATCH 03/15] bip352: restrict labels to 32-bit ints, fix formatting Reject non-int and reserved/out-of-range labels (m=0 is change), drop the non-standard str/bytes label support, correct swapped scan/spend keys in the network test, and apply black formatting. --- src/embit/bip352.py | 39 +++++++++++------- tests/tests/test_bip352.py | 81 ++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/embit/bip352.py b/src/embit/bip352.py index 0a33bef2..95b1007c 100644 --- a/src/embit/bip352.py +++ b/src/embit/bip352.py @@ -7,13 +7,15 @@ * Implement check to determine if a given output is an SP output for a given SP address. * Implement signing SP spends (once psbt format is settled). """ + from embit import bech32, ec from embit.util import secp256k1 from embit.hashes import tagged_hash - -def generate_silent_payment_address(B_scan: ec.PublicKey, B_spend: ec.PublicKey, network: str = "main", version: int = 0) -> str: +def generate_silent_payment_address( + B_scan: ec.PublicKey, B_spend: ec.PublicKey, network: str = "main", version: int = 0 +) -> str: """ Adapted from https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py """ @@ -22,24 +24,31 @@ def generate_silent_payment_address(B_scan: ec.PublicKey, B_spend: ec.PublicKey, return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) - -def generate_labeled_silent_payment_address(b_scan: ec.PrivateKey, B_spend: ec.PublicKey, label, network: str = "main", version: int = 0) -> str: +def generate_labeled_silent_payment_address( + b_scan: ec.PrivateKey, + B_spend: ec.PublicKey, + label: int, + network: str = "main", + version: int = 0, +) -> str: """ The spending key is tweaked with the label to generate a labeled silent payment address. see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding - `label` must be an int, str, or bytes. + `label` must be a 32-bit unsigned integer `m`. `m = 0` is reserved for change + outputs and cannot be used here. """ - if isinstance(label, int): - label_bytes = label.to_bytes(4, "big") - elif isinstance(label, str): - label_bytes = label.encode() - elif isinstance(label, bytes): - label_bytes = label - else: - raise Exception("Label must be an int, str, or bytes.") + if not isinstance(label, int) or isinstance(label, bool): + raise TypeError("Label must be an int.") + if not 1 <= label <= 0xFFFFFFFF: + raise ValueError("Label must be a 32-bit unsigned integer in [1, 2**32 - 1].") + label_bytes = label.to_bytes(4, "big") tweak = tagged_hash("BIP0352/Label", b_scan.secret + label_bytes) - label_pubkey = ec.PublicKey(secp256k1.ec_pubkey_add(secp256k1.ec_pubkey_parse(B_spend.sec()), tweak)) + label_pubkey = ec.PublicKey( + secp256k1.ec_pubkey_add(secp256k1.ec_pubkey_parse(B_spend.sec()), tweak) + ) - return generate_silent_payment_address(b_scan.get_public_key(), label_pubkey, network=network, version=version) + return generate_silent_payment_address( + b_scan.get_public_key(), label_pubkey, network=network, version=version + ) diff --git a/tests/tests/test_bip352.py b/tests/tests/test_bip352.py index f613a199..2883e141 100644 --- a/tests/tests/test_bip352.py +++ b/tests/tests/test_bip352.py @@ -11,76 +11,91 @@ from embit.ec import PrivateKey from embit.networks import NETWORKS - BASIC_TEST_VECTORS = [ { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", - "sp_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + "sp_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", }, { "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002", - "sp_address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" - } + "sp_address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s", + }, ] LABEL_TEST_VECTORS = { "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", - "labels": [ - 2, - 3, - 1001337 - ], + "labels": [2, 3, 1001337], "addresses": [ "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" - ] + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", + ], } class BIP352Test(TestCase): def test_generate_silent_payment_address(self): - """ Should generate the expected silent payment address """ + """Should generate the expected silent payment address""" for test_vector in BASIC_TEST_VECTORS: spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) - sp_address = bip352.generate_silent_payment_address(scan_priv_key.get_public_key(), spend_priv_key.get_public_key()) + sp_address = bip352.generate_silent_payment_address( + scan_priv_key.get_public_key(), spend_priv_key.get_public_key() + ) assert sp_address == test_vector["sp_address"] - def test_generate_silent_payment_address_for_network(self): - """ Test network silent payment addrs should start with "tsp" """ + """Test network silent payment addrs should start with "tsp" """ test_networks = [k for k in NETWORKS.keys() if k != "main"] - scan_pubkey = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"])).get_public_key() - spend_pubkey = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])).get_public_key() + scan_pubkey = PrivateKey( + unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"]) + ).get_public_key() + spend_pubkey = PrivateKey( + unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"]) + ).get_public_key() for network in test_networks: - payment_addr = bip352.generate_silent_payment_address(scan_pubkey, spend_pubkey, network=network) + payment_addr = bip352.generate_silent_payment_address( + scan_pubkey, spend_pubkey, network=network + ) assert payment_addr.startswith("tsp") - def test_generate_labeled_silent_payment_address(self): - """ Should generate the expected labeled silent payment addresses """ + """Should generate the expected labeled silent payment addresses""" spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) - for label, address in zip(LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"]): - sp_address = bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label) + for label, address in zip( + LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"] + ): + sp_address = bip352.generate_labeled_silent_payment_address( + scan_priv_key, spend_priv_key.get_public_key(), label + ) assert sp_address == address - # Label may also be a string, but the bip does not provide any test vectors - bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label="tenant 6102") - - # Label may also be passed in as bytes - bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label="I am bytes".encode()) + def test_generate_labeled_silent_payment_address_invalid_label(self): + """Labels must be 32-bit unsigned ints in [1, 2**32 - 1]""" + spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) + spend_pubkey = spend_priv_key.get_public_key() - with pytest.raises(Exception): + with pytest.raises(TypeError): # Label is required - bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key()) - - with pytest.raises(Exception): - # Label must be an int, str, or bytes - bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_priv_key.get_public_key(), label=1.0) + bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_pubkey) + + for bad_label in ["tenant 6102", b"I am bytes", 1.0, True]: + with pytest.raises(TypeError): + # Label must be an int (and not a bool) + bip352.generate_labeled_silent_payment_address( + scan_priv_key, spend_pubkey, label=bad_label + ) + + for bad_label in [0, -1, 0x100000000]: + with pytest.raises(ValueError): + # m = 0 is reserved for change; values must fit in 32 bits + bip352.generate_labeled_silent_payment_address( + scan_priv_key, spend_pubkey, label=bad_label + ) From 78ce0cb1fd32fee57d5996e5d493ba8e2f779bd6 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Fri, 11 Jul 2025 18:45:22 +0530 Subject: [PATCH 04/15] chore: make bech32 flexible for large payloads(sp) --- src/embit/bech32.py | 38 ++++++++++++++------- tests/tests/test_bech32.py | 67 +++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 24ee7fa7..23e2e594 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -33,6 +33,12 @@ class Encoding: BECH32M = 2 +class Bech32DecodeError(Exception): + """Exception raised for errors during Bech32 decoding.""" + + pass + + def bech32_polymod(values): """Internal function that computes the Bech32 checksum.""" generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] @@ -77,21 +83,28 @@ def bech32_encode(encoding, hrp, data): def bech32_decode(bech): """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( - bech.lower() != bech and bech.upper() != bech - ): - return (None, None, None) + if any(ord(x) < 33 or ord(x) > 126 for x in bech): + raise Bech32DecodeError("Invalid character in input") + if bech.lower() != bech and bech.upper() != bech: + raise Bech32DecodeError("Mixed case strings not allowed") bech = bech.lower() pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos + 1 :]): - return (None, None, None) + if pos < 1: + raise Bech32DecodeError("Separator '1' not found or misplaced") + if pos > 83: + raise Bech32DecodeError("HRP too long (max 83 characters)") + if pos + 7 > len(bech): + raise Bech32DecodeError("Data part too short") + if len(bech) > 118: + raise Bech32DecodeError("String too long for SP address") hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos + 1 :]] + data_part = bech[pos + 1 :] + if not all(x in CHARSET for x in data_part): + raise Bech32DecodeError("Data part contains invalid characters") + data = [CHARSET.find(x) for x in data_part] encoding = bech32_verify_checksum(hrp, data) if encoding is None: - return (None, None, None) + raise Bech32DecodeError("Checksum verification failed") return (encoding, hrp, data[:-6]) @@ -120,7 +133,10 @@ def convertbits(data, frombits, tobits, pad=True): def decode(hrp, addr): """Decode a segwit address.""" - encoding, hrpgot, data = bech32_decode(addr) + try: + encoding, hrpgot, data = bech32_decode(addr) + except Bech32DecodeError: + return (None, None) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) diff --git a/tests/tests/test_bech32.py b/tests/tests/test_bech32.py index 7d71fb17..295e3e90 100644 --- a/tests/tests/test_bech32.py +++ b/tests/tests/test_bech32.py @@ -99,9 +99,11 @@ def segwit_scriptpubkey(witver, witprog): ("bc", 16, 41), ] +SILENT_PAYMENTS_ADDRESS = "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + class Bech32Test(TestCase): - """Unit test class for segwit addressess.""" + """Unit test class for segwit addresses.""" def test_valid_checksum(self): """Test checksum creation and validation.""" @@ -110,14 +112,14 @@ def test_valid_checksum(self): self.assertIsNotNone(hrp) pos = test.rfind("1") test = test[: pos + 1] + chr(ord(test[pos + 1]) ^ 1) + test[pos + 2 :] - enc, hrp, _ = segwit_addr.bech32_decode(test) - self.assertIsNone(hrp) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.bech32_decode(test) def test_invalid_checksum(self): """Test validation of invalid checksums.""" for test in INVALID_CHECKSUM: - enc, hrp, _ = segwit_addr.bech32_decode(test) - self.assertIsNone(hrp) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.bech32_decode(test) def test_valid_address(self): """Test whether valid addresses decode to the correct output.""" @@ -143,3 +145,58 @@ def test_invalid_address_enc(self): for hrp, version, length in INVALID_ADDRESS_ENC: code = segwit_addr.encode(hrp, version, [0] * length) self.assertIsNone(code) + + def test_silent_payments_address(self): + """Test decoding of Silent Payments address (Bech32m format).""" + encoding, hrp, data = segwit_addr.bech32_decode(SILENT_PAYMENTS_ADDRESS) + self.assertIsNotNone(hrp) + self.assertEqual(hrp, "sp") + self.assertEqual(encoding, segwit_addr.Encoding.BECH32M) + + def test_bech32_decode_exceptions(self): + """Test exception cases for bech32_decode.""" + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qw!@#$%") + self.assertIn("Data part contains invalid characters", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("BC1qwgHUDx") + self.assertIn("Mixed case", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("nonseparatoraddress") + self.assertIn("Separator '1' not found", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1short") + self.assertIn("Data part too short", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qinvalidchar") + self.assertIn("Data part contains invalid characters", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5") + self.assertIn("Checksum verification failed", str(context.exception)) + + +class Bech32RoundTripTest(TestCase): + """Test round-trip encoding/decoding for various payload sizes.""" + + def test_round_trip(self): + """Test round-trip encoding/decoding for various payload sizes.""" + test_cases = [ + ("test", []), # empty payload + ("ln", [0] * 100), # medium + ("hrp-with-dashes", list(range(31))), # full charset + ] + + for hrp, payload in test_cases: + for encoding in [segwit_addr.Encoding.BECH32, segwit_addr.Encoding.BECH32M]: + encoded = segwit_addr.bech32_encode(encoding, hrp, payload) + decoded_enc, decoded_hrp, decoded_data = segwit_addr.bech32_decode( + encoded + ) + + self.assertEqual(decoded_enc, encoding) + self.assertEqual(decoded_hrp, hrp.lower()) From 1fa298f139c396f8731501ea13d9b437985433e8 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Fri, 11 Jul 2025 18:52:29 +0530 Subject: [PATCH 05/15] chore: replace None w error msg --- src/embit/bech32.py | 40 ++++++++++++++++++++++++-------------- tests/tests/test_bech32.py | 12 ++++++------ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 23e2e594..2f7e15bd 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -117,7 +117,7 @@ def convertbits(data, frombits, tobits, pad=True): max_acc = (1 << (frombits + tobits - 1)) - 1 for value in data: if value < 0 or (value >> frombits): - return None + raise Bech32DecodeError("Invalid input value for bit conversion") acc = ((acc << frombits) | value) & max_acc bits += frombits while bits >= tobits: @@ -127,36 +127,46 @@ def convertbits(data, frombits, tobits, pad=True): if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None + raise Bech32DecodeError("Invalid padding in bit conversion") return ret def decode(hrp, addr): """Decode a segwit address.""" - try: - encoding, hrpgot, data = bech32_decode(addr) - except Bech32DecodeError: - return (None, None) + encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: - return (None, None) + raise Bech32DecodeError(f"HRP mismatch: expected {hrp}, got {hrpgot}") decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) + if len(decoded) < 2 or len(decoded) > 40: + raise Bech32DecodeError("Invalid witness program length") if data[0] > 16: - return (None, None) + raise Bech32DecodeError("Invalid witness version") if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) + raise Bech32DecodeError("Invalid witness program length for version 0") if (data[0] == 0 and encoding != Encoding.BECH32) or ( data[0] != 0 and encoding != Encoding.BECH32M ): - return (None, None) + raise Bech32DecodeError("Invalid encoding for witness version") return (data[0], decoded) def encode(hrp, witver, witprog): """Encode a segwit address.""" + if witver < 0 or witver > 16: + raise Bech32DecodeError("Invalid witness version") + if len(witprog) < 2 or len(witprog) > 40: + raise Bech32DecodeError("Invalid witness program length") + if witver == 0 and len(witprog) != 20 and len(witprog) != 32: + raise Bech32DecodeError("Invalid witness program length for version 0") + encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) - if decode(hrp, ret) == (None, None): - return None + converted = convertbits(witprog, 8, 5) + ret = bech32_encode(encoding, hrp, [witver] + converted) + + # Verify the encoding worked correctly + try: + decode(hrp, ret) + except Bech32DecodeError: + raise Bech32DecodeError("Failed to encode valid segwit address") + return ret diff --git a/tests/tests/test_bech32.py b/tests/tests/test_bech32.py index 295e3e90..f9aecea7 100644 --- a/tests/tests/test_bech32.py +++ b/tests/tests/test_bech32.py @@ -135,16 +135,16 @@ def test_valid_address(self): def test_invalid_address(self): """Test whether invalid addresses fail to decode.""" for test in INVALID_ADDRESS: - witver, _ = segwit_addr.decode("bc", test) - self.assertIsNone(witver) - witver, _ = segwit_addr.decode("tb", test) - self.assertIsNone(witver) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.decode("bc", test) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.decode("tb", test) def test_invalid_address_enc(self): """Test whether address encoding fails on invalid input.""" for hrp, version, length in INVALID_ADDRESS_ENC: - code = segwit_addr.encode(hrp, version, [0] * length) - self.assertIsNone(code) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.encode(hrp, version, [0] * length) def test_silent_payments_address(self): """Test decoding of Silent Payments address (Bech32m format).""" From c1f360881f3df834b07cac095397801191f4f7fd Mon Sep 17 00:00:00 2001 From: notTanveer Date: Tue, 15 Jul 2025 11:06:54 +0530 Subject: [PATCH 06/15] sp checks --- src/embit/bech32.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 2f7e15bd..178aa33f 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -34,8 +34,6 @@ class Encoding: class Bech32DecodeError(Exception): - """Exception raised for errors during Bech32 decoding.""" - pass @@ -137,14 +135,20 @@ def decode(hrp, addr): if hrpgot != hrp: raise Bech32DecodeError(f"HRP mismatch: expected {hrp}, got {hrpgot}") decoded = convertbits(data[1:], 5, 8, False) - if len(decoded) < 2 or len(decoded) > 40: - raise Bech32DecodeError("Invalid witness program length") + if len(decoded) < 2 or len(decoded) > 66: + raise Bech32DecodeError(f"Invalid witness program length") if data[0] > 16: raise Bech32DecodeError("Invalid witness version") - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + if ( + hrp not in ["sp", "tsp"] + and data[0] == 0 + and len(decoded) != 20 + and len(decoded) != 32 + ): raise Bech32DecodeError("Invalid witness program length for version 0") - if (data[0] == 0 and encoding != Encoding.BECH32) or ( - data[0] != 0 and encoding != Encoding.BECH32M + if hrp not in ["sp", "tsp"] and ( + (data[0] == 0 and encoding != Encoding.BECH32) + or (data[0] != 0 and encoding != Encoding.BECH32M) ): raise Bech32DecodeError("Invalid encoding for witness version") return (data[0], decoded) @@ -160,10 +164,8 @@ def encode(hrp, witver, witprog): raise Bech32DecodeError("Invalid witness program length for version 0") encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - converted = convertbits(witprog, 8, 5) - ret = bech32_encode(encoding, hrp, [witver] + converted) + ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) - # Verify the encoding worked correctly try: decode(hrp, ret) except Bech32DecodeError: From 8356d27db88864e8a2cb579a1b4105fe951c4b94 Mon Sep 17 00:00:00 2001 From: odudex Date: Fri, 29 May 2026 09:38:00 -0300 Subject: [PATCH 07/15] bech32: make Bech32DecodeError an EmbitError and tighten validation - Subclass EmbitError so existing except EmbitError callers still catch malformed-address errors (e.g. script.address_to_scriptpubkey). - Restore the BIP-141 2-40 byte witness program limit in decode() and drop the sp/tsp special-casing; SP addresses are not witness programs. - Replace the 118 length cap with BIP-352's recommended 1023 so valid higher-version silent payment addresses decode; cite BIP-173/BIP-352. - Drop the no-placeholder f-string and the encode() try/except that masked the original Bech32DecodeError. --- src/embit/bech32.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 178aa33f..b6766c52 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -19,7 +19,9 @@ # THE SOFTWARE. """Reference implementation for Bech32 and segwit addresses.""" + from .misc import const +from .base import EmbitError CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" BECH32_CONST = const(1) @@ -33,7 +35,7 @@ class Encoding: BECH32M = 2 -class Bech32DecodeError(Exception): +class Bech32DecodeError(EmbitError): pass @@ -89,12 +91,16 @@ def bech32_decode(bech): pos = bech.rfind("1") if pos < 1: raise Bech32DecodeError("Separator '1' not found or misplaced") + # BIP-173: the HRP must contain 1 to 83 characters. if pos > 83: raise Bech32DecodeError("HRP too long (max 83 characters)") if pos + 7 > len(bech): raise Bech32DecodeError("Data part too short") - if len(bech) > 118: - raise Bech32DecodeError("String too long for SP address") + # BIP-173 caps Bech32 strings at 90 chars, but BIP-352 silent payment + # addresses are longer (>=117) and recommend a 1023-char limit to leave + # room for future versions. + if len(bech) > 1023: + raise Bech32DecodeError("String too long (max 1023 characters)") hrp = bech[:pos] data_part = bech[pos + 1 :] if not all(x in CHARSET for x in data_part): @@ -130,25 +136,24 @@ def convertbits(data, frombits, tobits, pad=True): def decode(hrp, addr): - """Decode a segwit address.""" + """Decode a segwit address. + + Silent payment (sp/tsp) addresses are not witness programs and must not be + decoded here; use bech32_decode + convertbits for those. + """ encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: - raise Bech32DecodeError(f"HRP mismatch: expected {hrp}, got {hrpgot}") + raise Bech32DecodeError("HRP mismatch: expected {}, got {}".format(hrp, hrpgot)) decoded = convertbits(data[1:], 5, 8, False) - if len(decoded) < 2 or len(decoded) > 66: - raise Bech32DecodeError(f"Invalid witness program length") + # BIP-141: a witness program is 2 to 40 bytes. + if len(decoded) < 2 or len(decoded) > 40: + raise Bech32DecodeError("Invalid witness program length") if data[0] > 16: raise Bech32DecodeError("Invalid witness version") - if ( - hrp not in ["sp", "tsp"] - and data[0] == 0 - and len(decoded) != 20 - and len(decoded) != 32 - ): + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: raise Bech32DecodeError("Invalid witness program length for version 0") - if hrp not in ["sp", "tsp"] and ( - (data[0] == 0 and encoding != Encoding.BECH32) - or (data[0] != 0 and encoding != Encoding.BECH32M) + if (data[0] == 0 and encoding != Encoding.BECH32) or ( + data[0] != 0 and encoding != Encoding.BECH32M ): raise Bech32DecodeError("Invalid encoding for witness version") return (data[0], decoded) @@ -166,9 +171,8 @@ def encode(hrp, witver, witprog): encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) - try: - decode(hrp, ret) - except Bech32DecodeError: - raise Bech32DecodeError("Failed to encode valid segwit address") + # Sanity check: the result must round-trip. Any failure propagates with its + # original, descriptive Bech32DecodeError. + decode(hrp, ret) return ret From 77cf14add89a851549eed46136343e8a090096ff Mon Sep 17 00:00:00 2001 From: notTanveer Date: Thu, 21 May 2026 13:52:04 +0530 Subject: [PATCH 08/15] feat: silent payment send support --- src/embit/bip352.py | 54 ------- src/embit/silent_payments/__init__.py | 0 src/embit/silent_payments/bip352.py | 204 ++++++++++++++++++++++++++ src/embit/transaction.py | 11 ++ src/embit/util/key.py | 2 + tests/tests/test_bip352.py | 32 ++-- 6 files changed, 235 insertions(+), 68 deletions(-) delete mode 100644 src/embit/bip352.py create mode 100644 src/embit/silent_payments/__init__.py create mode 100644 src/embit/silent_payments/bip352.py diff --git a/src/embit/bip352.py b/src/embit/bip352.py deleted file mode 100644 index 95b1007c..00000000 --- a/src/embit/bip352.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -BIP-352: Silent Payments -see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki - -TODO: -* Implement deriving a destination addr for a given output and recipient SP address. -* Implement check to determine if a given output is an SP output for a given SP address. -* Implement signing SP spends (once psbt format is settled). -""" - -from embit import bech32, ec -from embit.util import secp256k1 -from embit.hashes import tagged_hash - - -def generate_silent_payment_address( - B_scan: ec.PublicKey, B_spend: ec.PublicKey, network: str = "main", version: int = 0 -) -> str: - """ - Adapted from https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py - """ - data = bech32.convertbits(B_scan.sec() + B_spend.sec(), 8, 5) - hrp = "sp" if network == "main" else "tsp" - return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) - - -def generate_labeled_silent_payment_address( - b_scan: ec.PrivateKey, - B_spend: ec.PublicKey, - label: int, - network: str = "main", - version: int = 0, -) -> str: - """ - The spending key is tweaked with the label to generate a labeled silent payment address. - see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding - - `label` must be a 32-bit unsigned integer `m`. `m = 0` is reserved for change - outputs and cannot be used here. - """ - if not isinstance(label, int) or isinstance(label, bool): - raise TypeError("Label must be an int.") - if not 1 <= label <= 0xFFFFFFFF: - raise ValueError("Label must be a 32-bit unsigned integer in [1, 2**32 - 1].") - - label_bytes = label.to_bytes(4, "big") - tweak = tagged_hash("BIP0352/Label", b_scan.secret + label_bytes) - label_pubkey = ec.PublicKey( - secp256k1.ec_pubkey_add(secp256k1.ec_pubkey_parse(B_spend.sec()), tweak) - ) - - return generate_silent_payment_address( - b_scan.get_public_key(), label_pubkey, network=network, version=version - ) diff --git a/src/embit/silent_payments/__init__.py b/src/embit/silent_payments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/embit/silent_payments/bip352.py b/src/embit/silent_payments/bip352.py new file mode 100644 index 00000000..d8548ca8 --- /dev/null +++ b/src/embit/silent_payments/bip352.py @@ -0,0 +1,204 @@ +""" +BIP-352: Silent Payments +see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki +""" + +from collections import Counter, defaultdict +from typing import Tuple, List, Dict + +from .. import bech32, ec +from ..util import secp256k1 +from ..hashes import tagged_hash +from ..util.key import SECP256K1_ORDER +from ..transaction import COutPoint +from ..util.secp256k1 import ( + ec_pubkey_create, + ec_pubkey_serialize, + ec_pubkey_parse, + ec_pubkey_tweak_mul, + ec_pubkey_tweak_add, + ec_seckey_verify, + ec_privkey_negate, +) +from binascii import hexlify + + +def generate_silent_payment_address( + scan_privkey: ec.PrivateKey, + spend_pubkey: ec.PublicKey, + label=None, + network: str = "main", + version: int = 0, +) -> str: + """ + Adapted from https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py + + Generates the recipient's reusable silent payment address for a given: + * scan private key + * spend public key + * optional label for labeled addresses. It must be a 32-bit unsigned + integer `m`; `m = 0` is reserved for change and cannot be used here. + """ + scan_pubkey = scan_privkey.get_public_key() + if label is not None: + # Labels are 32-bit unsigned ints; m = 0 is reserved for change. See + # https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding + if not isinstance(label, int) or isinstance(label, bool): + raise TypeError("Label must be an int.") + if not 1 <= label <= 0xFFFFFFFF: + raise ValueError( + "Label must be a 32-bit unsigned integer in [1, 2**32 - 1]." + ) + label_bytes = label.to_bytes(4, "big") + tweak = tagged_hash("BIP0352/Label", scan_privkey.secret + label_bytes) + spend_pubkey = ec.PublicKey( + secp256k1.ec_pubkey_add( + secp256k1.ec_pubkey_parse(spend_pubkey.sec()), tweak + ) + ) + + data = bech32.convertbits(scan_pubkey.sec() + spend_pubkey.sec(), 8, 5) + hrp = "sp" if network == "main" else "tsp" + return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) + + +# TODO: use the bech32 decode function once the flexible bech32 PR is in +def decode_silent_payment_address(address: str) -> Tuple[ec.PublicKey, ec.PublicKey]: + """ + Decode a silent payment address and return the scan and spend public keys. + Silent payment addresses can be longer than 90 characters, so we need custom decoding. + """ + if address.startswith("sp1"): + hrp = "sp" + elif address.startswith("tsp1"): + hrp = "tsp" + else: + raise ValueError("Invalid silent payment address: unknown HRP") + + # custom bech32 to bypass the 90-character limit + if (any(ord(x) < 33 or ord(x) > 126 for x in address)) or ( + address.lower() != address and address.upper() != address + ): + raise ValueError("Invalid silent payment address: invalid characters") + + address = address.lower() + pos = address.rfind("1") + if pos < 1 or pos + 7 > len(address): + raise ValueError("Invalid silent payment address: invalid format") + + if not all(x in bech32.CHARSET for x in address[pos + 1 :]): + raise ValueError( + "Invalid silent payment address: invalid characters in data part" + ) + + hrpgot = address[:pos] + data = [bech32.CHARSET.find(x) for x in address[pos + 1 :]] + + if hrpgot != hrp: + raise ValueError("Invalid silent payment address: HRP mismatch") + + encoding = bech32.bech32_verify_checksum(hrpgot, data) + if encoding is None: + raise ValueError("Invalid silent payment address: checksum verification failed") + + if encoding != bech32.Encoding.BECH32M: + raise ValueError("Invalid silent payment address: must use bech32m encoding") + + data = data[:-6] + + if data[0] != 0: + raise ValueError( + f"Invalid silent payment address: unsupported version {data[0]}" + ) + + decoded = bech32.convertbits(data[1:], 5, 8, False) + if decoded is None: + raise ValueError("Invalid silent payment address: conversion failed") + + try: + B_scan = ec.PublicKey.parse(bytes(decoded[:33])) + B_spend = ec.PublicKey.parse(bytes(decoded[33:])) + except Exception as e: + raise ValueError(f"Invalid silent payment address: invalid public keys - {e}") + + return B_scan, B_spend + + +def get_input_hash(outpoints: List["COutPoint"], sum_pubkey_bytes: bytes) -> bytes: + lowest_outpoint = sorted(outpoints, key=lambda o: o.serialize())[0] + preimage = lowest_outpoint.serialize() + sum_pubkey_bytes + return tagged_hash("BIP0352/Inputs", preimage) + + +def create_outputs( + input_privkeys: List[Tuple[bytes, bool]], + outpoints: List["COutPoint"], + recipients: List[str], +) -> Dict[str, List[str]]: + """ + Creates silent payment outputs for given recipients. + + Args: + input_privkeys: List of (private_key_bytes, is_xonly) tuples + outpoints: List of transaction outpoints + recipients: List of silent payment addresses (strings) - duplicates are allowed + + Returns: + Dictionary mapping each unique recipient address to list of output hex strings + """ + if not input_privkeys: + return {} + + signing_keys = [] + for sec, is_xonly in input_privkeys: + if not ec_seckey_verify(sec): + raise ValueError("Invalid private key") + + if is_xonly: + pub = ec_pubkey_create(sec) + ser = ec_pubkey_serialize(pub) + if ser[0] == 0x03: + sec = ec_privkey_negate(sec) + signing_keys.append(int.from_bytes(sec, "big")) + + a_sum = sum(signing_keys) % SECP256K1_ORDER + if a_sum == 0: + return {} + + a_sum_bytes = a_sum.to_bytes(32, "big") + A = ec_pubkey_create(a_sum_bytes) + + input_hash = get_input_hash(outpoints, ec_pubkey_serialize(A)) + + recipient_counts = Counter(recipients) + + groups: Dict[ec.PublicKey, List[Tuple[ec.PublicKey, str, int]]] = defaultdict(list) + for addr, count in recipient_counts.items(): + B_scan, B_spend = decode_silent_payment_address(addr) + groups[B_scan].append((B_spend, addr, count)) + + result: Dict[str, List[str]] = {addr: [] for addr in recipient_counts.keys()} + scalar = (int.from_bytes(input_hash, "big") * a_sum) % SECP256K1_ORDER + scalar_bytes = scalar.to_bytes(32, "big") + + for B_scan, B_spend_list in groups.items(): + ecdh_point = ec_pubkey_parse(B_scan.sec()) + ec_pubkey_tweak_mul(ecdh_point, scalar_bytes) + xonly_shared_secret = ec_pubkey_serialize(ecdh_point) + + k = 0 + for B_spend, addr, count in B_spend_list: + for _ in range(count): + t_k = tagged_hash( + "BIP0352/SharedSecret", + xonly_shared_secret + k.to_bytes(4, "big"), + ) + + P_k = ec_pubkey_parse(B_spend.sec()) + ec_pubkey_tweak_add(P_k, t_k) + + xonly = ec_pubkey_serialize(P_k)[1:33] + result[addr].append(hexlify(xonly).decode()) + k += 1 + + return result diff --git a/src/embit/transaction.py b/src/embit/transaction.py index 6eec9b87..1919a9d0 100644 --- a/src/embit/transaction.py +++ b/src/embit/transaction.py @@ -4,6 +4,7 @@ from .base import EmbitBase, EmbitError from .script import Script, Witness from .misc import const +from typing import NamedTuple class TransactionError(EmbitError): @@ -398,3 +399,13 @@ def read_from(cls, stream): value = int.from_bytes(stream.read(8), "little") script_pubkey = Script.read_from(stream) return cls(value, script_pubkey) + + +class COutPoint(NamedTuple): + txid: bytes # endianness same as hex string displayed; reverse of tx serialization order + out_idx: int + + def serialize(self) -> bytes: + return self.txid[::-1] + int.to_bytes( + self.out_idx, length=4, byteorder="little", signed=False + ) diff --git a/src/embit/util/key.py b/src/embit/util/key.py index f573ec5b..0d3c3833 100644 --- a/src/embit/util/key.py +++ b/src/embit/util/key.py @@ -2,6 +2,7 @@ Copy-paste from key.py in bitcoin test_framework. This is a fallback option if the library can't do ctypes bindings to secp256k1 library. """ + import random import hmac import hashlib @@ -291,6 +292,7 @@ def set(self, data): self.valid = False else: self.valid = False + return self @property def is_compressed(self): diff --git a/tests/tests/test_bip352.py b/tests/tests/test_bip352.py index 2883e141..49ee8e31 100644 --- a/tests/tests/test_bip352.py +++ b/tests/tests/test_bip352.py @@ -7,7 +7,7 @@ from unittest import TestCase import pytest -from embit import bip352 +from embit.silent_payments import bip352 from embit.ec import PrivateKey from embit.networks import NETWORKS @@ -44,23 +44,21 @@ def test_generate_silent_payment_address(self): spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) sp_address = bip352.generate_silent_payment_address( - scan_priv_key.get_public_key(), spend_priv_key.get_public_key() + scan_priv_key, spend_priv_key.get_public_key() ) assert sp_address == test_vector["sp_address"] def test_generate_silent_payment_address_for_network(self): """Test network silent payment addrs should start with "tsp" """ test_networks = [k for k in NETWORKS.keys() if k != "main"] - scan_pubkey = PrivateKey( - unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"]) - ).get_public_key() + scan_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])) spend_pubkey = PrivateKey( unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"]) ).get_public_key() for network in test_networks: payment_addr = bip352.generate_silent_payment_address( - scan_pubkey, spend_pubkey, network=network + scan_priv_key, spend_pubkey, network=network ) assert payment_addr.startswith("tsp") @@ -71,8 +69,8 @@ def test_generate_labeled_silent_payment_address(self): for label, address in zip( LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"] ): - sp_address = bip352.generate_labeled_silent_payment_address( - scan_priv_key, spend_priv_key.get_public_key(), label + sp_address = bip352.generate_silent_payment_address( + scan_priv_key, spend_priv_key.get_public_key(), label=label ) assert sp_address == address @@ -82,20 +80,26 @@ def test_generate_labeled_silent_payment_address_invalid_label(self): scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) spend_pubkey = spend_priv_key.get_public_key() - with pytest.raises(TypeError): - # Label is required - bip352.generate_labeled_silent_payment_address(scan_priv_key, spend_pubkey) - for bad_label in ["tenant 6102", b"I am bytes", 1.0, True]: with pytest.raises(TypeError): # Label must be an int (and not a bool) - bip352.generate_labeled_silent_payment_address( + bip352.generate_silent_payment_address( scan_priv_key, spend_pubkey, label=bad_label ) for bad_label in [0, -1, 0x100000000]: with pytest.raises(ValueError): # m = 0 is reserved for change; values must fit in 32 bits - bip352.generate_labeled_silent_payment_address( + bip352.generate_silent_payment_address( scan_priv_key, spend_pubkey, label=bad_label ) + + def test_decode_silent_payment_address_round_trip(self): + """A generated address should decode back to its scan/spend pubkeys""" + spend_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])) + address = BASIC_TEST_VECTORS[0]["sp_address"] + + B_scan, B_spend = bip352.decode_silent_payment_address(address) + assert B_scan.sec() == scan_priv_key.get_public_key().sec() + assert B_spend.sec() == spend_priv_key.get_public_key().sec() From 735eef5ad82cc3e9be6a6738fbf5df97e4bf4ac0 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Thu, 21 May 2026 13:55:17 +0530 Subject: [PATCH 09/15] test(sp): outputs and BIP-352 test vectors --- .../data/send_and_receive_test_vectors.json | 2760 +++++++++++++++++ tests/tests/test_bip352.py | 210 +- 2 files changed, 2931 insertions(+), 39 deletions(-) create mode 100644 tests/tests/data/send_and_receive_test_vectors.json diff --git a/tests/tests/data/send_and_receive_test_vectors.json b/tests/tests/data/send_and_receive_test_vectors.json new file mode 100644 index 00000000..264f7bec --- /dev/null +++ b/tests/tests/data/send_and_receive_test_vectors.json @@ -0,0 +1,2760 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" + } + ] + } + } + ] + }, + { + "comment": "Outpoint ordering byte-lexicographically vs. vout-integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "c8ac0292997b5bca98b3ebd99a57e253071137550f270452cd3df8a3e2266d36", + "pub_key": "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", + "signature": "c036ee38bfe46aba03234339ae7219b31b824b52ef9d5ce05810a0d6f62330dedc2b55652578aa5bdabf930fae941acd839d5a66f8fce7caa9710ccb446bddd1" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + } + ], + "outputs": [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + } + ], + "outputs": [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + } + ], + "outputs": [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ] + }, + "expected": { + "outputs": [ + [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + }, + { + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with even parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq" + ] + }, + "expected": { + "outputs": [ + [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n" + ] + }, + "expected": { + "outputs": [ + [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: large label integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ] + }, + "expected": { + "outputs": [ + [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; same recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ] + }, + "expected": { + "outputs": [ + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1, + 1337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ], + "outputs": [ + { + "priv_key_tweak": "4e3352fbe0505c25e718d96007c259ef08db34f8c844e4ff742d9855ff03805a", + "pub_key": "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "signature": "6eeae1ea9eb826e3d0e812f65937100e0836ea188c04f36fabc4981eda29de8d3d3529390a0a8b3d830f7bca4f5eae5994b9788ddaf05ad259ffe26d86144b4b" + }, + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "bf709f98d4418f8a67e738154ae48818dad44689cd37fbc070891a396dd1c633", + "pub_key": "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "signature": "42a19fd8a63dde1824966a95d65a28203e631e49bf96ca5dae1b390e7a0ace2cc8709c9b0c5715047032f57f536a3c80273cbecf4c05be0b5456c183fa122c06" + }, + { + "priv_key_tweak": "736f05e4e3072c3b8656bedef2e9bf54cbcaa2b6fe5320d3e86f5b96874dda71", + "pub_key": "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "signature": "2e61bb3d79418ecf55f68847cf121bfc12d397b39d1da8643246b2f0a9b96c3daa4bfe9651beb5c9ce20e1f29282c4566400a4b45ee6657ec3b18fdc554da0b4" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ] + }, + "expected": { + "outputs": [ + [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ], + "outputs": [ + { + "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" + } + ] + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with NUMS point", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + } + } + ], + "outputs": [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" + } + ] + } + } + ] + }, + { + "comment": "Pubkey extraction from malleated p2pkh", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + } + } + ], + "outputs": [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" + } + ] + } + } + ] + }, + { + "comment": "P2PKH and P2WPKH Uncompressed Keys are skipped", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Skip invalid P2SH inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Recipient ignores unrelated outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ] + }, + "expected": { + "outputs": [ + [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + }, + { + "comment": "No valid inputs, sender generates no outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + }, + { + "comment": "Input keys sum up to zero / point at infinity: sending fails, receiver skips tx", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + } + ], + "recipients": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ] + }, + "expected": { + "outputs": [ + [] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + } + ], + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "key_material": { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ], + "outputs": [] + } + } + ] + } +] diff --git a/tests/tests/test_bip352.py b/tests/tests/test_bip352.py index 49ee8e31..b4da4091 100644 --- a/tests/tests/test_bip352.py +++ b/tests/tests/test_bip352.py @@ -9,7 +9,87 @@ import pytest from embit.silent_payments import bip352 from embit.ec import PrivateKey -from embit.networks import NETWORKS +import os +import json +from embit import hashes +from embit.script import Script, Witness +from embit.transaction import COutPoint +from embit.util.key import ECPubKey +from embit.ec import NUMS_PUBKEY + + +def get_input_pubkey(prevout_script, script_sig=None, witness=None) -> ECPubKey: + """Extract and validate the input pubkey for a prevout, by script type. + + Test helper for BIP-352 send vectors. Returns an ECPubKey with .valid=False + when no suitable compressed pubkey can be determined. + """ + spk = ( + prevout_script if isinstance(prevout_script, Script) else Script(prevout_script) + ) + + if isinstance(script_sig, str): + try: + ss = bytes.fromhex(script_sig) + except Exception: + ss = b"" + elif isinstance(script_sig, bytes): + ss = script_sig + else: + ss = b"" + + if isinstance(witness, Witness): + wstack = witness.items + elif isinstance(witness, list): + wstack = witness + else: + wstack = [] + + script_type = spk.script_type() + + def _compressed(pubkey_bytes): + pubkey = ECPubKey().set(pubkey_bytes) + return pubkey if (pubkey.valid and pubkey.is_compressed) else None + + if script_type == "p2pkh": + spk_hash = spk.data[3:23] + for i in range(len(ss), 32, -1): + if i >= 33: + pubkey_bytes = ss[i - 33 : i] + if ( + pubkey_bytes[0] in (0x02, 0x03) + and hashes.hash160(pubkey_bytes) == spk_hash + ): + pubkey = _compressed(pubkey_bytes) + if pubkey: + return pubkey + return ECPubKey() + + if script_type in ("p2sh", "p2wpkh"): + if wstack and (script_type == "p2wpkh" or len(ss) > 1): + pubkey = _compressed(wstack[-1]) + if pubkey: + return pubkey + return ECPubKey() + + if script_type == "p2tr": + if wstack: + # strip annex if present (last element starting with 0x50) + if len(wstack) > 1 and wstack[-1][:1] == b"\x50": + wstack = wstack[:-1] + # Script-path spend with NUMS internal key: not key-spendable + if len(wstack) > 1: + control_block = wstack[-1] + if len(control_block) >= 33 and control_block[1:33] == NUMS_PUBKEY.xonly(): + return ECPubKey() + # Key-path spend: reconstruct even-y compressed SEC from x-only + if len(spk.data) >= 34: + pubkey = ECPubKey().set(b"\x02" + spk.data[2:34]) + if pubkey.valid: + return pubkey + + return ECPubKey() + BASIC_TEST_VECTORS = [ { @@ -37,6 +117,9 @@ } +INVALID_LABEL_TEST_VECTORS = ["not an int", 99999999999999999999999999, -15, 1.0] + + class BIP352Test(TestCase): def test_generate_silent_payment_address(self): """Should generate the expected silent payment address""" @@ -48,20 +131,6 @@ def test_generate_silent_payment_address(self): ) assert sp_address == test_vector["sp_address"] - def test_generate_silent_payment_address_for_network(self): - """Test network silent payment addrs should start with "tsp" """ - test_networks = [k for k in NETWORKS.keys() if k != "main"] - scan_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])) - spend_pubkey = PrivateKey( - unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"]) - ).get_public_key() - - for network in test_networks: - payment_addr = bip352.generate_silent_payment_address( - scan_priv_key, spend_pubkey, network=network - ) - assert payment_addr.startswith("tsp") - def test_generate_labeled_silent_payment_address(self): """Should generate the expected labeled silent payment addresses""" spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) @@ -70,36 +139,99 @@ def test_generate_labeled_silent_payment_address(self): LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"] ): sp_address = bip352.generate_silent_payment_address( - scan_priv_key, spend_priv_key.get_public_key(), label=label + scan_priv_key, spend_priv_key.get_public_key(), label ) assert sp_address == address - def test_generate_labeled_silent_payment_address_invalid_label(self): - """Labels must be 32-bit unsigned ints in [1, 2**32 - 1]""" - spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) - scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) - spend_pubkey = spend_priv_key.get_public_key() - - for bad_label in ["tenant 6102", b"I am bytes", 1.0, True]: - with pytest.raises(TypeError): - # Label must be an int (and not a bool) + with pytest.raises(Exception): + for label in INVALID_LABEL_TEST_VECTORS: bip352.generate_silent_payment_address( - scan_priv_key, spend_pubkey, label=bad_label + scan_priv_key, spend_priv_key.get_public_key(), label ) - for bad_label in [0, -1, 0x100000000]: - with pytest.raises(ValueError): - # m = 0 is reserved for change; values must fit in 32 bits - bip352.generate_silent_payment_address( - scan_priv_key, spend_pubkey, label=bad_label + def test_decode_silent_payment_address(self): + """Should decode the silent payment address and return the expected keys""" + for test_vector in BASIC_TEST_VECTORS: + scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) + spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) + B_scan, B_spend = bip352.decode_silent_payment_address( + test_vector["sp_address"] + ) + + assert B_scan == scan_priv_key.get_public_key() + assert B_spend == spend_priv_key.get_public_key() + + with pytest.raises(ValueError): + # Invalid HRP + bip352.decode_silent_payment_address( + "st1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ) + + with pytest.raises(ValueError): + # Invalid encoding + bip352.decode_silent_payment_address( + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwvm" + ) + + def test_create_silent_payments_outputs(self): + """Test silent payment output generation using test vectors""" + __location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__)) + ) + with open( + os.path.join(__location__, "data/send_and_receive_test_vectors.json"), "r" + ) as f: + SEND_AND_RECEIVE_TEST_VECTORS = json.load(f) + + from io import BytesIO + + for case in SEND_AND_RECEIVE_TEST_VECTORS: + for sending_test in case["sending"]: + given = sending_test["given"] + expected = sending_test["expected"] + + outpoints: list[COutPoint] = [] + input_privkeys: list[tuple] = [] + + for txin in given["vin"]: + outpoints.append( + COutPoint(txid=unhexlify(txin["txid"]), out_idx=txin["vout"]) + ) + + spk_hex = txin["prevout"]["scriptPubKey"]["hex"] + spk = Script(unhexlify(spk_hex)) + + wit_hex = txin.get("txinwitness", "") or "" + witness = None + if wit_hex: + try: + witness = Witness.read_from(BytesIO(bytes.fromhex(wit_hex))) + except Exception: + witness = None + + pub = get_input_pubkey(spk, txin.get("scriptSig", ""), witness) + if not getattr(pub, "valid", False): + continue + + is_xonly = spk.script_type() == "p2tr" + input_privkeys.append((unhexlify(txin["private_key"]), is_xonly)) + + outputs_map = bip352.create_outputs( + input_privkeys=input_privkeys, + outpoints=outpoints, + recipients=given["recipients"], ) - def test_decode_silent_payment_address_round_trip(self): - """A generated address should decode back to its scan/spend pubkeys""" - spend_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["spend_priv_key"])) - scan_priv_key = PrivateKey(unhexlify(BASIC_TEST_VECTORS[0]["scan_priv_key"])) - address = BASIC_TEST_VECTORS[0]["sp_address"] + expected_outputs = expected["outputs"] - B_scan, B_spend = bip352.decode_silent_payment_address(address) - assert B_scan.sec() == scan_priv_key.get_public_key().sec() - assert B_spend.sec() == spend_priv_key.get_public_key().sec() + actual_outputs = [] + for recipient, outputs in outputs_map.items(): + actual_outputs.extend(outputs) + + self.assertTrue( + any( + set(actual_outputs) == set(expected_set) + for expected_set in expected_outputs + ), + f"Actual outputs {set(actual_outputs)} did not match any expected set {expected_outputs}", + ) From a7ebf3f2078363a954f2712aacc360d3037d9269 Mon Sep 17 00:00:00 2001 From: odudex Date: Fri, 29 May 2026 10:19:46 -0300 Subject: [PATCH 10/15] sp: make silent payment code MicroPython-safe Drop the typing and collections imports (Counter/defaultdict, Tuple/List/Dict) and convert COutPoint from typing.NamedTuple to a plain class with positional int.to_bytes. Replace remaining f-strings with str.format and remove the now unused COutPoint import from bip352. --- src/embit/silent_payments/bip352.py | 30 +++++++++++++---------------- src/embit/transaction.py | 13 ++++++------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/embit/silent_payments/bip352.py b/src/embit/silent_payments/bip352.py index d8548ca8..635b1330 100644 --- a/src/embit/silent_payments/bip352.py +++ b/src/embit/silent_payments/bip352.py @@ -3,14 +3,10 @@ see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki """ -from collections import Counter, defaultdict -from typing import Tuple, List, Dict - from .. import bech32, ec from ..util import secp256k1 from ..hashes import tagged_hash from ..util.key import SECP256K1_ORDER -from ..transaction import COutPoint from ..util.secp256k1 import ( ec_pubkey_create, ec_pubkey_serialize, @@ -63,7 +59,7 @@ def generate_silent_payment_address( # TODO: use the bech32 decode function once the flexible bech32 PR is in -def decode_silent_payment_address(address: str) -> Tuple[ec.PublicKey, ec.PublicKey]: +def decode_silent_payment_address(address: str): """ Decode a silent payment address and return the scan and spend public keys. Silent payment addresses can be longer than 90 characters, so we need custom decoding. @@ -108,7 +104,7 @@ def decode_silent_payment_address(address: str) -> Tuple[ec.PublicKey, ec.Public if data[0] != 0: raise ValueError( - f"Invalid silent payment address: unsupported version {data[0]}" + "Invalid silent payment address: unsupported version {}".format(data[0]) ) decoded = bech32.convertbits(data[1:], 5, 8, False) @@ -119,22 +115,20 @@ def decode_silent_payment_address(address: str) -> Tuple[ec.PublicKey, ec.Public B_scan = ec.PublicKey.parse(bytes(decoded[:33])) B_spend = ec.PublicKey.parse(bytes(decoded[33:])) except Exception as e: - raise ValueError(f"Invalid silent payment address: invalid public keys - {e}") + raise ValueError( + "Invalid silent payment address: invalid public keys - {}".format(e) + ) return B_scan, B_spend -def get_input_hash(outpoints: List["COutPoint"], sum_pubkey_bytes: bytes) -> bytes: +def get_input_hash(outpoints, sum_pubkey_bytes: bytes) -> bytes: lowest_outpoint = sorted(outpoints, key=lambda o: o.serialize())[0] preimage = lowest_outpoint.serialize() + sum_pubkey_bytes return tagged_hash("BIP0352/Inputs", preimage) -def create_outputs( - input_privkeys: List[Tuple[bytes, bool]], - outpoints: List["COutPoint"], - recipients: List[str], -) -> Dict[str, List[str]]: +def create_outputs(input_privkeys, outpoints, recipients): """ Creates silent payment outputs for given recipients. @@ -170,14 +164,16 @@ def create_outputs( input_hash = get_input_hash(outpoints, ec_pubkey_serialize(A)) - recipient_counts = Counter(recipients) + recipient_counts = {} + for addr in recipients: + recipient_counts[addr] = recipient_counts.get(addr, 0) + 1 - groups: Dict[ec.PublicKey, List[Tuple[ec.PublicKey, str, int]]] = defaultdict(list) + groups = {} for addr, count in recipient_counts.items(): B_scan, B_spend = decode_silent_payment_address(addr) - groups[B_scan].append((B_spend, addr, count)) + groups.setdefault(B_scan, []).append((B_spend, addr, count)) - result: Dict[str, List[str]] = {addr: [] for addr in recipient_counts.keys()} + result = {addr: [] for addr in recipient_counts.keys()} scalar = (int.from_bytes(input_hash, "big") * a_sum) % SECP256K1_ORDER scalar_bytes = scalar.to_bytes(32, "big") diff --git a/src/embit/transaction.py b/src/embit/transaction.py index 1919a9d0..fb64d8c7 100644 --- a/src/embit/transaction.py +++ b/src/embit/transaction.py @@ -4,7 +4,6 @@ from .base import EmbitBase, EmbitError from .script import Script, Witness from .misc import const -from typing import NamedTuple class TransactionError(EmbitError): @@ -401,11 +400,11 @@ def read_from(cls, stream): return cls(value, script_pubkey) -class COutPoint(NamedTuple): - txid: bytes # endianness same as hex string displayed; reverse of tx serialization order - out_idx: int +class COutPoint: + def __init__(self, txid: bytes, out_idx: int): + # txid endianness same as hex string displayed; reverse of tx serialization order + self.txid = txid + self.out_idx = out_idx def serialize(self) -> bytes: - return self.txid[::-1] + int.to_bytes( - self.out_idx, length=4, byteorder="little", signed=False - ) + return self.txid[::-1] + self.out_idx.to_bytes(4, "little") From de23863b5d163ccc9da1de11d95cae85f704c41d Mon Sep 17 00:00:00 2001 From: notTanveer Date: Tue, 26 May 2026 01:53:39 +0530 Subject: [PATCH 11/15] feat(BIP-392): sp descriptors --- src/embit/bech32.py | 21 +++ src/embit/descriptor/__init__.py | 1 + src/embit/descriptor/descriptor.py | 8 + src/embit/descriptor/sp.py | 286 +++++++++++++++++++++++++++++ tests/tests/test_sp_descriptor.py | 277 ++++++++++++++++++++++++++++ 5 files changed, 593 insertions(+) create mode 100644 src/embit/descriptor/sp.py create mode 100644 tests/tests/test_sp_descriptor.py diff --git a/src/embit/bech32.py b/src/embit/bech32.py index b6766c52..b230f180 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -112,6 +112,27 @@ def bech32_decode(bech): return (encoding, hrp, data[:-6]) +# TODO: remove this once flexible bech32 is in +def bech32_decode_long(bech): + """Like bech32_decode but without the 90-character length limit.""" + if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( + bech.lower() != bech and bech.upper() != bech + ): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind("1") + if pos < 1 or pos + 7 > len(bech): + return (None, None, None) + if not all(x in CHARSET for x in bech[pos + 1 :]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos + 1 :]] + encoding = bech32_verify_checksum(hrp, data) + if encoding is None: + return (None, None, None) + return (encoding, hrp, data[:-6]) + + def convertbits(data, frombits, tobits, pad=True): """General power-of-2 base conversion.""" acc = 0 diff --git a/src/embit/descriptor/__init__.py b/src/embit/descriptor/__init__.py index 600296d3..00298b68 100644 --- a/src/embit/descriptor/__init__.py +++ b/src/embit/descriptor/__init__.py @@ -1,3 +1,4 @@ from . import miniscript from .descriptor import Descriptor from .arguments import Key +from .sp import SilentPaymentDescriptor diff --git a/src/embit/descriptor/descriptor.py b/src/embit/descriptor/descriptor.py index fd3c3057..c0737e74 100644 --- a/src/embit/descriptor/descriptor.py +++ b/src/embit/descriptor/descriptor.py @@ -6,6 +6,7 @@ from .miniscript import Miniscript, Multi, Sortedmulti from .arguments import Key from .taptree import TapTree +from .sp import SilentPaymentDescriptor class Descriptor(DescriptorBase): @@ -302,6 +303,13 @@ def read_from(cls, s): is_miniscript = True taproot = False taptree = TapTree() + if start.startswith(b"sp("): + s.seek(-5, 1) + sp_desc = SilentPaymentDescriptor.read_from(s) + end = s.read(1) + if end != b")": + raise DescriptorError("Expected closing ) for sp()") + return sp_desc if start.startswith(b"tr("): taproot = True s.seek(-5, 1) diff --git a/src/embit/descriptor/sp.py b/src/embit/descriptor/sp.py new file mode 100644 index 00000000..94c6c823 --- /dev/null +++ b/src/embit/descriptor/sp.py @@ -0,0 +1,286 @@ +from io import BytesIO +from .. import bech32, ec +from ..misc import read_until +from .base import DescriptorBase +from .errors import DescriptorError +from .arguments import KeyOrigin, Key + +SPSCAN_HRPS = {"spscan": "main", "tspscan": "test"} +SPSPEND_HRPS = {"spspend": "main", "tspspend": "test"} +SP_KEY_HRPS = {**SPSCAN_HRPS, **SPSPEND_HRPS} + + +def _bech32m_decode_sp_key(encoded): + encoding, hrp, data = bech32.bech32_decode_long(encoded) + if encoding is None: + raise DescriptorError("Invalid bech32m encoding in SP key: %s" % encoded) + if encoding != bech32.Encoding.BECH32M: + raise DescriptorError("SP key must use bech32m encoding") + if hrp not in SP_KEY_HRPS: + raise DescriptorError("Unknown SP key HRP: %s" % hrp) + if len(data) < 1: + raise DescriptorError("SP key data too short") + version = data[0] + if version != 0: + raise DescriptorError("Unsupported SP key version: %d" % version) + payload = bech32.convertbits(data[1:], 5, 8, False) + if payload is None: + raise DescriptorError("Invalid SP key payload encoding") + return hrp, bytes(payload) + + +def _bech32m_encode_sp_key(hrp, payload): + data = bech32.convertbits(payload, 8, 5) + return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [0] + data) + + +class SPScanKey: + """spscan key expression: encodes scan_privkey + spend_pubkey.""" + + def __init__(self, scan_privkey, spend_pubkey, origin=None, network="main"): + if not isinstance(scan_privkey, ec.PrivateKey): + raise DescriptorError("SPScanKey scan key must be a PrivateKey") + if not isinstance(spend_pubkey, ec.PublicKey): + raise DescriptorError("SPScanKey spend key must be a PublicKey") + self.scan_privkey = scan_privkey + self.spend_pubkey = spend_pubkey + self.origin = origin + self.network = network + + @property + def is_watch_only(self): + return True + + @classmethod + def decode(cls, encoded, origin=None): + hrp, payload = _bech32m_decode_sp_key(encoded) + if hrp not in SPSCAN_HRPS: + raise DescriptorError("Expected spscan HRP, got: %s" % hrp) + if len(payload) != 65: + raise DescriptorError( + "spscan payload must be 65 bytes (32 privkey + 33 pubkey), got %d" + % len(payload) + ) + scan_privkey = ec.PrivateKey(payload[:32]) + spend_pubkey = ec.PublicKey.parse(payload[32:65]) + network = SPSCAN_HRPS[hrp] + return cls(scan_privkey, spend_pubkey, origin, network) + + def encode(self): + hrp = "tspscan" if self.network == "test" else "spscan" + payload = self.scan_privkey.secret + self.spend_pubkey.sec() + return _bech32m_encode_sp_key(hrp, payload) + + def __str__(self): + prefix = "[%s]" % self.origin if self.origin else "" + return prefix + self.encode() + + +class SPSpendKey: + """spspend key expression: encodes scan_privkey + spend_privkey.""" + + def __init__(self, scan_privkey, spend_privkey, origin=None, network="main"): + if not isinstance(scan_privkey, ec.PrivateKey): + raise DescriptorError("SPSpendKey scan key must be a PrivateKey") + if not isinstance(spend_privkey, ec.PrivateKey): + raise DescriptorError("SPSpendKey spend key must be a PrivateKey") + self.scan_privkey = scan_privkey + self.spend_privkey = spend_privkey + self.origin = origin + self.network = network + + @property + def spend_pubkey(self): + return self.spend_privkey.get_public_key() + + @property + def is_watch_only(self): + return False + + @classmethod + def decode(cls, encoded, origin=None): + hrp, payload = _bech32m_decode_sp_key(encoded) + if hrp not in SPSPEND_HRPS: + raise DescriptorError("Expected spspend HRP, got: %s" % hrp) + if len(payload) != 64: + raise DescriptorError( + "spspend payload must be 64 bytes (32 + 32), got %d" % len(payload) + ) + scan_privkey = ec.PrivateKey(payload[:32]) + spend_privkey = ec.PrivateKey(payload[32:64]) + network = SPSPEND_HRPS[hrp] + return cls(scan_privkey, spend_privkey, origin, network) + + def encode(self): + hrp = "tspspend" if self.network == "test" else "spspend" + payload = self.scan_privkey.secret + self.spend_privkey.secret + return _bech32m_encode_sp_key(hrp, payload) + + def __str__(self): + prefix = "[%s]" % self.origin if self.origin else "" + return prefix + self.encode() + + +def _read_sp_key_expression(s): + """Read an spscan/spspend expression or a standard Key from stream.""" + first = s.read(1) + origin = None + if first == b"[": + prefix, char = read_until(s, b"]") + if char != b"]": + raise DescriptorError("Invalid key - missing ]") + origin = KeyOrigin.from_string(prefix.decode()) + else: + s.seek(-1, 1) + + pos_before = s.tell() + token, char = read_until(s, b",)") + if char is not None: + s.seek(-1, 1) + token_str = token.decode() + + lower = token_str.lower() + for hrp in SPSCAN_HRPS: + if lower.startswith(hrp + "1"): + return SPScanKey.decode(token_str, origin), char + for hrp in SPSPEND_HRPS: + if lower.startswith(hrp + "1"): + return SPSpendKey.decode(token_str, origin), char + + s.seek(pos_before) + if origin: + origin_str = "[%s]" % origin + combined = BytesIO(origin_str.encode() + s.read()) + key = Key.read_from(combined) + else: + key = Key.read_from(s) + return key, None + + +class SilentPaymentDescriptor(DescriptorBase): + """BIP-392 sp() descriptor for Silent Payments.""" + + def __init__(self, sp_key=None, scan_key=None, spend_key=None): + if sp_key is not None: + if not isinstance(sp_key, (SPScanKey, SPSpendKey)): + raise DescriptorError( + "Single-arg sp() requires an spscan or spspend key expression" + ) + self.sp_key = sp_key + self.scan_key = None + self.spend_key = None + elif scan_key is not None: + if not _is_private_key(scan_key): + raise DescriptorError("Two-arg sp(): scan key must be private") + self.sp_key = None + self.scan_key = scan_key + self.spend_key = spend_key + else: + raise DescriptorError("sp() requires at least one argument") + + @property + def is_single_arg(self): + return self.sp_key is not None + + @property + def is_watch_only(self): + if self.sp_key is not None: + return self.sp_key.is_watch_only + return not _is_private_key(self.spend_key) + + @property + def keys(self): + if self.sp_key is not None: + return [self.sp_key] + return [self.scan_key, self.spend_key] + + def get_scan_privkey(self): + if self.sp_key is not None: + return self.sp_key.scan_privkey + k = self.scan_key + if isinstance(k, Key): + return k.private_key + return None + + def get_spend_pubkey(self): + if self.sp_key is not None: + return self.sp_key.spend_pubkey + k = self.spend_key + if isinstance(k, Key): + return k.get_public_key() + return None + + @classmethod + def from_string(cls, desc): + if "#" in desc: + desc = desc.split("#")[0] + s = BytesIO(desc.encode()) + start = s.read(3) + if start != b"sp(": + raise DescriptorError("Expected sp( prefix, got: %s" % start.decode()) + res = cls._read_args(s) + end = s.read(1) + if end != b")": + raise DescriptorError("Expected closing ) for sp()") + left = s.read() + if len(left) > 0: + raise DescriptorError("Unexpected characters after sp(): %r" % left) + return res + + @classmethod + def read_from(cls, s): + return cls._read_args(s) + + @classmethod + def _read_args(cls, s): + first_arg, sep = _read_sp_key_expression(s) + + if isinstance(first_arg, (SPScanKey, SPSpendKey)): + c = s.read(1) + if c == b")": + s.seek(-1, 1) + return cls(sp_key=first_arg) + raise DescriptorError( + "spscan/spspend key must be the only argument to sp()" + ) + + c = s.read(1) + if c != b",": + raise DescriptorError( + "Single-arg sp() requires spscan or spspend key expression, " + "got a standard key" + ) + + scan_key = first_arg + if not _is_private_key(scan_key): + raise DescriptorError("Two-arg sp(): scan key must be private") + if isinstance(scan_key.key, ec.PrivateKey) and not scan_key.key.compressed: + raise DescriptorError("Uncompressed keys are not allowed in sp()") + + spend_arg, _ = _read_sp_key_expression(s) + if isinstance(spend_arg, (SPScanKey, SPSpendKey)): + raise DescriptorError( + "Two-arg sp() cannot use spscan/spspend key expressions" + ) + if isinstance(spend_arg, Key) and isinstance(spend_arg.key, ec.PrivateKey): + if not spend_arg.key.compressed: + raise DescriptorError("Uncompressed keys are not allowed in sp()") + + return cls(scan_key=scan_key, spend_key=spend_arg) + + def to_string(self): + if self.sp_key is not None: + return "sp(%s)" % self.sp_key + return "sp(%s,%s)" % (self.scan_key, self.spend_key) + + def __str__(self): + return self.to_string() + + def __repr__(self): + return self.to_string() + + +def _is_private_key(key): + if isinstance(key, Key): + return key.is_private + return False diff --git a/tests/tests/test_sp_descriptor.py b/tests/tests/test_sp_descriptor.py new file mode 100644 index 00000000..bd48f613 --- /dev/null +++ b/tests/tests/test_sp_descriptor.py @@ -0,0 +1,277 @@ +from unittest import TestCase +from embit.descriptor import SilentPaymentDescriptor +from embit.descriptor.sp import SPScanKey, SPSpendKey +from embit.descriptor.arguments import KeyOrigin +from embit.descriptor.errors import DescriptorError +from embit import bip32, bip39, ec +from binascii import unhexlify + +# TODO: add more test vectors +VECTORS = [ + { + "mnemonic": "initial tilt corn easily leave weather strategy return topple gesture sad day", + "coin_type": 1, + "spscan": "tspscan1q09zrmaz09cdzs5jxm552qpv3f2gxd9vxhs0yady09jdd6aqt5e7s9fue8565hmue30u47mvc6rqwwwh0zw6ptjtqzwq7kr6h27sa09f5g6x977", + "spspend": "tspspend1q09zrmaz09cdzs5jxm552qpv3f2gxd9vxhs0yady09jdd6aqt5e772yf8kwpa7shfhuw9esasvgn8lh7e6ufea60fpvfx9dk7m3klg6sa90au8", + }, + { + "mnemonic": "tongue vanish post gentle fever figure kangaroo select infant blur phrase relief", + "coin_type": 0, + "spscan": "spscan1qnd95fpg2587jn73qg98pq8uk20y09v5c20u0e4kynsc4m2qmkrrs9cahrrlzln5nreangzkja2mj8pnwrfwudqws4vl3at3zyw2tslxtryq7pn", + "spspend": "spspend1qnd95fpg2587jn73qg98pq8uk20y09v5c20u0e4kynsc4m2qmkrrmd9tyggwt47773rhumkklet4g2us7c3x0gul65za8fg32nansdesukdqr2", + }, + { + "mnemonic": "index today witness obscure ugly curtain symbol pumpkin pelican child maple struggle arctic water tiny pizza harbor below violin eight tennis frost clown hood", + "coin_type": 1, + "spscan": "tspscan1q0z4tkwaar4ww77qgesalgzw0c40q89zh7p7hmp3qn73yrdw9jpvs9yjn9d7puunttpfjuale84erzh2z636fqgy63gp7m52v5hcnmmrrlrxnur", + "spspend": "tspspend1q0z4tkwaar4ww77qgesalgzw0c40q89zh7p7hmp3qn73yrdw9jpvnadsvv7qqcd8ytmdtzn6r5ywvccgzpw2386spvymglmszzep0svg39qpev", + }, + { + "mnemonic": "fold cotton pipe robust eagle rabbit coach average orient utility minor absurd fine claim artist rabbit kingdom original lobster cruise march city vibrant resemble", + "coin_type": 0, + "spscan": "spscan1q79q4zljllyehszny72w5zfptzpxnp96esg0n2fwecgzd2v7fr6fsy54qq8jfr6mm3pgrze8hln43my7epsfkg98wtl77ch6r5lz6pedd2jcnxk", + "spspend": "spspend1q79q4zljllyehszny72w5zfptzpxnp96esg0n2fwecgzd2v7fr6fs8y6dg3fu9jp5rhrycnuhtd555t6904x4xs7cklka8z5tk5p9xwq82pf6k", + }, +] + + +def _derive_sp_keys(mnemonic, coin_type): + seed = bip39.mnemonic_to_seed(mnemonic) + master = bip32.HDKey.from_seed(seed) + scan_priv = master.derive("m/352h/%dh/0h/1h/0" % coin_type).key + spend_priv = master.derive("m/352h/%dh/0h/0h/0" % coin_type).key + return scan_priv, spend_priv + + +class TestMnemonicVectors(TestCase): + def _keys(self, v): + network = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + return network, scan_priv, spend_priv, spend_priv.get_public_key() + + def test_spscan_encoding(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + self.assertEqual( + SPScanKey(scan_priv, spend_pub, network=net).encode(), v["spscan"] + ) + + def test_spspend_encoding(self): + for v in VECTORS: + net, scan_priv, spend_priv, _ = self._keys(v) + self.assertEqual( + SPSpendKey(scan_priv, spend_priv, network=net).encode(), v["spspend"] + ) + + def test_descriptor_roundtrip_spscan(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertTrue(desc.is_watch_only) + + def test_descriptor_roundtrip_spspend(self): + for v in VECTORS: + net, scan_priv, spend_priv, _ = self._keys(v) + desc_str = ( + "sp(%s)" % SPSpendKey(scan_priv, spend_priv, network=net).encode() + ) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertFalse(desc.is_watch_only) + + def test_keys_extractable_from_descriptor(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + spscan = SPScanKey(scan_priv, spend_pub, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % spscan.encode()) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + +class TestKeyOrigin(TestCase): + """Key origin prefix [fingerprint/path] is parsed and preserved in SP descriptors.""" + + def test_spscan_with_origin_roundtrip(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("deadbeef/352h/%dh/0h" % v["coin_type"]) + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc_str = "sp(%s)" % str(spscan) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertTrue(desc.is_watch_only) + + def test_spspend_with_origin_roundtrip(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + origin = KeyOrigin.from_string("cafebabe/352h/%dh/0h" % v["coin_type"]) + spspend = SPSpendKey(scan_priv, spend_priv, origin=origin, network=net) + desc_str = "sp(%s)" % str(spspend) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertFalse(desc.is_watch_only) + + def test_parsed_key_retains_origin_fingerprint(self): + v = VECTORS[0] + net = "test" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("deadbeef/352h/1h/0h") + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % str(spscan)) + self.assertIsNotNone(desc.sp_key.origin) + self.assertEqual(desc.sp_key.origin.fingerprint, unhexlify("deadbeef")) + + def test_origin_does_not_affect_key_content(self): + """Origin prefix doesn't change the encoded key bytes.""" + v = VECTORS[1] + net = "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("cafebabe/352h/0h/0h") + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % str(spscan)) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + +class TestTwoArgDescriptor(TestCase): + """sp(scan_key, spend_key) two-argument descriptor form.""" + + def _leaf_hdkeys(self, v): + seed = bip39.mnemonic_to_seed(v["mnemonic"]) + master = bip32.HDKey.from_seed(seed) + ct = v["coin_type"] + scan_hd = master.derive("m/352h/%dh/0h/1h/0" % ct) + spend_hd_priv = master.derive("m/352h/%dh/0h/0h/0" % ct) + spend_hd_pub = spend_hd_priv.to_public() + return scan_hd, spend_hd_priv, spend_hd_pub + + def test_xprv_xpub_is_watch_only(self): + for v in VECTORS: + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertFalse(desc.is_single_arg) + self.assertTrue(desc.is_watch_only) + + def test_xprv_xprv_is_not_watch_only(self): + for v in VECTORS: + scan_hd, spend_hd_priv, _ = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_priv.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertFalse(desc.is_single_arg) + self.assertFalse(desc.is_watch_only) + + def test_two_arg_roundtrip(self): + for v in VECTORS: + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + + def test_two_arg_keys_match_direct_derivation(self): + """Two-arg form with leaf-level keys produces the same scan/spend keys as direct derivation.""" + for v in VECTORS: + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + +class TestChecksumHandling(TestCase): + """Descriptor #checksum suffix is stripped during parsing.""" + + def test_checksum_suffix_stripped(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str + "#aaaaaaaa") + self.assertEqual(str(desc), desc_str) + + def test_no_checksum_parses_normally(self): + v = VECTORS[0] + net = "test" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertTrue(desc.is_watch_only) + self.assertTrue(desc.is_single_arg) + + +class TestInvalidDescriptor(TestCase): + """Invalid sp() descriptors raise DescriptorError.""" + + def _scan_priv(self): + return ec.PrivateKey(bytes([0x01] * 32)) + + def _spend_pub(self): + return ec.PrivateKey(bytes([0x02] * 32)).get_public_key() + + def test_empty_sp(self): + self.assertRaises(Exception, SilentPaymentDescriptor.from_string, "sp()") + + def test_bare_xpub_single_arg(self): + """Single-arg sp() with a plain xpub (not spscan/spspend) is rejected.""" + seed = bytes(range(16)) + master = bip32.HDKey.from_seed(seed) + xpub = master.to_public().to_base58() + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, "sp(%s)" % xpub + ) + + def test_xpub_xpub_scan_key_rejected(self): + """Two-arg sp(xpub, xpub) is rejected — scan key must be private.""" + seed = bip39.mnemonic_to_seed(VECTORS[0]["mnemonic"]) + master = bip32.HDKey.from_seed(seed) + scan_xpub = master.derive("m/352h/1h/0h/1h/0").to_public().to_base58() + spend_xpub = master.derive("m/352h/1h/0h/0h/0").to_public().to_base58() + self.assertRaises( + DescriptorError, + SilentPaymentDescriptor.from_string, + "sp(%s,%s)" % (scan_xpub, spend_xpub), + ) + + def test_hex_pubkey_scan_key_rejected(self): + """Two-arg sp(pubkey_hex, pubkey_hex) is rejected — scan key must be private.""" + scan_pub = self._scan_priv().get_public_key() + spend_pub = self._spend_pub() + desc_str = "sp(%s,%s)" % (scan_pub.sec().hex(), spend_pub.sec().hex()) + self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + + def test_spscan_in_second_position_rejected(self): + """Two-arg sp(wif, spscan1...) is rejected — second arg cannot be an spscan key.""" + scan_priv = self._scan_priv() + spscan = SPScanKey(scan_priv, self._spend_pub()) + desc_str = "sp(%s,%s)" % (scan_priv.wif(), spscan.encode()) + self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + + def test_two_spscan_args_rejected(self): + """sp(spscan1..., spscan1...) is rejected — spscan must be the only argument.""" + spscan = SPScanKey(self._scan_priv(), self._spend_pub()) + desc_str = "sp(%s,%s)" % (spscan.encode(), spscan.encode()) + self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + + def test_uncompressed_scan_key_rejected(self): + """Uncompressed private key as scan arg is rejected.""" + scan_priv = ec.PrivateKey(bytes([0x01] * 32), compressed=False) + spend_pub = self._spend_pub() + desc_str = "sp(%s,%s)" % (scan_priv.wif(), spend_pub.sec().hex()) + self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + + def test_trailing_junk_rejected(self): + """Characters after the closing ) are rejected.""" + spscan = SPScanKey(self._scan_priv(), self._spend_pub()) + desc_str = "sp(%s)junk" % spscan.encode() + self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) From b1917711d1e75d2689b07d2c4f470119b4dda7c5 Mon Sep 17 00:00:00 2001 From: odudex Date: Fri, 29 May 2026 10:34:28 -0300 Subject: [PATCH 12/15] sp descriptors: use flexible bech32, robust parsing, clearer errors - Remove bech32_decode_long; the flexible bech32_decode (this branch) already handles long strings, so decode SP keys and silent payment addresses through it, converting Bech32DecodeError to the caller's error type. - Drop the now-dead 'is None' guards (convertbits raises instead of returning None) and wrap conversion to keep DescriptorError/ValueError consistent. - Seek to an absolute offset after 'sp(' so parsing is robust to a short read. - Raise a clear DescriptorError for unsupported address/derive/script_pubkey on SilentPaymentDescriptor, and cover the Descriptor.from_string('sp(...)') path. --- src/embit/bech32.py | 21 ----------------- src/embit/descriptor/descriptor.py | 4 +++- src/embit/descriptor/sp.py | 30 ++++++++++++++++++++----- src/embit/silent_payments/bip352.py | 34 ++++++---------------------- tests/tests/test_sp_descriptor.py | 35 ++++++++++++++++++++++++----- 5 files changed, 63 insertions(+), 61 deletions(-) diff --git a/src/embit/bech32.py b/src/embit/bech32.py index b230f180..b6766c52 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -112,27 +112,6 @@ def bech32_decode(bech): return (encoding, hrp, data[:-6]) -# TODO: remove this once flexible bech32 is in -def bech32_decode_long(bech): - """Like bech32_decode but without the 90-character length limit.""" - if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( - bech.lower() != bech and bech.upper() != bech - ): - return (None, None, None) - bech = bech.lower() - pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech): - return (None, None, None) - if not all(x in CHARSET for x in bech[pos + 1 :]): - return (None, None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos + 1 :]] - encoding = bech32_verify_checksum(hrp, data) - if encoding is None: - return (None, None, None) - return (encoding, hrp, data[:-6]) - - def convertbits(data, frombits, tobits, pad=True): """General power-of-2 base conversion.""" acc = 0 diff --git a/src/embit/descriptor/descriptor.py b/src/embit/descriptor/descriptor.py index c0737e74..fe8b576e 100644 --- a/src/embit/descriptor/descriptor.py +++ b/src/embit/descriptor/descriptor.py @@ -296,6 +296,7 @@ def from_string(cls, desc): @classmethod def read_from(cls, s): # starts with sh(wsh()), sh() or wsh() + start_pos = s.tell() start = s.read(8) sh = False wsh = False @@ -304,7 +305,8 @@ def read_from(cls, s): taproot = False taptree = TapTree() if start.startswith(b"sp("): - s.seek(-5, 1) + # position right after "sp(" (absolute, robust to a short read(8)) + s.seek(start_pos + 3) sp_desc = SilentPaymentDescriptor.read_from(s) end = s.read(1) if end != b")": diff --git a/src/embit/descriptor/sp.py b/src/embit/descriptor/sp.py index 94c6c823..3d3963ed 100644 --- a/src/embit/descriptor/sp.py +++ b/src/embit/descriptor/sp.py @@ -7,13 +7,15 @@ SPSCAN_HRPS = {"spscan": "main", "tspscan": "test"} SPSPEND_HRPS = {"spspend": "main", "tspspend": "test"} -SP_KEY_HRPS = {**SPSCAN_HRPS, **SPSPEND_HRPS} +SP_KEY_HRPS = SPSCAN_HRPS.copy() +SP_KEY_HRPS.update(SPSPEND_HRPS) def _bech32m_decode_sp_key(encoded): - encoding, hrp, data = bech32.bech32_decode_long(encoded) - if encoding is None: - raise DescriptorError("Invalid bech32m encoding in SP key: %s" % encoded) + try: + encoding, hrp, data = bech32.bech32_decode(encoded) + except bech32.Bech32DecodeError as e: + raise DescriptorError("Invalid bech32m encoding in SP key: %s" % e) if encoding != bech32.Encoding.BECH32M: raise DescriptorError("SP key must use bech32m encoding") if hrp not in SP_KEY_HRPS: @@ -23,8 +25,9 @@ def _bech32m_decode_sp_key(encoded): version = data[0] if version != 0: raise DescriptorError("Unsupported SP key version: %d" % version) - payload = bech32.convertbits(data[1:], 5, 8, False) - if payload is None: + try: + payload = bech32.convertbits(data[1:], 5, 8, False) + except bech32.Bech32DecodeError: raise DescriptorError("Invalid SP key payload encoding") return hrp, bytes(payload) @@ -268,6 +271,21 @@ def _read_args(cls, s): return cls(scan_key=scan_key, spend_key=spend_arg) + def derive(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors do not support derive(); see BIP-352 for output derivation" + ) + + def script_pubkey(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors have no fixed script_pubkey(); outputs are derived per BIP-352" + ) + + def address(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors have no address(); use BIP-352 silent payment address generation" + ) + def to_string(self): if self.sp_key is not None: return "sp(%s)" % self.sp_key diff --git a/src/embit/silent_payments/bip352.py b/src/embit/silent_payments/bip352.py index 635b1330..3688db8e 100644 --- a/src/embit/silent_payments/bip352.py +++ b/src/embit/silent_payments/bip352.py @@ -62,7 +62,6 @@ def generate_silent_payment_address( def decode_silent_payment_address(address: str): """ Decode a silent payment address and return the scan and spend public keys. - Silent payment addresses can be longer than 90 characters, so we need custom decoding. """ if address.startswith("sp1"): hrp = "sp" @@ -71,44 +70,25 @@ def decode_silent_payment_address(address: str): else: raise ValueError("Invalid silent payment address: unknown HRP") - # custom bech32 to bypass the 90-character limit - if (any(ord(x) < 33 or ord(x) > 126 for x in address)) or ( - address.lower() != address and address.upper() != address - ): - raise ValueError("Invalid silent payment address: invalid characters") - - address = address.lower() - pos = address.rfind("1") - if pos < 1 or pos + 7 > len(address): - raise ValueError("Invalid silent payment address: invalid format") - - if not all(x in bech32.CHARSET for x in address[pos + 1 :]): - raise ValueError( - "Invalid silent payment address: invalid characters in data part" - ) - - hrpgot = address[:pos] - data = [bech32.CHARSET.find(x) for x in address[pos + 1 :]] + try: + encoding, hrpgot, data = bech32.bech32_decode(address) + except bech32.Bech32DecodeError as e: + raise ValueError("Invalid silent payment address: {}".format(e)) if hrpgot != hrp: raise ValueError("Invalid silent payment address: HRP mismatch") - encoding = bech32.bech32_verify_checksum(hrpgot, data) - if encoding is None: - raise ValueError("Invalid silent payment address: checksum verification failed") - if encoding != bech32.Encoding.BECH32M: raise ValueError("Invalid silent payment address: must use bech32m encoding") - data = data[:-6] - if data[0] != 0: raise ValueError( "Invalid silent payment address: unsupported version {}".format(data[0]) ) - decoded = bech32.convertbits(data[1:], 5, 8, False) - if decoded is None: + try: + decoded = bech32.convertbits(data[1:], 5, 8, False) + except bech32.Bech32DecodeError: raise ValueError("Invalid silent payment address: conversion failed") try: diff --git a/tests/tests/test_sp_descriptor.py b/tests/tests/test_sp_descriptor.py index bd48f613..39a9c5f1 100644 --- a/tests/tests/test_sp_descriptor.py +++ b/tests/tests/test_sp_descriptor.py @@ -1,5 +1,5 @@ from unittest import TestCase -from embit.descriptor import SilentPaymentDescriptor +from embit.descriptor import Descriptor, SilentPaymentDescriptor from embit.descriptor.sp import SPScanKey, SPSpendKey from embit.descriptor.arguments import KeyOrigin from embit.descriptor.errors import DescriptorError @@ -89,6 +89,19 @@ def test_keys_extractable_from_descriptor(self): self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + def test_descriptor_from_string_dispatches_to_sp(self): + """Descriptor.from_string('sp(...)') returns a SilentPaymentDescriptor.""" + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = Descriptor.from_string(desc_str) + self.assertIsInstance(desc, SilentPaymentDescriptor) + self.assertEqual(str(desc), desc_str) + # Unsupported standard descriptor operations raise a clear error + self.assertRaises(DescriptorError, desc.address) + self.assertRaises(DescriptorError, desc.derive, 0) + self.assertRaises(DescriptorError, desc.script_pubkey) + class TestKeyOrigin(TestCase): """Key origin prefix [fingerprint/path] is parsed and preserved in SP descriptors.""" @@ -248,30 +261,40 @@ def test_hex_pubkey_scan_key_rejected(self): scan_pub = self._scan_priv().get_public_key() spend_pub = self._spend_pub() desc_str = "sp(%s,%s)" % (scan_pub.sec().hex(), spend_pub.sec().hex()) - self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) def test_spscan_in_second_position_rejected(self): """Two-arg sp(wif, spscan1...) is rejected — second arg cannot be an spscan key.""" scan_priv = self._scan_priv() spscan = SPScanKey(scan_priv, self._spend_pub()) desc_str = "sp(%s,%s)" % (scan_priv.wif(), spscan.encode()) - self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) def test_two_spscan_args_rejected(self): """sp(spscan1..., spscan1...) is rejected — spscan must be the only argument.""" spscan = SPScanKey(self._scan_priv(), self._spend_pub()) desc_str = "sp(%s,%s)" % (spscan.encode(), spscan.encode()) - self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) def test_uncompressed_scan_key_rejected(self): """Uncompressed private key as scan arg is rejected.""" scan_priv = ec.PrivateKey(bytes([0x01] * 32), compressed=False) spend_pub = self._spend_pub() desc_str = "sp(%s,%s)" % (scan_priv.wif(), spend_pub.sec().hex()) - self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) def test_trailing_junk_rejected(self): """Characters after the closing ) are rejected.""" spscan = SPScanKey(self._scan_priv(), self._spend_pub()) desc_str = "sp(%s)junk" % spscan.encode() - self.assertRaises(DescriptorError, SilentPaymentDescriptor.from_string, desc_str) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) From c8cb4e41a26eb29960bc15f61a419cf9d3138368 Mon Sep 17 00:00:00 2001 From: kdmukai <934746+kdmukai@users.noreply.github.com> Date: Wed, 13 May 2026 10:05:59 -0500 Subject: [PATCH 13/15] secp256k1: implement ec_pubkey_tweak_mul and ec_pubkey_combine python fallbacks --- src/embit/util/py_secp256k1.py | 56 +++---- tests/tests/test_bindings.py | 259 ++++++++++++++++++++++++++++++++- 2 files changed, 287 insertions(+), 28 deletions(-) diff --git a/src/embit/util/py_secp256k1.py b/src/embit/util/py_secp256k1.py index 0f1f8e1d..543bc226 100644 --- a/src/embit/util/py_secp256k1.py +++ b/src/embit/util/py_secp256k1.py @@ -242,33 +242,35 @@ def ec_pubkey_add(pub, tweak, context=None): return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") -# def ec_privkey_tweak_mul(secret, tweak, context=None): -# if len(secret)!=32 or len(tweak)!=32: -# raise ValueError("Secret and tweak should both be 32 bytes long") -# s = int.from_bytes(secret, 'big') -# t = int.from_bytes(tweak, 'big') -# if t > _key.SECP256K1_ORDER or s > _key.SECP256K1_ORDER: -# raise ValueError("Failed to tweak the secret") -# r = pow(s, t, _key.SECP256K1_ORDER) -# res = r.to_bytes(32, 'big') -# for i in range(len(secret)): -# secret[i] = res[i] - -# def ec_pubkey_tweak_mul(pub, tweak, context=None): -# if len(pub)!=64: -# raise ValueError("Public key should be 64 bytes long") -# if len(tweak)!=32: -# raise ValueError("Tweak should be 32 bytes long") -# if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: -# raise ValueError("Failed to tweak the public key") - -# def ec_pubkey_combine(*args, context=None): -# pub = bytes(64) -# pubkeys = (c_char_p * len(args))(*args) -# r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) -# if r == 0: -# raise ValueError("Failed to negate pubkey") -# return pub +def ec_pubkey_tweak_mul(pub, tweak, context=None): + if len(pub) != 64: + raise ValueError("Public key should be 64 bytes long") + if len(tweak) != 32: + raise ValueError("Tweak should be 32 bytes long") + t = int.from_bytes(tweak, "big") + if t == 0 or t >= _key.SECP256K1_ORDER: + raise ValueError("Failed to tweak the public key") + pubkey = _pubkey_parse(pub) + Q = _key.SECP256K1.affine(_key.SECP256K1.mul([(pubkey.p, t)])) + if Q is None: + raise ValueError("Failed to tweak the public key") + res = Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") + for i in range(len(pub)): + pub[i] = res[i] + + +def ec_pubkey_combine(*args, context=None): + if len(args) == 0: + raise ValueError("At least one pubkey is required") + acc = (0, 1, 0) + for pub in args: + if len(pub) != 64: + raise ValueError("Public key should be 64 bytes long") + acc = _key.SECP256K1.add(acc, _pubkey_parse(pub).p) + Q = _key.SECP256K1.affine(acc) + if Q is None: + raise ValueError("Failed to combine pubkeys") + return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") # schnorrsig diff --git a/tests/tests/test_bindings.py b/tests/tests/test_bindings.py index d0cbc519..4182e0fb 100644 --- a/tests/tests/test_bindings.py +++ b/tests/tests/test_bindings.py @@ -1,4 +1,4 @@ -from binascii import hexlify +from binascii import hexlify, unhexlify from unittest import TestCase, skipUnless from embit.util import py_secp256k1 @@ -113,6 +113,29 @@ def test_cross(self): ctypes_secp256k1.ec_pubkey_add(pub1, b"9" * 32), ) + # ec_pubkey_tweak_mul mutates in place. The ctypes binding's c_char_p + # argtype requires `bytes`; the py_secp256k1 implementation requires a + # mutable `bytearray`. Pass each the type it expects, then compare. + tweak = b"9" * 32 + pub_py = bytearray(pub1) + pub_c = bytes(pub1) # mutated in place via the CPython ctypes quirk + py_secp256k1.ec_pubkey_tweak_mul(pub_py, tweak) + ctypes_secp256k1.ec_pubkey_tweak_mul(pub_c, tweak) + self.assertEqual(bytes(pub_py), pub_c) + + # ec_pubkey_combine takes varargs and returns a new pubkey. + pub_other = py_secp256k1.ec_pubkey_create(b"7" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(pub1, pub_other), + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other), + ) + # Three-arg combine. + pub_third = py_secp256k1.ec_pubkey_create(b"3" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(pub1, pub_other, pub_third), + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other, pub_third), + ) + @skipUnless( _SCHNORR_AVAILABLE, "ctypes backend missing schnorr symbols; skipping parity checks", @@ -170,3 +193,237 @@ def test_recovery(self): pub = ctypes_secp256k1.ecdsa_recover(sig + bytes([i]), msg) pub2 = py_secp256k1.ecdsa_recover(sig + bytes([i]), msg) self.assertEqual(pub, pub2) + + +# Curve order N. Used to construct overflow/zero tweaks. +_SECP256K1_ORDER = ( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +) + + +def _tweak_mul_result_x(pubkey_compressed_hex, tweak_hex): + """Helper: parse a compressed pubkey, multiply by tweak, return x-coord hex.""" + pub = bytearray(py_secp256k1.ec_pubkey_parse(unhexlify(pubkey_compressed_hex))) + py_secp256k1.ec_pubkey_tweak_mul(pub, unhexlify(tweak_hex)) + return py_secp256k1.ec_pubkey_serialize(bytes(pub))[1:33].hex() + + +class PubkeyTweakMulTest(TestCase): + """Standalone tests for py_secp256k1.ec_pubkey_tweak_mul. + + Runs without libsecp256k1 — covers correctness via algebraic identities + and canonical BIP-47 spec vectors. + + Spec reference: + secp256k1_ec_pubkey_tweak_mul (libsecp256k1 public API) + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L830-L846 + + Tweak validity is defined by secp256k1_ec_seckey_verify (tweak must be a + 32-byte big-endian integer in the range [1, N-1] where N is the curve order): + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L721-L729 + """ + + def test_identity_tweak_one_unchanged(self): + """tweak_mul(P, 1) == P — multiplicative identity of the scalar group.""" + secret = b"2" * 32 + pub = py_secp256k1.ec_pubkey_create(secret) + ba = bytearray(pub) + py_secp256k1.ec_pubkey_tweak_mul(ba, (1).to_bytes(32, "big")) + self.assertEqual(bytes(ba), pub) + + def test_generator_times_k_equals_pubkey_create_k(self): + """tweak_mul(G, k) == ec_pubkey_create(k) for valid scalar k. + + ec_pubkey_create(k) is defined as k*G (G = curve generator); so + tweak_mul applied to G must yield the same point. + """ + for k in (1, 2, 3, 12345, 0xDEADBEEF, _SECP256K1_ORDER - 1): + with self.subTest(k=k): + G = py_secp256k1.ec_pubkey_create((1).to_bytes(32, "big")) + ba = bytearray(G) + py_secp256k1.ec_pubkey_tweak_mul(ba, k.to_bytes(32, "big")) + self.assertEqual( + bytes(ba), py_secp256k1.ec_pubkey_create(k.to_bytes(32, "big")) + ) + + def test_double_tweak_composes(self): + """tweak_mul(tweak_mul(P, a), b) == tweak_mul(P, a*b mod N). + + Associativity of scalar multiplication on the curve. + """ + secret = b"5" * 32 + P = py_secp256k1.ec_pubkey_create(secret) + a = int.from_bytes(b"\x03" * 32, "big") + b = int.from_bytes(b"\x07" * 32, "big") + + # Sequential a then b. + ba = bytearray(P) + py_secp256k1.ec_pubkey_tweak_mul(ba, a.to_bytes(32, "big")) + py_secp256k1.ec_pubkey_tweak_mul(ba, b.to_bytes(32, "big")) + + # Single combined scalar. + bc = bytearray(P) + ab = (a * b) % _SECP256K1_ORDER + py_secp256k1.ec_pubkey_tweak_mul(bc, ab.to_bytes(32, "big")) + + self.assertEqual(bytes(ba), bytes(bc)) + + def test_rejects_zero_tweak(self): + """tweak == 0 must fail: 0*P = point-at-infinity (not a valid pubkey). + + Matches libsecp256k1 behavior: tweak is rejected by seckey_verify, so + tweak_mul returns 0 (we raise ValueError). See upstream test at + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6325 + """ + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + ba = bytearray(pub) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(ba, b"\x00" * 32) + + def test_rejects_overflow_tweak(self): + """tweak >= curve order N must fail (no canonical representative). + + See upstream test at + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6357 + """ + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + for t in (_SECP256K1_ORDER, _SECP256K1_ORDER + 1, 2**256 - 1): + with self.subTest(t=t): + ba = bytearray(pub) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(ba, t.to_bytes(32, "big")) + + def test_rejects_wrong_pubkey_length(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(bytearray(63), b"\x01" * 32) + + def test_rejects_wrong_tweak_length(self): + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(bytearray(pub), b"\x01" * 31) + + def test_bip47_spec_vectors(self): + """Canonical BIP-47 ECDH vectors: x-coord of (Alice's a0 * Bob's Bi). + + BIP-47 (Reusable Payment Codes) defines the shared secret as a*B where + a is the payer's private key and B is the recipient's derived pubkey. + These vectors are the X coordinate of that curve point — exactly what + ec_pubkey_tweak_mul computes. + + Spec: https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki + Test vectors: https://gist.github.com/SamouraiDev/6aad669604c5930864bd + """ + a0 = unhexlify( + "8d6a8ecd8ee5e0042ad0cb56e3a971c760b5145c3917a8e7beaf0ed92d7a520c" + ) + vectors = [ + ( + "024ce8e3b04ea205ff49f529950616c3db615b1e37753858cc60c1ce64d17e2ad8", + "f5bb84706ee366052471e6139e6a9a969d586e5fe6471a9b96c3d8caefe86fef", + ), + ( + "03e092e58581cf950ff9c8fc64395471733e13f97dedac0044ebd7d60ccc1eea4d", + "adfb9b18ee1c4460852806a8780802096d67a8c1766222598dc801076beb0b4d", + ), + ( + "029b5f290ef2f98a0462ec691f5cc3ae939325f7577fcaf06cfc3b8fc249402156", + "79e860c3eb885723bb5a1d54e5cecb7df5dc33b1d56802906762622fa3c18ee5", + ), + ( + "02094be7e0eef614056dd7c8958ffa7c6628c1dab6706f2f9f45b5cbd14811de44", + "d8339a01189872988ed4bd5954518485edebf52762bf698b75800ac38e32816d", + ), + ( + "031054b95b9bc5d2a62a79a58ecfe3af000595963ddc419c26dab75ee62e613842", + "14c687bc1a01eb31e867e529fee73dd7540c51b9ff98f763adf1fc2f43f98e83", + ), + ( + "03dac6d8f74cacc7630106a1cfd68026c095d3d572f3ea088d9a078958f8593572", + "725a8e3e4f74a50ee901af6444fb035cb8841e0f022da2201b65bc138c6066a2", + ), + ( + "02396351f38e5e46d9a270ad8ee221f250eb35a575e98805e94d11f45d763c4651", + "521bf140ed6fb5f1493a5164aafbd36d8a9e67696e7feb306611634f53aa9d1f", + ), + ( + "039d46e873827767565141574aecde8fb3b0b4250db9668c73ac742f8b72bca0d0", + "5f5ecc738095a6fb1ea47acda4996f1206d3b30448f233ef6ed27baf77e81e46", + ), + ( + "038921acc0665fd4717eb87f81404b96f8cba66761c847ebea086703a6ae7b05bd", + "1e794128ac4c9837d7c3696bbc169a8ace40567dc262974206fcf581d56defb4", + ), + ( + "03d51a06c6b48f067ff144d5acdfbe046efa2e83515012cf4990a89341c1440289", + "fe36c27c62c99605d6cd7b63bf8d9fe85d753592b14744efca8be20a4d767c37", + ), + ] + for i, (B_hex, expected_x_hex) in enumerate(vectors): + with self.subTest(i=i): + self.assertEqual( + _tweak_mul_result_x(B_hex, a0.hex()), expected_x_hex + ) + + +class PubkeyCombineTest(TestCase): + """Standalone tests for py_secp256k1.ec_pubkey_combine. + + Spec reference: + secp256k1_ec_pubkey_combine (libsecp256k1 public API) + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L884-L898 + """ + + def test_combine_single_pubkey_unchanged(self): + """combine(P) == P — combining one pubkey is a no-op.""" + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + self.assertEqual(py_secp256k1.ec_pubkey_combine(pub), pub) + + def test_combine_p_plus_p_equals_tweak_mul_by_two(self): + """combine(P, P) == tweak_mul(P, 2) — point doubling. + + P + P on the curve equals 2*P. Mirrors the 'Tweak mul * 2 = 1+1' + identity in bitcoin-core/secp256k1's test suite: + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6383-L6386 + """ + P = py_secp256k1.ec_pubkey_create(b"9" * 32) + combined = py_secp256k1.ec_pubkey_combine(P, P) + doubled = bytearray(P) + py_secp256k1.ec_pubkey_tweak_mul(doubled, (2).to_bytes(32, "big")) + self.assertEqual(combined, bytes(doubled)) + + def test_combine_three_pubkeys_associates(self): + """combine(A, B, C) == combine(combine(A, B), C). + + Point addition on an elliptic curve is associative. + """ + A = py_secp256k1.ec_pubkey_create(b"1" * 32) + B = py_secp256k1.ec_pubkey_create(b"2" * 32) + C = py_secp256k1.ec_pubkey_create(b"3" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(A, B, C), + py_secp256k1.ec_pubkey_combine( + py_secp256k1.ec_pubkey_combine(A, B), C + ), + ) + + def test_combine_rejects_pubkey_and_negation(self): + """combine(P, -P) sums to the point at infinity — must fail. + + Per the libsecp256k1 API contract, 'the sum of the public keys is not + valid' returns 0 (we raise ValueError). The point at infinity is not a + valid pubkey representation. + """ + P = py_secp256k1.ec_pubkey_create(b"5" * 32) + neg_P = py_secp256k1.ec_pubkey_negate(P) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine(P, neg_P) + + def test_combine_rejects_no_args(self): + """API contract: n must be at least 1 (see header doc above).""" + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine() + + def test_combine_rejects_wrong_length(self): + pub = py_secp256k1.ec_pubkey_create(b"6" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine(pub, b"\x00" * 63) From 6f1be8329d9944e99a49c00ed88c9952ab2543a3 Mon Sep 17 00:00:00 2001 From: Mike Tolkachev Date: Fri, 15 May 2026 13:53:26 -0300 Subject: [PATCH 14/15] secp256k1: fix mutable key in tweak functions and add ec_privkey_tweak_mul --- src/embit/liquid/descriptor.py | 15 +++-- src/embit/liquid/pset.py | 2 +- src/embit/liquid/transaction.py | 2 +- src/embit/util/ctypes_secp256k1.py | 63 ++++++++++++++---- src/embit/util/py_secp256k1.py | 44 ++++++++++++- tests/tests/test_bindings.py | 101 ++++++++++++++++++++++++++++- tests/tests/test_liquid.py | 66 ++++++++++++++----- 7 files changed, 253 insertions(+), 40 deletions(-) diff --git a/src/embit/liquid/descriptor.py b/src/embit/liquid/descriptor.py index 23829315..cefeebe5 100644 --- a/src/embit/liquid/descriptor.py +++ b/src/embit/liquid/descriptor.py @@ -225,12 +225,15 @@ def sec(self): def musig_combine_privs(privs, sort=True): keys = [ - [b"" + prv, secp256k1.ec_pubkey_serialize(secp256k1.ec_pubkey_create(prv))] + [ + bytearray(prv), + secp256k1.ec_pubkey_serialize(secp256k1.ec_pubkey_create(prv)), + ] for prv in privs ] for karr in keys: if karr[1][0] == 0x03: - secp256k1.ec_privkey_negate(karr[0]) + karr[0] = bytearray(secp256k1.ec_privkey_negate(karr[0])) # x only karr[1] = karr[1][1:] if sort: @@ -238,7 +241,7 @@ def musig_combine_privs(privs, sort=True): secs = [k[1] for k in keys] ll = sha256(b"".join(secs)) coefs = [ - tagged_hash("MuSig coefficient", ll + i.to_bytes(4, "little")) + bytearray(tagged_hash("MuSig coefficient", ll + i.to_bytes(4, "little"))) for i in range(len(keys)) ] # tweak them all @@ -249,15 +252,15 @@ def musig_combine_privs(privs, sort=True): s = secp256k1.ec_privkey_add(s, c) pub = secp256k1.ec_pubkey_create(s) if secp256k1.ec_pubkey_serialize(pub)[0] == 0x03: - secp256k1.ec_privkey_negate(s) + s = secp256k1.ec_privkey_negate(s) return s def musig_combine_pubs(pubs, sort=True): - keys = [[pub, secp256k1.ec_pubkey_serialize(pub)] for pub in pubs] + keys = [[bytearray(pub), secp256k1.ec_pubkey_serialize(pub)] for pub in pubs] for karr in keys: if karr[1][0] == 0x03: - secp256k1.ec_pubkey_negate(karr[0]) + karr[0] = bytearray(secp256k1.ec_pubkey_negate(karr[0])) # x only karr[1] = karr[1][1:] if sort: diff --git a/src/embit/liquid/pset.py b/src/embit/liquid/pset.py index 290c59ac..dc97b2ba 100644 --- a/src/embit/liquid/pset.py +++ b/src/embit/liquid/pset.py @@ -370,7 +370,7 @@ def reblind(self, nonce, blinding_pubkey=None, extra_message=b""): blinding_pubkey = blinding_pubkey or self.blinding_pubkey if not blinding_pubkey: raise PSBTError("Blinding pubkey required") - pub = secp256k1.ec_pubkey_parse(blinding_pubkey) + pub = bytearray(secp256k1.ec_pubkey_parse(blinding_pubkey)) self.ecdh_pubkey = ec.PrivateKey(nonce).sec() secp256k1.ec_pubkey_tweak_mul(pub, nonce) sec = secp256k1.ec_pubkey_serialize(pub) diff --git a/src/embit/liquid/transaction.py b/src/embit/liquid/transaction.py index ca7af0a5..43c0b6ef 100644 --- a/src/embit/liquid/transaction.py +++ b/src/embit/liquid/transaction.py @@ -82,7 +82,7 @@ def unblind( raise TransactionError("Invalid value commitment length") if len(asset_commitment) != 33: raise TransactionError("Invalid asset commitment length") - pub = secp256k1.ec_pubkey_parse(pubkey) + pub = bytearray(secp256k1.ec_pubkey_parse(pubkey)) secp256k1.ec_pubkey_tweak_mul(pub, blinding_key) sec = secp256k1.ec_pubkey_serialize(pub) nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() diff --git a/src/embit/util/ctypes_secp256k1.py b/src/embit/util/ctypes_secp256k1.py index 5ce2b6c3..cddb3971 100644 --- a/src/embit/util/ctypes_secp256k1.py +++ b/src/embit/util/ctypes_secp256k1.py @@ -42,7 +42,19 @@ def _copy(a: bytes) -> bytes: """Ugly copy that works everywhere incl micropython""" if len(a) == 0: return b"" - return a[:1] + a[1:] + return bytes(a[:1] + a[1:]) + + +def _readable_buffer(a): + if isinstance(a, bytearray): + return bytes(a) + return a + + +def _writable_buffer(a): + if isinstance(a, bytearray): + return (c_char * len(a)).from_buffer(a) + return a def _platform_binary_ext(): @@ -562,7 +574,7 @@ def _init(flags=(CONTEXT_SIGN | CONTEXT_VERIFY)): def context_randomize(seed, context=_secp.ctx): if len(seed) != 32: raise ValueError("Seed should be 32 bytes long") - if _secp.secp256k1_context_randomize(context, seed) == 0: + if _secp.secp256k1_context_randomize(context, _readable_buffer(seed)) == 0: raise RuntimeError("Failed to randomize context") @@ -571,7 +583,7 @@ def ec_pubkey_create(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Private key should be 32 bytes long") pub = bytes(64) - r = _secp.secp256k1_ec_pubkey_create(context, pub, secret) + r = _secp.secp256k1_ec_pubkey_create(context, pub, _readable_buffer(secret)) if r == 0: raise ValueError("Invalid private key") return pub @@ -588,7 +600,9 @@ def ec_pubkey_parse(sec, context=_secp.ctx): if sec[0] != 0x04: raise ValueError("Uncompressed pubkey should start with 0x04") pub = bytes(64) - r = _secp.secp256k1_ec_pubkey_parse(context, pub, sec, len(sec)) + r = _secp.secp256k1_ec_pubkey_parse( + context, pub, _readable_buffer(sec), len(sec) + ) if r == 0: raise ValueError("Failed parsing public key") return pub @@ -602,7 +616,9 @@ def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=_secp.ctx): raise ValueError("Invalid flag") sec = bytes(33) if (flag == EC_COMPRESSED) else bytes(65) sz = c_size_t(len(sec)) - r = _secp.secp256k1_ec_pubkey_serialize(context, sec, byref(sz), pubkey, flag) + r = _secp.secp256k1_ec_pubkey_serialize( + context, sec, byref(sz), _readable_buffer(pubkey), flag + ) if r == 0: raise ValueError("Failed to serialize pubkey") return sec @@ -693,7 +709,7 @@ def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=_secp. def ec_seckey_verify(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") - return bool(_secp.secp256k1_ec_seckey_verify(context, secret)) + return bool(_secp.secp256k1_ec_seckey_verify(context, _readable_buffer(secret))) @locked @@ -720,7 +736,12 @@ def ec_pubkey_negate(pubkey, context=_secp.ctx): def ec_privkey_tweak_add(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") - if _secp.secp256k1_ec_privkey_tweak_add(context, secret, tweak) == 0: + if ( + _secp.secp256k1_ec_privkey_tweak_add( + context, _writable_buffer(secret), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the secret") return None @@ -731,7 +752,12 @@ def ec_pubkey_tweak_add(pub, tweak, context=_secp.ctx): raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") - if _secp.secp256k1_ec_pubkey_tweak_add(context, pub, tweak) == 0: + if ( + _secp.secp256k1_ec_pubkey_tweak_add( + context, _writable_buffer(pub), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the public key") return None @@ -755,7 +781,7 @@ def ec_pubkey_add(pub, tweak, context=_secp.ctx): if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") p = _copy(pub) - if _secp.secp256k1_ec_pubkey_tweak_add(context, p, tweak) == 0: + if _secp.secp256k1_ec_pubkey_tweak_add(context, p, _readable_buffer(tweak)) == 0: raise ValueError("Failed to tweak the public key") return p @@ -764,7 +790,12 @@ def ec_pubkey_add(pub, tweak, context=_secp.ctx): def ec_privkey_tweak_mul(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") - if _secp.secp256k1_ec_privkey_tweak_mul(context, secret, tweak) == 0: + if ( + _secp.secp256k1_ec_privkey_tweak_mul( + context, _writable_buffer(secret), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the secret") @@ -774,15 +805,21 @@ def ec_pubkey_tweak_mul(pub, tweak, context=_secp.ctx): raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") - if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: + if ( + _secp.secp256k1_ec_pubkey_tweak_mul( + context, _writable_buffer(pub), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the public key") @locked def ec_pubkey_combine(*args, context=_secp.ctx): pub = bytes(64) - pubkeys = (c_char_p * len(args))(*args) - r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) + readable_args = [_readable_buffer(arg) for arg in args] + pubkeys = (c_char_p * len(readable_args))(*readable_args) + r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(readable_args)) if r == 0: raise ValueError("Failed to combine pubkeys") return pub diff --git a/src/embit/util/py_secp256k1.py b/src/embit/util/py_secp256k1.py index 543bc226..ba52a98d 100644 --- a/src/embit/util/py_secp256k1.py +++ b/src/embit/util/py_secp256k1.py @@ -205,18 +205,52 @@ def ec_pubkey_negate(pubkey, context=None): return ec_pubkey_parse(bytes([0x05 - sec[0]]) + sec[1:]) -def ec_privkey_tweak_add(secret, tweak, context=None): +def _ensure_mutable_key(key): + if not isinstance(key, bytearray): + raise TypeError("Tweaked key must be a bytearray") + + +def ec_privkey_tweak_add( + secret: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Add tweak to secret in place.""" + _ensure_mutable_key(secret) res = ec_privkey_add(secret, tweak) for i in range(len(secret)): secret[i] = res[i] -def ec_pubkey_tweak_add(pub, tweak, context=None): +def ec_pubkey_tweak_add( + pub: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Add tweak to public key in place.""" + _ensure_mutable_key(pub) res = ec_pubkey_add(pub, tweak) for i in range(len(pub)): pub[i] = res[i] +def ec_privkey_tweak_mul( + secret: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Multiply secret by tweak in place.""" + _ensure_mutable_key(secret) + if len(secret) != 32 or len(tweak) != 32: + raise ValueError("Secret and tweak should both be 32 bytes long") + s = int.from_bytes(secret, "big") + t = int.from_bytes(tweak, "big") + if ( + s == 0 + or s >= _key.SECP256K1_ORDER + or t == 0 + or t >= _key.SECP256K1_ORDER + ): + raise ValueError("Failed to tweak the secret") + res = ((s * t) % _key.SECP256K1_ORDER).to_bytes(32, "big") + for i in range(len(secret)): + secret[i] = res[i] + + def ec_privkey_add(secret, tweak, context=None): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") @@ -242,7 +276,11 @@ def ec_pubkey_add(pub, tweak, context=None): return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") -def ec_pubkey_tweak_mul(pub, tweak, context=None): +def ec_pubkey_tweak_mul( + pub: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Multiply public key by tweak in place.""" + _ensure_mutable_key(pub) if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: diff --git a/tests/tests/test_bindings.py b/tests/tests/test_bindings.py index 4182e0fb..52227e2a 100644 --- a/tests/tests/test_bindings.py +++ b/tests/tests/test_bindings.py @@ -118,10 +118,22 @@ def test_cross(self): # mutable `bytearray`. Pass each the type it expects, then compare. tweak = b"9" * 32 pub_py = bytearray(pub1) - pub_c = bytes(pub1) # mutated in place via the CPython ctypes quirk + pub_c = ctypes_secp256k1._copy(pub1) # mutated via the CPython ctypes quirk py_secp256k1.ec_pubkey_tweak_mul(pub_py, tweak) ctypes_secp256k1.ec_pubkey_tweak_mul(pub_c, tweak) self.assertEqual(bytes(pub_py), pub_c) + pub_c_mutable = bytearray(pub1) + ctypes_secp256k1.ec_pubkey_tweak_mul(pub_c_mutable, bytearray(tweak)) + self.assertEqual(bytes(pub_py), bytes(pub_c_mutable)) + + secret_py = bytearray(secret) + secret_c = ctypes_secp256k1._copy(secret) + py_secp256k1.ec_privkey_tweak_mul(secret_py, tweak) + ctypes_secp256k1.ec_privkey_tweak_mul(secret_c, tweak) + self.assertEqual(bytes(secret_py), secret_c) + secret_c_mutable = bytearray(secret) + ctypes_secp256k1.ec_privkey_tweak_mul(secret_c_mutable, bytearray(tweak)) + self.assertEqual(bytes(secret_py), bytes(secret_c_mutable)) # ec_pubkey_combine takes varargs and returns a new pubkey. pub_other = py_secp256k1.ec_pubkey_create(b"7" * 32) @@ -129,6 +141,10 @@ def test_cross(self): py_secp256k1.ec_pubkey_combine(pub1, pub_other), ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other), ) + self.assertEqual( + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other), + ctypes_secp256k1.ec_pubkey_combine(bytearray(pub1), bytearray(pub_other)), + ) # Three-arg combine. pub_third = py_secp256k1.ec_pubkey_create(b"3" * 32) self.assertEqual( @@ -208,6 +224,89 @@ def _tweak_mul_result_x(pubkey_compressed_hex, tweak_hex): return py_secp256k1.ec_pubkey_serialize(bytes(pub))[1:33].hex() +class PrivkeyTweakMulTest(TestCase): + """Standalone tests for py_secp256k1.ec_privkey_tweak_mul.""" + + def test_identity_tweak_one_unchanged(self): + secret = bytearray(b"2" * 32) + expected = bytes(secret) + py_secp256k1.ec_privkey_tweak_mul(secret, (1).to_bytes(32, "big")) + self.assertEqual(bytes(secret), expected) + + def test_multiplies_scalar_mod_order(self): + for secret_int, tweak_int in ( + (1, 2), + (2, 3), + (12345, 0xDEADBEEF), + (_SECP256K1_ORDER - 1, _SECP256K1_ORDER - 1), + ): + with self.subTest(secret_int=secret_int, tweak_int=tweak_int): + secret = bytearray(secret_int.to_bytes(32, "big")) + tweak = tweak_int.to_bytes(32, "big") + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + expected = (secret_int * tweak_int) % _SECP256K1_ORDER + self.assertEqual(bytes(secret), expected.to_bytes(32, "big")) + + def test_public_key_matches_tweaked_secret(self): + secret = bytearray(b"5" * 32) + tweak = b"9" * 32 + pub = bytearray(py_secp256k1.ec_pubkey_create(secret)) + + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + py_secp256k1.ec_pubkey_tweak_mul(pub, tweak) + + self.assertEqual(bytes(pub), py_secp256k1.ec_pubkey_create(secret)) + + def test_rejects_zero_secret(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(32), b"\x01" * 32) + + def test_rejects_zero_tweak(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(b"4" * 32), b"\x00" * 32) + + def test_rejects_overflow_secret_or_tweak(self): + for secret_int, tweak_int in ( + (_SECP256K1_ORDER, 1), + (_SECP256K1_ORDER + 1, 1), + (1, _SECP256K1_ORDER), + (1, _SECP256K1_ORDER + 1), + (2**256 - 1, 1), + (1, 2**256 - 1), + ): + with self.subTest(secret_int=secret_int, tweak_int=tweak_int): + secret = bytearray(secret_int.to_bytes(32, "big")) + tweak = tweak_int.to_bytes(32, "big") + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + + def test_rejects_wrong_lengths(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(31), b"\x01" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(b"4" * 32), b"\x01" * 31) + + +class TweakMutationTypeTest(TestCase): + def test_rejects_immutable_private_key_for_add(self): + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_privkey_tweak_add(b"2" * 32, b"\x01" * 32) + + def test_rejects_immutable_public_key_for_add(self): + pub = py_secp256k1.ec_pubkey_create(b"2" * 32) + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_pubkey_tweak_add(pub, b"\x01" * 32) + + def test_rejects_immutable_private_key_for_mul(self): + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_privkey_tweak_mul(b"2" * 32, b"\x01" * 32) + + def test_rejects_immutable_public_key_for_mul(self): + pub = py_secp256k1.ec_pubkey_create(b"2" * 32) + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_pubkey_tweak_mul(pub, b"\x01" * 32) + + class PubkeyTweakMulTest(TestCase): """Standalone tests for py_secp256k1.ec_pubkey_tweak_mul. diff --git a/tests/tests/test_liquid.py b/tests/tests/test_liquid.py index cc8a290b..e9d9730a 100644 --- a/tests/tests/test_liquid.py +++ b/tests/tests/test_liquid.py @@ -12,27 +12,42 @@ from embit.ec import PrivateKey from embit.hashes import tagged_hash -_LIQUID_REQUIRED_FUNCS = [ - "ec_pubkey_tweak_mul", - "ec_privkey_tweak_mul", +_LIQUID_ZKP_REQUIRED_FUNCS = [ "generator_generate_blinded", + "generator_parse", + "generator_serialize", + "pedersen_blind_generator_blind_sum", "pedersen_commit", + "pedersen_commitment_parse", + "pedersen_commitment_serialize", "rangeproof_rewind", + "rangeproof_sign", "surjectionproof_generate", + "surjectionproof_initialize", + "surjectionproof_serialize", ] -_LIQUID_AVAILABLE = all(hasattr(secp256k1, func) for func in _LIQUID_REQUIRED_FUNCS) -if not _LIQUID_AVAILABLE: +_LIQUID_MUSIG_REQUIRED_FUNCS = [ + "ec_pubkey_tweak_mul", + "ec_privkey_tweak_mul", + "ec_pubkey_combine", +] +_LIQUID_ZKP_AVAILABLE = all( + hasattr(secp256k1, func) for func in _LIQUID_ZKP_REQUIRED_FUNCS +) +_LIQUID_MUSIG_AVAILABLE = all( + hasattr(secp256k1, func) for func in _LIQUID_MUSIG_REQUIRED_FUNCS +) +if not _LIQUID_ZKP_AVAILABLE: print( - "[tests] Liquid/ZKP backend unavailable; " - "skipping liquid tests and using pure-Python fallback." + "[tests] Liquid/ZKP backend unavailable; skipping ZKP-heavy liquid tests." ) -@skipUnless( - _LIQUID_AVAILABLE, - "Liquid/ZKP backend unavailable; pure-Python fallback in use", -) class LiquidTest(TestCase): + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_value_commitment(self): # scalars in little endian vbf = bytes( @@ -124,6 +139,10 @@ def test_slip77(self): ), ) + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_rangeproof(self): tx = "02000000010161ec81269a870c234cf5bc940e6a501a59170634dc40dec64a2113bc0f18b5510100000000fdffffff030bbfaa67f3a1d4d17804c5f1661ac538f179e34030ca3314f2c932828646493ed5081ca5ecd48661c55e40da8e680810671ceb4e0052046289db8a61c6921bd0d818035165006aab5ad15093372f2268a0f17a174a225f77cb8c79cb06066499f5ec0c17a914c5cf7922d5c4882f3718de54977f086199e3ca33870a3f985d1e2c7302f4ed33b265eb4e50361c1a36f42994fae73f19b399df81e9de09369e66ab256f85390aa0d7826a27b28c63002046a4941863d41c88ba73db003b021b86ddebdf2783d477db0f829fdc327bc2a9882e7697c4a3e28b9b151e53e13717a9143171392675316d931905b590cc08d44653e71610870118696cf23e3209a6e84479d01f13fe79c901356ae53907cc882cffd3e26401230100000000000000f900006e00000000000247304402207c660c76a672313c9b0e85f725558c6a9376a51da1978d5c52b1e28f514a2e1e022029d20e7975499a6c70edddf76ab805f795a69e24239f5d784f5e189fb4e04dee012102f88d70eb972f0034548fab6cc583dfb35330766d92fb9793e998e4ea19e725af0043010001d24bc22e3ba61c698f9127a79e6642f94bdf11c51ffcb0a15fc379a77d69e4a27eee88c76e70879b4c723c8bcc47255a0e2241aaca0d4d620cb1972200f87011fd4e106033000000000000000160aa54000e33153b17f39866cbbd8f443a587c3bf8873523f8f85133b7e1dae762dc34417c89dc2067da545a49972b8deb67cddce8160b031f3e44c3bac86d02fe73399ca7843336d4c9a4c38c15c644320ebd5ca7d3526ced8886a9bded6c7834b5a727d8c1fba64738c5b5c4ccd3d745e52abed218f7439b9476afc38ca1fb49ec2691ce085b317a23338062fdbd7f93bd9261fad4aa25604423dc71f5e593d1bcc396fdb940cd159d7edaaafaa62ce1a01af097d26d2f0ccefd6ac59284676b8c2113e6bfc4420c362f7882d6af18c50f9aab55ecd113348f358c9fe00c0f79efc53eb21393c4064d55a3707a1bc942afec4480dbb11b0fa0e48fbaf5c17378263415588511097d299d88f76912a186e81eca3ae429e43c6a50ccc6ccaccf02cc01cac81048611fd5efb4b59c14f841ae973b03cf484dfc2d1922161ef52dc4bb65845b8edd16cfe9a9509249d06661906275495a351966180aecbd7f68503daa6379825fc6f40860e7e7487bd602a93d5c10267bc77af9230fa659fea8a75fdbbd4b3e4aec8ce852393a99ebf5695abb443959bdcec8a36ec0ce46226ff745a3c9650a2808841413b3eba91db4f66c978930d3413951d2e6a2e77561c95cb91755e7a115f5d6ea4bcf4d9672752782d8281b941a12c0d2bd205628771506bf88c445836f654e46cea98c550f98144617e493e85aa6aebade127a6b50a11c4760f74eb9ffe4226d3e36461006eaeb83d9a03e3d26adfde5e4c7e5ee7ab348d3a1dcbefafd58b56ed7fd30afebc524fc737d87fcd334c3ad9db437d8f310a0306538c1e5207565d705475ec470308cd7c6fa8e0ad59d12ba30f75bc3428eef91aec871b761afd91e31b92dcf1260a50371b7f020aaf1e767a4aaac8925e31842c57a8cb56e66da2dc3d88040a52859698f6e0dbb2c84e674c4de09eac9abf260163f2f9ff18631d477dccb009e1ede337e1165691e4f5fc4bf3d09a966834c76f9971941dcb242d28b2cd819d7584ece74d21550d9db13e539ac473622915611222109c8b87204d4ffbfadf16e063132990c295a42d40a2d01ad6332d5a34a028f1a7b2839f29ab6e19b24dc24a02d3a103f23e3031dc4c8a4886941e51feede21b60cfdd54e8db129408e5c575578b3b8b193d138dee87eb5406a44fe8b0b37e16cb3f260ff02e0f375ec6028429ca0cb60722ccfac66f87aee14e16e3ff54abe7eb3796c83846259200340080ef1f80dec2540ba4d77290bcad05794f8253681fcadbafbd701059b460e41bdbdaffc7f5993c45fc90b10ca1d3b63be3b3819ef235cf75da209c23d6562d228311293d7c4db6f9a3ec20989a02e66529a8e2d9316899b3bd2af2931670e2a6209a4ffaab5e8e05f9412f58c087e99161ad57057f86ac0775434b5a6a224c27ad3f3e6e6ea4190a23b50d47c37096612c29fb02a3108bb5c903a979c36d0e7c7341a0dffed887803a3a78bfe1b5cc2891dcd0e3f01c66ac975e45e1307320b63095583c4df9794fafb5d13ad22a039029c5cc686f2782fbd1cf83f7a916bd74abcc2e6e6b56b22b168ee3570c8b8b418303906a663ef8fc92dc5e665daab345d9c05887ad88c1ff7b39f128087ad74f2a8e52bbfc049bab0425e480f9af1b999374e32afb2ae809ca602bc50ae47afa517c5b14cfbb567600d808e1048ea6b4d68ae2dcc19c6eb2be08f2c1b8c8d428335dd9151caf77151c4b2886f20d4e2c568e6c1f6e70bc2ec97323a7cb9404ff113246b45b72a3a9ca6975ef981fbfec6f4e1bda1424cf8fb4d270f4c29fc2ddb6c38b662524876d81afe14a80e9e3d54fbaf54b8ab41e0efdb01db7615098f9ea1a68298bd9acf61cd5eb66b7a171921342c9b9ee32495ce480fdd0dd58dcc7970edde2b29787b5f4c82888855a425393227a069f32b6f9db0e2ab8ad084b048dac5ee2fd4acb53f5338a96da898c239e92f77543ac93c90462e72f7673fe2ddb379d121a47f07c0a5b2453606001125ab626991c3436e7d9ed78e479248bf54dc502d243267a587216ad364740a9cd87b181febaeafb613cdac74fb3a199f1d854a8965d6d93a508592c5dac86e8db5592e980bca3ec3e18897d254c376d78a2fcfff4a16cfc1116f5517a8cea53f400b597fbc264174e7467219a07c90a4a9dae471de17e14a088134001a3b486d6a4a10f95a75686e1c9bf4e35290e5b73e61064ca460e5674e0dd5092978f3c264f0036049f2979429fbf8e616d90343ec4dca6ed464dde7b2dec5637ebb9dd4550dd137d9b6b39cd7dbe64863d5b100da80c8226261a68b5f06413656afb165e5730f2b3677d35b0fddb3e1f6ec35b99a9900738aa4f7bd3a71916088415a17bdd5b752535642b9c5fdd5db3196d1dae707ecbb14c6d5266f6438eff62adeb101d43404a792ee5396f6e23c5611b3daf1176c0eed3c82d0d5c381d7f6069e1b5034cc666fd42ae87fe5a62e7c322efdf52d1c20fcdccdc425b23e41820eb74f567a6b2d2cb2acc73848df30f835502da8e149c86c729d0ec2f16bca6ab8e3226654055682a487f97c1d4cbc4af1ac81872ff2f4631ef5bf0c3b050151a8d99905e03f4da42458b9b4fd3700aa24bf884a50e3b79690bba922192a6ed272e18556f3c5822d9c357ac4de38ef695b9c4128bb3ebdaa721a84d1d0a5bf761e41020bc36605be79c472d247c8cdb3c3c97aed81e637030bc8173d85467201326836a52fc4dc5f7be5c57eb1f30c455b597e713b54f09e42ad91c5543f942f4d21471b8493bbfcf7ca6812757b9d4a92578f0848149b8f5db4f19f7743d3c3dcce24239a8f17332c5c82cc670b8880709af331317bcd6b3e9696f4b739dd0bb133b699cf473594ba4eb20d746df7f5e1a815083eaef02c829f08993a580fc9ef5fe261b638579fd92c24311753c7d15d9294d9503b83e3ce25300d1ed41417c74347db0cdbc0b6320c320b8084ca6f124b5ca83f51b2ebe118fd17f4ef7ff777486fe138eb90147698d8abda55351cb7aada68ef5d0eaf32f2c506a4cd51eadbc34c876b4ded119fa5df951bae43d26ccaf90e934145f929735684bd2665a4e6d4917736df100688d6d225e2667d0555ab684c374799c5468ca28b7e7f653a33340bc78d1dca7fcc4cf365dfa87401ec87e8a9cf472f60e7bc3b6949b8adaf7a2ce5079ad8dff9fcd3f8284a016b7386e085894801e8603e77a394e6e93c899cfba1e89adcc177ba306ee82775eedb84b00ee92987a56b8386a007802383566133dde589c87f09a071f2c110090cdec84a92f133ed08d4b8073293c307f21461d52ad15043ad4535af4c9576c3288e83f1302bec91777087c9c2b37fdd87a78cb4083ed87fb24069e9aa459170deb30774369b3233d23ee4b42700b0869fadafdecf2b884fbbe7b870729625c4eed3a74fb5a0560871faf45327b4327cf03608561d5bb3a65007d594a08212ca992d0fb84258168a4b2c882442775e8ecb3b614d0a3aee0909a0a92d94856ee79f5e37ad53fce688597f218cbfcbb76febba01a411481b252ca3bc44e80ad0552c27be7f0bb8c9cbbad08ea1dfbf1201f9307be5d6ae7392c8a625de6f255af593929ef917d78132002d2260cfef45952a0181c8c5e47be8c9ee88e9ee176d6428e8de818d046f06cc3c359cdfffb1be8804580acc3e8748aeb4d0c2fecb50f7922259a2c52685b24ef63b04e9393e4e5af7c1c46e441fe36b3be8f9d3ee273ac43b296750199a3c0b4fd89380098598fd6b6202b4056ca9a6e6d03eb51451a4361f422a0b45c9875c064e9185a7e188ba13198dff9cb4605195cbea27e117e58e926176b8dd6863ebd4cbff7c7e472b7a38fa1727419130d1bfde56fe11557c5e3606e6991a1c242db3230aab1eaf7b463eef9cd6357b4060f663b3017c0ba4d0be2d22a709b76f167b5015a2769dbe9901d18abc0c2c285200b756057f9b2af9a7e83532f13f6c8be7fd40411bd4ab80fd0f11fe1538baa87e3b2083cab8fd660c0f1c90b4bcbba9ed5b784657b13f5b254ad8caf455bd4eb9260f580538fa69dde93e0a61e057d7903247af859e63dca5fbadedb1eb36f5722f8fb6e1a9cafaa204848b29826b89b556a450191ab5108273fb0e103a01bdfc673d7cd8160d90d2430a23945ebd319357ef09287bcdfce4f32cb5c043d690ad20c0d1177334a8792b2518c17874efb3f0afab5b7756dec945c0bf86d667c0a78f2aefebb285ed019ae6eace0a3be26459dbd6f2e9d1290f7befea161287676dad9430667789e69c090b2ad7a0dcf729d29bb6e350af5c03d6cec3231bd90e44da255baaaac80771d421386a1867ed584db55520178c490223976e3f8f5a6bee8f96c689c270f8f7ddb336c8e5fc49265a1e7642632f6a65ade6a4724a606a9a4955bbf8c9c95adf99228249ba8b76136e3cb2e82b33554f0a34ec48d1318ed85b3c13264abb13eddba21494a606787fcaf46d323204338fdce4323343f494431f58f2d6432c38a57dd83a8ab9f8cc46f2ce4bf8daa0a802eeb53990e6916b22b8719dd596316019f4e181f407626a4638990a3379470815361b2fa45564fb9aea96c00937f0d31286975c8c26e95adcf9e26df2524295d6b75b772ec95caa0535076ea85c1819b601de3f4e9c9e1962b470e147c8f22e0d1c6f7482f63fbc8039f4c9a879a7247e686decf9b36cb15a9afeb077cf20a398a814724ce5a5f53ded62a8baa322f2414bea00161b1023ec070aef656fbe21350876cff4e96e58128838b934d95cb9dcf741772efc455e89279005c09caabe1f3aa70ca4ab8ba92b45ba4d7829ecf311916cc540e7e96ef928930da15917f2e13d78fed992b052545876d638b3898c36795a61b78a2db7ce635a9e2621ef9cc6e9acb7b7985269ec8ff2cd76dc94f47106c3ee5132c84629c54be23f270b886744e452ce9b2ecd2c5fcdf1738cda54808d5201bc7985e519b49bcbd16577ca887a9943f600d4119a8be587a9ebaf91683d0f8ae711800d8605674e82b8594fdf988ae887f3cef270d6b1e2df21b7ab23562144a858f71361424dbf36a980d20e630215172aa17d5d16af93354020e7f6f1e74a42ed96edae4f03d420c9f1b389dd12cf0709b762333b684c7e76fc1d29d831da2bf3a2137bd7a5f7a74f94b2f54eef2dcbfc14276abfbbb2417d91299d39743d6de831860666ac704974b9cc2c3489c5111c07b92b8aba3853e0119dfb79e33a32dabb696d3acefa722e03a9d1429d1296c2bbe3e38847eb4d5a57a27c63bedb17fbdee19537a242d81c7ccbc0d6660e7a3270f27810f3c5035f687b8915e7cd7afe5b008bc97eae792bdca46fb08fccd5455846a0af1ce7ea838b948b3dc962a3dc389e244d3e25c95c0b88c637bdca7edd674507677a8fefd1fda1d1319a8bbd322783c918a447e369d1838af0b248f8f255145e285cdd84f2278b95bc8330599029c188a88f16a9fd7e0c0c0ee52110f6811c293e1ecf463cd092c985b12b55f5b9d128bfe7eac91302be9b48cf1221bef73fda345781372be992d390c1f86c7a8a14d4620cc7f3c0f7cfab196fa4b7796f9dfd819d757dd02fdf43c3bb9a938f16b99da8cb55f26f2d51ba8d24362527899a1d1d51d8ae0258d8d5b5c5221d2f008404425f5ac5c7787dff20ddf95e9ec0c446c955e1a19be50bed0d3f4f344b9f8813fe5d25bacbec36dd7be1b5795a8e125a8e1e26434ec51fc79c357925d1ef2a6a254c4ef829566f5a784718bd746b6c0c0cfb49d96edaa995bb07ad16e37a62535e8ba50fefd0c5e05e907fa77b72743c3479b1a1960b60ff197d0f31940e0eb62c386f86302a0d0559cd0381711ff421330d3010497259755fce9dd4b1d459fab672430100013f32023c28e566200d19bf60f3594dd68631e53ab701d81c7e352146d2463041a38e457d30ab5e6d81a423058bfa6963c228fa6c2249f20a625e7668d77c17e8fd4e10603300000000000000010b87a8005cd141bb0e23431c240f0d9ba6abfbb207095a386fe307e8b9a0e4ca5b640e5698b810ab3fdd54af982953691caab9475378b8742ce4ef1782e2e297da7470f4bcad95f0b9dd5b67cd42d514586a06fc5852c0d2f79e78651e567f8136ca40e5cb5d2f16ba3fd25623e455704cbdaa9a12e1cc236e6e075ff9d0d205f054359baca2c17065a5795af277c80c0d949fabe8d843a81a88006e6921d352eb3bf73d928fb9f68c46b303a8d46ffdd01ed987b64ba80f9897d659e2773c20599917b9877c0a453500252abe335d322083c5e00b092e2b981ed56380083b9d6e71b6460002edcc846e0f7bd4a8f2f81dc7e1f057c4259f9487ecadac14d665270d29cce513f838f0a8bd977d7698ac33e9fc5defea8832a1d93ec5cbcadd8754154c7ab6dd8d8c668a78ea73ba95994b9bc3896c065e61bf0326920535574140066d6c0d14cacf3833c54e3ca490cc071ed0f1f95c7daa99c0141b25dda44432392d8c307fb7f15792b688af9cc6994e2c7bf96c8737cdcfd3de7296260b43aff50896ea5069c254b9eae4fa00af02162307b826fc3f6a0df021303858d9f28f2f44debddbb9fdfcee7817c046063870fd66cc367efcaaf2ab22d364faecb912300d4eef688a20b41ffeb4ac37729604716c63123f812c3a9bf0f97e1b7caa044bc4d5b9e856d03af435e8ff8876437a17390d804d2c94b9ea9243deb63740c8587daba9022de2f55a2bb489140f2c626292f322d661e9be2d5f15b9a086030c19a89e54bd09d8943182241aaeafb0ddb0ec4828fb0de3e1713148aa715f9d597cdef52a3b6c1f850f037862dd86a15f15765b47aa9156c9bba604f51aedfe1d6e8a6d5f5ca9397e3850c5e27ab94334eef8641fc81568e75ee0af53cc3e9d60bc1b5a6e917681c85b38e85aeab517f1c8a4a536801298943210ed216bd221b2005a70a0f8fb2bfa38e88a169e28794ec6de4bd25bb56a8eaf45453f957d4beba02e7e6074da354b899c74fe815b6bd8e1aa6681b6824e97d79a016ee510e7dc594a7a1b3f2ecd77e774bd10266b087e31c54d5b12037997cd4e7b2decded0e206788911110d9a35db605a7b55b6e46a9b0490c95efc67c47699dc1e838e0df2cdbc6f3f22ec815ba2f1221bb7d65408479b41b0f140cf46d3594ab2f98261ba6608f5c48707809c0399b3b32d3a117ec82647398388fc2e1f734d2d07dc9d33f802597e0edac665dde52e906c2c7b7eafca54a57464ea4870e1251e862524c144c48d8bd5eaba3b22d9f4d55c81463a061672293049d1c09602e0141d97a2f0b844ceb32755062640b507603f7105382502a6342b87c52db21b9433d3574914baa7fe65a2db8debd24f955242a8fd966006d8bdb63f9db75b5f221a39ed5cdb616488974c13d7969aca573fa9c2a5ff13e3c1eb301561eaae3c8e15cea416aa9ddef3698be52bb5bda033af6988adf3e731ee62208af238cf3e2db2fbf50a9a86dd9eaa99faad62d27373e988c69c1b8d6676c4a7c4bafb168fda7c0c2803b2d4a03e8b7f04a0a7e2e82798052c373e34c873bbe98b538da9d515b43f12c3a3a33e590ec92221ccf2946dcacb42a95e7ab0821fc44af1d486573f73f045e0367f5bc82c841bb7ab132b6363931145395440aacafa7b93d5820a30862c3f37cefc2cf7f3657a03a7a35191529b95b80c55610c62aac5755815d3e290e5306ba4ff3baaac06ce90fffbcbe87ad41e48a4166aa8293bee0c77d5e994fc3cd12a4ad013b173949d753e1c2d96844dc1b37bbd3c0fc281d6e9b5ac66fb083acea67cc69ec553c074543f55851a334be29d793bcb73db56f72ae1893602037a0ffde4359a23dc26c469f3418523d8788fd25050add339311161dc6496735c3ed3a565504ec576bd64f4a51e42a60cbca11558533a8f67f5511fb4a16bad299851312bb3cd653572ad6b87e690df5dd6b9848db96df64dc4b9b4d30d824576aba02e252a8323ca083e73be4440b560fad4e835a9378cee2d96bde213a4552f54b9b7428c9661844a14a4d07717ffd379df959cf72fa04952ccf0b15c8ae5e736dd355e851d4199c316a77f800a9fa841a07cb64c0605ffc31484cf5f381538465d3d5e2f2edefb97045c17dd1d5641a18a27392550a476976cea2640c3c3f6ba9766b605934bb9b18169966c5104d1f6938235c6e78305a94ff182f80636ccb1195a020f89b703810d674b0d8e1bcb44dcfae32c89c35b57a407f2265aebb80c1543c04893ef903fa315d0be2a29611c772b276630e5355067657447a0a8c36c1363da0cb86a360b3345860e9ceee8b6ee1f846858d7ac281c4f3d77698b27a697875e3d4cb7352221785cc2ed959d8cd0317e20c45f92670d6ba8893206ac51da9ccd52e8405154d68fa3e025083adbfdfd917c2d186c0114480355bee202949a4b6ffb66e74d1b0b395e161aa591eb9eabf98194b0622276219cb4804263cbe1d942c862ca551a12ee008ddda041a913780cf5309a1520fce9b6d82f1aef28bff1ba04d2734ce07588fe26ef45e5e96e1c1036a6c671143416d1643786cb4231e291770586ebf32df774233edcc414fa6e3ceb12d14e4350d883432db129359501d8b73ff7a0b33de48c5227bc49ef75016c18539e986f7b1343979b71cb9a44f3bccf132b8d2276ab273f1626d9a0bf52ae8f8e73100b15517aa6ad21ae3c853ae9ae3fe0741aa803a3d9825b903a15b6972c512fa1466bb68ff5e38096a94b1c3fd5b91c8aeed69937ec4ef8f994ebc89e8bf5b56028d66bbedf2b22d780e9375f26ec707cfb0389d9e2787c6aabdf05473566c73e105dbd78940bdbf6c47d811009ffe4ff175778e4fbbbd757c57150103c58d4e3a7bd8f7212bea642d3668bcc6f67462b3f5ca46e5a91162748fb1efdf8d2250c9458c8857273736622b440cdf5c6ccaae15f49c71c18aee816da67e4a9eaf50b7980a252b96a9dfa94fb759756bf97e9f5e498688e5f38add25b2958bc38c98eaccce8f44e9a2a63aea30de18ba47109240f03e0620f26d16a6a47f2d4ffdc6d12529e3c7d0e7eec5bc0a24badf57d5746d45b5519869c5ca287945ab81086301c87c1f7fda5392597a3a6ac6e7a8606149d8fe6bc2a8aee2ea3f13abd52da3af3a8003b9842d9d64ddd183c03739c031d068eac37d03c92bc7977d6f35c4668320c8f2a523869f380cd1b47a8e2db7edc31e0f1e27a25ae378ffe780f776e4a7695bf8ca7a61d73ce143078bf7d1d84cd96872d06707ba3249ae257f54ff0ef40eb271cdde69392e559b4988ae30ea17e2805faea5f9b127be42e29e6c803dd561adda18f7b73f50a1b948131cdf8ce1a3434e2d8a7b0d1c6ecbdc1a1db24bd25df35cad84a25202815c5e6a688b8b37ec6a2ad2748a5d0ea2a5a0cad14a6d509dd08fdad62e7b10829103c7e4550910bc3b88669c7eeeb8f10dc0abf64b6c911ab00014909083c978939f745e162d027c01a489d6777fa95da337c24a9640e5aaeb0a9ac3458955d601c208bb68bfc71737e29c5f7181ceca530255d73d558099ed0445ac6e12ce8f89ed770eee9f88a2c991ef606c8ce2237dd9f4b9ca32512ec24b9cf2ca4d000f7d167acb949c6121dcc30c0bb976190191eeb98a753231e8268cfeb0ae862e6e0d6d918d6452e88b7be6192bf47d0e9b6a27093f00f3d1835bab2f98585202e30798aedafc233c39ddcefb981cef4fa5de9e558b42a38b64d8dbfb033f1d81bce7e15658dd7e93d15904c19b3cc98a8e32b825c8e63ac0a19e4456cf0ae6c8bdc45daaea5922d98eab2ad7c335cca067c73fb5c01fe788e8fb3ab24b9f1bad7f0c3251134b3b81c6374ce0df9538d551254d7e05fe35e19b020d504a53d839f0e4f6b1c31da8165e0120dec87edac833807a0c613968fc955d9355d45f47a8c2d5d33c98810890cadfc2f626691d11a1540e1b3222a9d9d93ecdbca0eda8adf9203de044570b55b87b25a1443a04640a79d65eda2ff58a30ee4a90e5725f26b23a7c375c3c113d6f0e5cf93c5e1005e184b078a97ca838d79793d988c2f089ef5960ee234f7ce95c93c482c826df9b8a981dd246d5cbed48e9e0f8ce251edd536c978c2a34f28cf24ba67006ec6f026e314b3257cd367c656c7fec4011ef5f52561900641a323879a4705fb4756bccdb5fde8d750ecfb20f9f3e0555c767df65ff851ab0b77edbd28dcdbfce8bfe271ca79e3d845bc4fd713bef06e1835285c030c877becb78976ac521e71bf3816eb5f9c987f0290675f8f7332424dfba5dd962979dca0ca11bc734b8da81739ec13113ca8a06a8cac5823685f26e45acaef53f9d18f1c251daa084f95caf0f8c0dcac6f5a7c3ff166cc73f42e5045597d13927012cb0ada24a10338fef868a93dbd00366d7d02722f3ebaa6307ada7789852554af7cae4d9e8ab44c723ed248f3a577432ae7c943a943354a46bb4cb407e034b2fdbdc57aed0b6eb91bf438fed03cfe6d9416de10c8b8b009a5ae99f7fc9c715839127dbb800224d8f14480d1bdbebbc49aec17a0522dbf7abd077ec3bc6e2317194d91e01bb99d1d6dc690a5fbc01643f7fac1dd6275a967b13a3b1eb0aaf169ae4a1121430235defdaee37cd0a5456a7c46753790805360ad7c55078bf415f67629341603213cf4934bdad52081e1a3598619646ed2a80c0f1bdbf04348cb34c5622a01194eeeef0334872dc500566407a3628fd72648e143141946c379db7ac35a078a6cda18b67bfd66bc8c59293023a781e01ac9bf5af4dc830dedae04514a346041348a7a3be16a93b2de6d193a2d95024287677d4f6524e3d245d91884ce4ce837ee48036a28665cf1cecf047097b7e811b8b94b4eb98e8d40bc1a04103df9d68a2ee817fd32a01e30fcaa804c84d9893fcfe9843bfdbc78a2d2b0bc13d4311221ab69f5810ca5dbcdc16974e11a183cea17f45345f4290a7924f079fda182bf0889f6d2598a2a074e181f3d5afe44064716921024bc2100eb74fba49d93983a6adc3619ff524ec69fce6814b18e745d9a9ec748c605ab88402ed7e1b1ca3b16745bf6bcef16d41ca462f53ce4f61a4dc0a3ddd97542790de36fb71ca4f22a875c71b9d2bb734262d6b64dad68b3e74b3ccfc991a16e4c0d82000333adb601a92c505f3777bc24f481a5d9718f967c7da5be14e6afd56c53503b82803dfdc081e1818c96818560e3f1b2623ceec30732da696a1ceacf7560cb8366340085284a7635d350a3ff2e4030f670d359225529a9d99523e48d40dd33143dadbf6909ef72490599100c8d31b768352998df8e511c6b0f308376d8325faf65eafd9bf4c7a0473a7e90190b4730bc4e99e8369ef1bd085db8891dd456ba073b289420f231250c0ece8bf751f3d24b8df030ca37f77db88b9b60f3dd595e2ae08dbd167ca48044e08471cf2ae4785d34f737b269beeaaf665b438ef1648efc5bb983f61f60c8a08a2d65490fd8fd261deccbd4b1ec99e12bf84937242f566438d38a2185995048028eee40c4d1f7aba3f1554ad1986a5af53b725462fc71b412b79ef515ff796ef1a5f917a8bb5875173ccace1408814af623d32bb69241743587d870ca95c3cdd5f38c2efb93b5031ebf262c6638bd01803a3e3b7cae967384a79780fd7542aaf9575565635e0a9b623018afa5b14b49335177bbbe4b556c0b6fe544fc6edc6dda333979c8659325bca20c9f161b2a32dfdd081a9ecca17c816e1ab7f6c79d15b5d3c19a9357f3d5f2891af8f902df414eef6da24f8d3f1ee6c6f69e1af4a449a3c218af53b688f58b1dd7991cae5269ca0de00ab57adacc3cc9429ba7a065c8ae8d47c02746e1a070d44ff18dcd3f97ffc4f2466072c3f8c1a0ebe0b057090000" raw = unhexlify(tx) @@ -153,10 +172,6 @@ def test_descriptors(self): % multi, "blinded(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,%s)" % multi, - "blinded(musig(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" - % multi, - "blinded(musig(xpub6E7tbahhWSuFWASKYwctWYX2XhXZcvrFDMurFmV2FdyScS1CJz8VXkf3WchmYnBmC8uMVgENPLYd8uWjXYjxFFwFXD6unhFXs6VBjHTAb9e/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" - % multi, ] for d in descs: desc = LDescriptor.from_string(d) @@ -174,6 +189,27 @@ def test_descriptors(self): with self.assertRaises((ValueError, RuntimeError, DescriptorError)): LDescriptor.from_string(d) + @skipUnless( + _LIQUID_MUSIG_AVAILABLE, + "Liquid MuSig backend unavailable; pure-Python fallback missing MuSig helpers", + ) + def test_musig_descriptors(self): + multi = "wsh(sortedmulti(1,[12345678/44h/12]xpub6BwcvdstHTJtLpp1WxUiQCYERWSB66XY5JrCpw71GAJxcJ6s2AiUoEK4Nzt6UDaTmanUiSe6TY2RoFturKNLXeWBhwBF6WBNghr8cr7qnjk/<0;1>/*,[abcdef12/84h/22h]xpub6F6wWxm8F64iBHNhyaoh3QKCuuMUY5pfPPr1H1WuZXUXeXtZ21qjFN5ykaqnLL1jtPEFB9d94CyZrcYWKVdSiJKQ6mLGEB5sfrGFBpg6wgA/<0;1>/*))" + descs = [ + "blinded(musig(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" + % multi, + "blinded(musig(xpub6E7tbahhWSuFWASKYwctWYX2XhXZcvrFDMurFmV2FdyScS1CJz8VXkf3WchmYnBmC8uMVgENPLYd8uWjXYjxFFwFXD6unhFXs6VBjHTAb9e/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" + % multi, + ] + for d in descs: + desc = LDescriptor.from_string(d) + self.assertEqual(d, str(desc)) + self.assertIsNotNone(desc.derive(0).address()) + + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_blind_unblind(self): # mnemonic = "abandon "*11 + "about" mbkey = PrivateKey.from_string( From 585c52dc5f0fefda7b417fea5180ad1d13ce1fcc Mon Sep 17 00:00:00 2001 From: odudex Date: Fri, 29 May 2026 11:15:03 -0300 Subject: [PATCH 15/15] bip352: pass bytearray to in-place pubkey tweaks in create_outputs The new pure-Python secp256k1 tweak functions require a bytearray (they mutate in place via index assignment); ec_pubkey_parse returns bytes, which only worked with the ctypes backend. Wrap parsed points in bytearray so create_outputs works on both backends (fixes the pure-Python CI path). --- src/embit/silent_payments/bip352.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/embit/silent_payments/bip352.py b/src/embit/silent_payments/bip352.py index 3688db8e..66c2fc27 100644 --- a/src/embit/silent_payments/bip352.py +++ b/src/embit/silent_payments/bip352.py @@ -158,7 +158,9 @@ def create_outputs(input_privkeys, outpoints, recipients): scalar_bytes = scalar.to_bytes(32, "big") for B_scan, B_spend_list in groups.items(): - ecdh_point = ec_pubkey_parse(B_scan.sec()) + # bytearray: tweak_* mutate the point in place (required by the pure-Python + # secp256k1 backend, and works with the ctypes one too) + ecdh_point = bytearray(ec_pubkey_parse(B_scan.sec())) ec_pubkey_tweak_mul(ecdh_point, scalar_bytes) xonly_shared_secret = ec_pubkey_serialize(ecdh_point) @@ -170,7 +172,7 @@ def create_outputs(input_privkeys, outpoints, recipients): xonly_shared_secret + k.to_bytes(4, "big"), ) - P_k = ec_pubkey_parse(B_spend.sec()) + P_k = bytearray(ec_pubkey_parse(B_spend.sec())) ec_pubkey_tweak_add(P_k, t_k) xonly = ec_pubkey_serialize(P_k)[1:33]