Skip to content

Commit 7aeefd6

Browse files
authored
Implement P2MR script and associated address handling
1 parent 8d6bbd7 commit 7aeefd6

5 files changed

Lines changed: 115 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ $ python example/message.py --prikey 1 --msg pybtc --sig ICvzXjwjJVMilSGyMqwlqMT
4646
# True
4747
```
4848

49+
**example/p2mr.py**
50+
51+
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.
52+
53+
```sh
54+
$ python example/p2mr.py
55+
```
56+
4957
**example/satoshi_nakamoto.py**
5058

5159
Brute-forcing the private key of Satoshi Nakamoto's address.
@@ -77,7 +85,7 @@ $ python example/sss.py -m 2 -n 3 0x1:0xb703d4ef79f209dd9b3c1c7e9395785ab511ec95
7785

7886
**example/taproot.py**
7987

80-
This example demonstrates how to create a P2TR script with two script spending paths: p2pk and p2ms(2-of-2 multisig).
88+
This example demonstrates how to create a P2TR script with two script spending paths: P2PK and P2MS(2-of-2 multisig).
8189

8290
```sh
8391
$ python example/taproot.py

example/p2mr.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pabtc
2+
3+
# This example shows how to create a P2MR script with two unlock conditions: p2pk and p2ms.
4+
5+
6+
# Here created two scripts, one of which is a p2pk script, which requires that it can only be unlocked by private key 2,
7+
# and the other is an 2-of-2 multisig script.
8+
mast = pabtc.core.TapBranch(
9+
pabtc.core.TapLeaf(pabtc.core.TapScript.p2pk(pabtc.core.PriKey(2).pubkey())),
10+
pabtc.core.TapLeaf(pabtc.core.TapScript.p2ms(2, [pabtc.core.PriKey(3).pubkey(), pabtc.core.PriKey(4).pubkey()])),
11+
)
12+
root = mast.hash
13+
14+
15+
class Signerp2mrp2pk(pabtc.wallet.Signer):
16+
def __init__(self) -> None:
17+
self.script = pabtc.core.ScriptPubKey.p2mr(root)
18+
# In p2tr, the least significant bit is the parity bit of the output public key's y-coordinate, which can be
19+
# either 0 or 1. In p2mr, which has no internal key/key path cost, this bit is always 1.
20+
self.prefix = 0xc0 + 1
21+
self.addr = pabtc.core.Address.p2mr(root)
22+
23+
def sign(self, tx: pabtc.core.Transaction) -> None:
24+
assert isinstance(mast.l, pabtc.core.TapLeaf)
25+
for i, e in enumerate(tx.vin):
26+
m = tx.digest_segwit_v1(i, pabtc.core.sighash_all, mast.l.script)
27+
e.witness = [
28+
pabtc.core.PriKey(2).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]),
29+
mast.l.script,
30+
bytearray([self.prefix]) + mast.r.hash,
31+
]
32+
33+
34+
class Signerp2mrp2ms(pabtc.wallet.Signer):
35+
def __init__(self) -> None:
36+
self.script = pabtc.core.ScriptPubKey.p2mr(root)
37+
# In p2tr, the least significant bit is the parity bit of the output public key's y-coordinate, which can be
38+
# either 0 or 1. In p2mr, which has no internal key/key path cost, this bit is always 1.
39+
self.prefix = 0xc0 + 1
40+
self.addr = pabtc.core.Address.p2mr(root)
41+
42+
def sign(self, tx: pabtc.core.Transaction) -> None:
43+
assert isinstance(mast.r, pabtc.core.TapLeaf)
44+
for i, e in enumerate(tx.vin):
45+
m = tx.digest_segwit_v1(i, pabtc.core.sighash_all, mast.r.script)
46+
e.witness = [
47+
pabtc.core.PriKey(4).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]),
48+
pabtc.core.PriKey(3).sign_schnorr(m) + bytearray([pabtc.core.sighash_all]),
49+
mast.r.script,
50+
bytearray([self.prefix]) + mast.l.hash,
51+
]

pabtc/core.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ def p2tr(cls, root: bytearray) -> bytearray:
297297
data.extend(pabtc.opcode.op_pushdata(root))
298298
return data
299299

300+
@classmethod
301+
def p2mr(cls, root: bytearray) -> bytearray:
302+
assert len(root) == 32
303+
data = bytearray()
304+
data.append(pabtc.opcode.op_2)
305+
data.extend(pabtc.opcode.op_pushdata(root))
306+
return data
307+
300308
@classmethod
301309
def address(cls, address: str) -> bytearray:
302310
if address.startswith(pabtc.config.current.prefix.bech32):
@@ -309,6 +317,9 @@ def address(cls, address: str) -> bytearray:
309317
if address[len(pabtc.config.current.prefix.bech32) + 1] == 'p':
310318
data = pabtc.bech32.decode_segwit_addr(pabtc.config.current.prefix.bech32, 1, address)
311319
return cls.p2tr(data)
320+
if address[len(pabtc.config.current.prefix.bech32) + 1] == 'z':
321+
data = pabtc.bech32.decode_segwit_addr(pabtc.config.current.prefix.bech32, 2, address)
322+
return cls.p2mr(data)
312323
data = pabtc.base58.decode(address)
313324
if data[0] == pabtc.config.current.prefix.base58.p2pkh:
314325
assert pabtc.core.hash256(data[0x00:0x15])[:4] == data[0x15:0x19]
@@ -433,6 +444,11 @@ def p2tr(cls, root: bytearray) -> str:
433444
assert len(root) == 32
434445
return pabtc.bech32.encode_segwit_addr(pabtc.config.current.prefix.bech32, 1, root)
435446

