From ad4b24588485ea959155a6487488f980b5195ab0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 26 Aug 2024 08:47:38 -0500 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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 + )