diff --git a/src/embit/bip352.py b/src/embit/bip352.py new file mode 100644 index 0000000..0a33bef --- /dev/null +++ b/src/embit/bip352.py @@ -0,0 +1,45 @@ +""" +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, 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 new file mode 100644 index 0000000..f613a19 --- /dev/null +++ b/tests/tests/test_bip352.py @@ -0,0 +1,86 @@ +""" +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 + +import pytest +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" + } +] + + +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 """ + 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") + + + 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)