447+
@classmethod
448+
def p2mr(cls, root: bytearray) -> str:
449+
assert len(root) == 32
450+
return pabtc.bech32.encode_segwit_addr(pabtc.config.current.prefix.bech32, 2, root)
451+
436452
@classmethod
437453
def script_pubkey(cls, script_pubkey: bytearray) -> str:
438454
if len(script_pubkey) == 25 and all([
@@ -464,6 +480,11 @@ def script_pubkey(cls, script_pubkey: bytearray) -> str:
464480
script_pubkey[0x01] == pabtc.opcode.op_data_32,
465481
]):
466482
return cls.p2tr(script_pubkey[0x02:0x22])
483+
if len(script_pubkey) == 34 and all([
484+
script_pubkey[0x00] == pabtc.opcode.op_2,
485+
script_pubkey[0x01] == pabtc.opcode.op_data_32,
486+
]):
487+
return cls.p2mr(script_pubkey[0x02:0x22])
467488
raise Exception('unreachable')
468489

469490

test/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,36 @@ def test_address_p2tr():
121121
assert addr == 'tb1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5ssk79hv2'
122122

123123

124+
def test_address_p2mr():
125+
# From https://github.com/bitcoin/bips/blob/master/bip-0360/ref-impl/common/tests/data/p2mr_construction.json
126+
pabtc.config.current = pabtc.config.mainnet
127+
128+
# Case: p2mr_single_leaf_script_tree
129+
l0 = pabtc.core.TapLeaf(bytearray.fromhex('20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac'))
130+
root = l0.hash
131+
assert pabtc.core.Address.p2mr(root) == 'bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k'
132+
133+
# Case: p2mr_two_leaf_same_version
134+
l0 = pabtc.core.TapLeaf(bytearray.fromhex('2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac'))
135+
l1 = pabtc.core.TapLeaf(bytearray.fromhex('07546170726f6f74'))
136+
root = pabtc.core.TapBranch(l0, l1).hash
137+
assert pabtc.core.Address.p2mr(root) == 'bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y'
138+
139+
# Case: p2mr_three_leaf_complex
140+
l0 = pabtc.core.TapLeaf(bytearray.fromhex('2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac'))
141+
l1 = pabtc.core.TapLeaf(bytearray.fromhex('202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac'))
142+
l2 = pabtc.core.TapLeaf(bytearray.fromhex('207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac'))
143+
root = pabtc.core.TapBranch(l0, pabtc.core.TapBranch(l1, l2)).hash
144+
assert pabtc.core.Address.p2mr(root) == 'bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq'
145+
146+
# Case: p2mr_three_leaf_alternative
147+
l0 = pabtc.core.TapLeaf(bytearray.fromhex('2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac'))
148+
l1 = pabtc.core.TapLeaf(bytearray.fromhex('20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac'))
149+
l2 = pabtc.core.TapLeaf(bytearray.fromhex('20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac'))
150+
root = pabtc.core.TapBranch(l0, pabtc.core.TapBranch(l1, l2)).hash
151+
assert pabtc.core.Address.p2mr(root) == 'bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv'
152+
153+
124154
def test_address_script_pubkey():
125155
pabtc.config.current = pabtc.config.mainnet
126156
for script_pubkey, addr in zip([bytearray.fromhex(e) for e in [

test/test_example.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def test_message():
1414
call('python example/message.py --prikey 1 --msg pybtc --sig ICvzXjwjJVMilSGyMqwlqMTuGF6UMwddFJzVmm0Di5qNnqkBRKP8Pldm3YbOskg3ewV1tszVLy8gVX1u+qFrx6o=')
1515

1616

17+
def test_p2mr():
18+
call('python example/p2mr.py')
19+
20+
1721
def test_sss():
1822
call('python example/sss.py -m 2 -n 3 0x0:0x0000000000000000000000000000000000000000000000000000000000000001')
1923
call('python example/sss.py -m 2 -n 3 0x2:0x5dee2bfbf85ebe932a0b305c621d9e6bbbb578a4c6d9eaa62a6a9cb9b923df92 0x3:0x0ce541f9f48e1ddcbf10c88a932c6da1999034f72a46dff93f9feb1715b5d143')

0 commit comments

Comments
 (0)