diff --git a/README.md b/README.md index d981ac5..cae1f9f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ $ python example/message.py --prikey 1 --msg pybtc --sig ICvzXjwjJVMilSGyMqwlqMT # True ``` +**example/p2mr.py** + +P2MR (Pay to Merkle Root) is a new type of output script proposed in BIP-360 (2026). This example demonstrates how to create a P2MR script. Since P2MR is not yet supported in Bitcoin Core, you cannot use this script to create a real P2MR output. However, you can still use it to understand how P2MR works and create P2MR scripts. + +```sh +$ python example/p2mr.py +``` + **example/satoshi_nakamoto.py** Brute-forcing the private key of Satoshi Nakamoto's address. @@ -77,7 +85,7 @@ $ python example/sss.py -m 2 -n 3 0x1:0xb703d4ef79f209dd9b3c1c7e9395785ab511ec95 **example/taproot.py** -This example demonstrates how to create a P2TR script with two script spending paths: p2pk and p2ms(2-of-2 multisig). +This example demonstrates how to create a P2TR script with two script spending paths: P2PK and P2MS(2-of-2 multisig). ```sh $ python example/taproot.py diff --git a/example/p2mr.py b/example/p2mr.py new file mode 100644 index 0000000..e96f4a4 --- /dev/null +++ b/example/p2mr.py @@ -0,0 +1,51 @@ +import pabtc + +# This example shows how to create a P2MR script with two unlock conditions: p2pk and p2ms. + + +# Here created two scripts, one of which is a p2pk script, which requires that it can only be unlocked by private key 2, +# and the other is an 2-of-2 multisig script. +mast = pabtc.core.TapBranch( + pabtc.core.TapLeaf(pabtc.core.TapScript.p2pk(pabtc.core.PriKey(2).pubkey())), + pabtc.core.TapLeaf(pabtc.core.TapScript.p2ms(2, [pabtc.core.PriKey(3).pubkey(), pabtc.core.PriKey(4).pubkey()])), +) +root = mast.hash + + +class Signerp2mrp2pk(pabtc.wallet.Signer): + def __init__(self) -> None: + self.script = pabtc.core.ScriptPubKey.p2mr(root) + # In p2tr, the least significant bit is the parity bit of the output public key's y-coordinate, which can be + # either 0 or 1. In p2mr, which has no internal key/key path cost, this bit is always 1. + self.prefix = 0xc0 + 1 + self.addr = pabtc.core.Address.p2mr(root) + + def sign(self, tx: pabtc.core.Transaction) -> None: + assert isinstance(mast.l, pabtc.core.TapLeaf) + for i, e in enumerate(tx.vin): + m = tx.digest_segwit_v1(i, pabtc.core.sighash_all, mast.l.script) + e.witness = [ + pabtc.core.PriKey(2).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]), + mast.l.script, + bytearray([self.prefix]) + mast.r.hash, + ] + + +class Signerp2mrp2ms(pabtc.wallet.Signer): + def __init__(self) -> None: + self.script = pabtc.core.ScriptPubKey.p2mr(root) + # In p2tr, the least significant bit is the parity bit of the output public key's y-coordinate, which can be + # either 0 or 1. In p2mr, which has no internal key/key path cost, this bit is always 1. + self.prefix = 0xc0 + 1 + self.addr = pabtc.core.Address.p2mr(root) + + def sign(self, tx: pabtc.core.Transaction) -> None: + assert isinstance(mast.r, pabtc.core.TapLeaf) + for i, e in enumerate(tx.vin): + m = tx.digest_segwit_v1(i, pabtc.core.sighash_all, mast.r.script) + e.witness = [ + pabtc.core.PriKey(4).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]), + pabtc.core.PriKey(3).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]), + mast.r.script, + bytearray([self.prefix]) + mast.l.hash, + ] diff --git a/pabtc/core.py b/pabtc/core.py index 0ba5a47..c1b132d 100644 --- a/pabtc/core.py +++ b/pabtc/core.py @@ -297,6 +297,14 @@ def p2tr(cls, root: bytearray) -> bytearray: data.extend(pabtc.opcode.op_pushdata(root)) return data + @classmethod + def p2mr(cls, root: bytearray) -> bytearray: + assert len(root) == 32 + data = bytearray() + data.append(pabtc.opcode.op_2) + data.extend(pabtc.opcode.op_pushdata(root)) + return data + @classmethod def address(cls, address: str) -> bytearray: if address.startswith(pabtc.config.current.prefix.bech32): @@ -309,6 +317,9 @@ def address(cls, address: str) -> bytearray: if address[len(pabtc.config.current.prefix.bech32) + 1] == 'p': data = pabtc.bech32.decode_segwit_addr(pabtc.config.current.prefix.bech32, 1, address) return cls.p2tr(data) + if address[len(pabtc.config.current.prefix.bech32) + 1] == 'z': + data = pabtc.bech32.decode_segwit_addr(pabtc.config.current.prefix.bech32, 2, address) + return cls.p2mr(data) data = pabtc.base58.decode(address) if data[0] == pabtc.config.current.prefix.base58.p2pkh: assert pabtc.core.hash256(data[0x00:0x15])[:4] == data[0x15:0x19] @@ -433,6 +444,11 @@ def p2tr(cls, root: bytearray) -> str: assert len(root) == 32 return pabtc.bech32.encode_segwit_addr(pabtc.config.current.prefix.bech32, 1, root) + @classmethod + def p2mr(cls, root: bytearray) -> str: + assert len(root) == 32 + return pabtc.bech32.encode_segwit_addr(pabtc.config.current.prefix.bech32, 2, root) + @classmethod def script_pubkey(cls, script_pubkey: bytearray) -> str: if len(script_pubkey) == 25 and all([ @@ -464,6 +480,11 @@ def script_pubkey(cls, script_pubkey: bytearray) -> str: script_pubkey[0x01] == pabtc.opcode.op_data_32, ]): return cls.p2tr(script_pubkey[0x02:0x22]) + if len(script_pubkey) == 34 and all([ + script_pubkey[0x00] == pabtc.opcode.op_2, + script_pubkey[0x01] == pabtc.opcode.op_data_32, + ]): + return cls.p2mr(script_pubkey[0x02:0x22]) raise Exception('unreachable') diff --git a/test/test_core.py b/test/test_core.py index 146d1d2..4519647 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -121,6 +121,36 @@ def test_address_p2tr(): assert addr == 'tb1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5ssk79hv2' +def test_address_p2mr(): + # From https://github.com/bitcoin/bips/blob/master/bip-0360/ref-impl/common/tests/data/p2mr_construction.json + pabtc.config.current = pabtc.config.mainnet + + # Case: p2mr_single_leaf_script_tree + l0 = pabtc.core.TapLeaf(bytearray.fromhex('20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac')) + root = l0.hash + assert pabtc.core.Address.p2mr(root) == 'bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k' + + # Case: p2mr_two_leaf_same_version + l0 = pabtc.core.TapLeaf(bytearray.fromhex('2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac')) + l1 = pabtc.core.TapLeaf(bytearray.fromhex('07546170726f6f74')) + root = pabtc.core.TapBranch(l0, l1).hash + assert pabtc.core.Address.p2mr(root) == 'bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y' + + # Case: p2mr_three_leaf_complex + l0 = pabtc.core.TapLeaf(bytearray.fromhex('2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac')) + l1 = pabtc.core.TapLeaf(bytearray.fromhex('202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac')) + l2 = pabtc.core.TapLeaf(bytearray.fromhex('207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac')) + root = pabtc.core.TapBranch(l0, pabtc.core.TapBranch(l1, l2)).hash + assert pabtc.core.Address.p2mr(root) == 'bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq' + + # Case: p2mr_three_leaf_alternative + l0 = pabtc.core.TapLeaf(bytearray.fromhex('2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac')) + l1 = pabtc.core.TapLeaf(bytearray.fromhex('20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac')) + l2 = pabtc.core.TapLeaf(bytearray.fromhex('20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac')) + root = pabtc.core.TapBranch(l0, pabtc.core.TapBranch(l1, l2)).hash + assert pabtc.core.Address.p2mr(root) == 'bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv' + + def test_address_script_pubkey(): pabtc.config.current = pabtc.config.mainnet for script_pubkey, addr in zip([bytearray.fromhex(e) for e in [ diff --git a/test/test_example.py b/test/test_example.py index 627880f..2fbf457 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -14,6 +14,10 @@ def test_message(): call('python example/message.py --prikey 1 --msg pybtc --sig ICvzXjwjJVMilSGyMqwlqMTuGF6UMwddFJzVmm0Di5qNnqkBRKP8Pldm3YbOskg3ewV1tszVLy8gVX1u+qFrx6o=') +def test_p2mr(): + call('python example/p2mr.py') + + def test_sss(): call('python example/sss.py -m 2 -n 3 0x0:0x0000000000000000000000000000000000000000000000000000000000000001') call('python example/sss.py -m 2 -n 3 0x2:0x5dee2bfbf85ebe932a0b305c621d9e6bbbb578a4c6d9eaa62a6a9cb9b923df92 0x3:0x0ce541f9f48e1ddcbf10c88a932c6da1999034f72a46dff93f9feb1715b5d143')