Skip to content
Merged

P2mr #17

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions example/p2mr.py
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
mohanson marked this conversation as resolved.
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)

Comment thread
mohanson marked this conversation as resolved.
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,
]
21 changes: 21 additions & 0 deletions pabtc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Comment thread
mohanson marked this conversation as resolved.
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]
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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')


Expand Down
30 changes: 30 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
mohanson marked this conversation as resolved.


def test_address_script_pubkey():
pabtc.config.current = pabtc.config.mainnet
for script_pubkey, addr in zip([bytearray.fromhex(e) for e in [
Expand Down
4 changes: 4 additions & 0 deletions test/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading