From 1390794470af414225c5b1f7198a0858420e072a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 9 Jun 2025 18:21:40 +0000 Subject: [PATCH 1/5] Initial PayToAnchor support --- src/embit/script.py | 18 ++++++++++++++++++ tests/tests/test_script.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/embit/script.py b/src/embit/script.py index 5cea7f9..8ed1676 100644 --- a/src/embit/script.py +++ b/src/embit/script.py @@ -34,6 +34,11 @@ def address(self, network=NETWORKS["main"]): ver = ver % 0x50 return bech32.encode(network["bech32"], ver, data[2:]) + if script_type == "p2a": + # PayToAnchor (OP_1 <0x4e73>) hard-codes version 1 + ver = 1 + return bech32.encode(network["bech32"], ver, data[1:]) + # we should never get here raise ValueError("Unsupported script type") @@ -57,6 +62,9 @@ def script_type(self): # OP_1 if len(data) == 34 and data[:2] == b"\x51\x20": return "p2tr" + # OP_1 <0x4e73> (PayToAnchor is always hard-coded to this value) + if data == b"\x51\x4e\x73": + return "p2a" # unknown type return None @@ -151,6 +159,12 @@ def p2tr(pubkey, script_tree=None): return Script(b"\x51\x20" + output_pubkey.xonly()) +def p2a(): + """Return Pay-To-Anchor Script""" + # PayToAnchor is hard-coded by definition to: OP_1 <0x4e73> + return Script(b"\x51\x4e\x73") + + def p2pkh_from_p2wpkh(script): """Convert p2wpkh to p2pkh script""" return Script(b"\x76\xa9" + script.serialize()[2:] + b"\x88\xac") @@ -172,6 +186,10 @@ def multisig(m: int, pubkeys): def address_to_scriptpubkey(addr): + if addr == "bc1pfeessrawgf": + # PayToAnchor is always hard-coded to this value + return p2a() + # try with base58 address try: data = base58.decode_check(addr) diff --git a/tests/tests/test_script.py b/tests/tests/test_script.py index 01b7cf1..a8a60d0 100644 --- a/tests/tests/test_script.py +++ b/tests/tests/test_script.py @@ -1,5 +1,5 @@ from unittest import TestCase -from embit.script import Script, p2wpkh, p2sh, p2pkh, p2tr +from embit.script import Script, p2wpkh, p2sh, p2pkh, p2tr, p2a from embit.ec import PrivateKey from embit.hashes import hash160 @@ -12,6 +12,7 @@ def test_from_addr(self): p2pkh(pk), p2sh(p2wpkh(pk)), p2tr(pk), + p2a(), ] for sc in scripts: self.assertEqual(sc, Script.from_address(sc.address())) From 453bf65c45625b15ba2c06867d45a39d598bb167 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 9 Jun 2025 18:49:05 +0000 Subject: [PATCH 2/5] bugfix for p2a address variations by network --- src/embit/script.py | 7 +++---- tests/tests/test_script.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/embit/script.py b/src/embit/script.py index 8ed1676..0d0f101 100644 --- a/src/embit/script.py +++ b/src/embit/script.py @@ -186,10 +186,6 @@ def multisig(m: int, pubkeys): def address_to_scriptpubkey(addr): - if addr == "bc1pfeessrawgf": - # PayToAnchor is always hard-coded to this value - return p2a() - # try with base58 address try: data = base58.decode_check(addr) @@ -203,6 +199,9 @@ def address_to_scriptpubkey(addr): # fail - then it's bech32 address hrp = addr.split("1")[0] ver, data = bech32.decode(hrp, addr) + if ver == 1 and data == [int.from_bytes(b"\x4e"), int.from_bytes(b"\x73")]: + # PayToAnchor address (OP_1 <0x4e73>) + return p2a() if ver not in [0, 1] or len(data) not in [20, 32]: raise EmbitError("Invalid bech32 address") if ver == 1 and len(data) != 32: diff --git a/tests/tests/test_script.py b/tests/tests/test_script.py index a8a60d0..4e6183f 100644 --- a/tests/tests/test_script.py +++ b/tests/tests/test_script.py @@ -2,6 +2,7 @@ from embit.script import Script, p2wpkh, p2sh, p2pkh, p2tr, p2a from embit.ec import PrivateKey from embit.hashes import hash160 +from embit.networks import NETWORKS class ScriptTest(TestCase): @@ -15,7 +16,9 @@ def test_from_addr(self): p2a(), ] for sc in scripts: - self.assertEqual(sc, Script.from_address(sc.address())) + for network in NETWORKS.values(): + # Addresses will differ by network (e.g. bc1q vs bcrt1q) so test them all + self.assertEqual(sc, Script.from_address(sc.address(network))) def test_push(self): pk = PrivateKey(b"\x11" * 32) From cec92b065673f61051b7e437e5337c692ad02da5 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 9 Jun 2025 23:27:38 +0000 Subject: [PATCH 3/5] bugfix: specify the 2-byte data size --- src/embit/script.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/embit/script.py b/src/embit/script.py index 0d0f101..352a851 100644 --- a/src/embit/script.py +++ b/src/embit/script.py @@ -35,9 +35,9 @@ def address(self, network=NETWORKS["main"]): return bech32.encode(network["bech32"], ver, data[2:]) if script_type == "p2a": - # PayToAnchor (OP_1 <0x4e73>) hard-codes version 1 + # PayToAnchor (OP_1 <2:0x4e73>) hard-codes version 1 ver = 1 - return bech32.encode(network["bech32"], ver, data[1:]) + return bech32.encode(network["bech32"], ver, data[2:]) # we should never get here raise ValueError("Unsupported script type") @@ -62,8 +62,8 @@ def script_type(self): # OP_1 if len(data) == 34 and data[:2] == b"\x51\x20": return "p2tr" - # OP_1 <0x4e73> (PayToAnchor is always hard-coded to this value) - if data == b"\x51\x4e\x73": + # OP_1 <2:0x4e73> (PayToAnchor is always hard-coded to this value) + if data == b"\x51\x02\x4e\x73": return "p2a" # unknown type return None @@ -161,8 +161,8 @@ def p2tr(pubkey, script_tree=None): def p2a(): """Return Pay-To-Anchor Script""" - # PayToAnchor is hard-coded by definition to: OP_1 <0x4e73> - return Script(b"\x51\x4e\x73") + # PayToAnchor is hard-coded by definition to: OP_1 <2:0x4e73> + return Script(b"\x51\x02\x4e\x73") def p2pkh_from_p2wpkh(script): From 681c13033effb62f497d934725c94f25ffd70e03 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 10 Jun 2025 07:35:36 -0500 Subject: [PATCH 4/5] Add p2a to `is_taproot` logic --- src/embit/psbt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embit/psbt.py b/src/embit/psbt.py index 54497c2..713939a 100644 --- a/src/embit/psbt.py +++ b/src/embit/psbt.py @@ -219,7 +219,7 @@ def is_verified(self): @property def is_taproot(self): - return self.utxo.script_pubkey.script_type() == "p2tr" + return self.utxo.script_pubkey.script_type() in ["p2tr", "p2a"] def verify(self, ignore_missing=False): """Verifies the hash of previous transaction provided in non_witness_utxo. From 7c4095a921642b4419b511a4b63b07bdc631e73e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 11 Jun 2025 10:35:40 -0500 Subject: [PATCH 5/5] Revert adding p2a to `is_taproot` prop --- src/embit/psbt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embit/psbt.py b/src/embit/psbt.py index 713939a..54497c2 100644 --- a/src/embit/psbt.py +++ b/src/embit/psbt.py @@ -219,7 +219,7 @@ def is_verified(self): @property def is_taproot(self): - return self.utxo.script_pubkey.script_type() in ["p2tr", "p2a"] + return self.utxo.script_pubkey.script_type() == "p2tr" def verify(self, ignore_missing=False): """Verifies the hash of previous transaction provided in non_witness_utxo.