Skip to content

Commit dc546f0

Browse files
Descriptors (#117)
* Add descriptors utils * Pre-commit fixes * Sourcery suggestion
1 parent 9670368 commit dc546f0

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed

HISTORY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ full year, short month, short day (YYYY-M-D)
99

1010
Major changes include:
1111

12-
- TBD
12+
- add descriptor util functions
1313

1414
## v2023.5.30
1515

btclib/descriptors.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (C) 2017-2022 The btclib developers
4+
#
5+
# This file is part of btclib. It is subject to the license terms in the
6+
# LICENSE file found in the top-level directory of this distribution.
7+
#
8+
# No part of btclib including this file, may be copied, modified, propagated,
9+
# or distributed except according to the terms contained in the LICENSE file.
10+
"""Descriptors util functions.
11+
12+
BIP 380: https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki
13+
"""
14+
15+
from btclib.exceptions import BTClibValueError
16+
17+
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
18+
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
19+
GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD]
20+
21+
22+
def __descsum_polymod(symbols):
23+
chk = 1
24+
for value in symbols:
25+
top = chk >> 35
26+
chk = (chk & 0x7FFFFFFFF) << 5 ^ value
27+
for i in range(5):
28+
chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
29+
return chk
30+
31+
32+
def __descsum_expand(descriptor_string: str):
33+
"""Perform the character to symbol expansion."""
34+
groups = []
35+
symbols = []
36+
for char in descriptor_string:
37+
if char not in INPUT_CHARSET:
38+
raise BTClibValueError()
39+
index = INPUT_CHARSET.find(char)
40+
symbols.append(index & 31)
41+
groups.append(index >> 5)
42+
if len(groups) == 3:
43+
symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
44+
groups = []
45+
if len(groups) == 1:
46+
symbols.append(groups[0])
47+
elif len(groups) == 2:
48+
symbols.append(groups[0] * 3 + groups[1])
49+
return symbols
50+
51+
52+
def descriptor_checksum(descriptor: str) -> str:
53+
"""Compute the descriptor checksum."""
54+
symbols = __descsum_expand(descriptor) + [0, 0, 0, 0, 0, 0, 0, 0]
55+
checksum = __descsum_polymod(symbols) ^ 1
56+
return "".join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))
57+
58+
59+
def descriptor_from_address(address: str) -> str:
60+
descriptor = f"addr({address})"
61+
return f"{descriptor}#{descriptor_checksum(descriptor)}"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
[
2+
{
3+
"desc": "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
4+
"checksum": "gn28ywm7"
5+
},
6+
{
7+
"desc": "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)",
8+
"checksum": "8fhd9pwu"
9+
},
10+
{
11+
"desc": "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)",
12+
"checksum": "8zl0zxma"
13+
},
14+
{
15+
"desc": "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))",
16+
"checksum": "qkrrc7je"
17+
},
18+
{
19+
"desc": "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
20+
"checksum": "lq9sf04s"
21+
},
22+
{
23+
"desc": "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))",
24+
"checksum": "2wtr0ej5"
25+
},
26+
{
27+
"desc": "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)",
28+
"checksum": "hzhjw406"
29+
},
30+
{
31+
"desc": "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))",
32+
"checksum": "y9zthqta"
33+
},
34+
{
35+
"desc": "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))",
36+
"checksum": "qwx6n9lh"
37+
},
38+
{
39+
"desc": "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))",
40+
"checksum": "en3tu306"
41+
},
42+
{
43+
"desc": "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))",
44+
"checksum": "ks05yr6p"
45+
},
46+
{
47+
"desc": "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)",
48+
"checksum": "axav5m0j"
49+
},
50+
{
51+
"desc": "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)",
52+
"checksum": "kczqajcv"
53+
},
54+
{
55+
"desc": "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)",
56+
"checksum": "ml40v0wf"
57+
},
58+
{
59+
"desc": "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))",
60+
"checksum": "t2zpj2eu"
61+
},
62+
{
63+
"desc": "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))",
64+
"checksum": "v66cvalc"
65+
},
66+
{
67+
"desc": "tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,{pk(fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),pk(e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)})",
68+
"checksum": "2rqrdjrh"
69+
},
70+
{
71+
"desc": "tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,sortedmulti_a(2,2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc))",
72+
"checksum": "tp09wjyq"
73+
}
74+
]

tests/test_descriptors.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (C) 2017-2022 The btclib developers
4+
#
5+
# This file is part of btclib. It is subject to the license terms in the
6+
# LICENSE file found in the top-level directory of this distribution.
7+
#
8+
# No part of btclib including this file, may be copied, modified, propagated,
9+
# or distributed except according to the terms contained in the LICENSE file.
10+
"""Tests for the `btclib.descriptors` module."""
11+
12+
import json
13+
from pathlib import Path
14+
15+
import pytest
16+
17+
from btclib.descriptors import (
18+
__descsum_expand,
19+
descriptor_checksum,
20+
descriptor_from_address,
21+
)
22+
from btclib.exceptions import BTClibValueError
23+
24+
25+
# descriptors taken from https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
26+
# checksum calculated using https://docs.rs/bdk/latest/bdk/descriptor/checksum/fn.get_checksum.html
27+
def test_checksum():
28+
filename = Path(__file__).parent / "_data" / "descriptor_checksums.json"
29+
with open(filename, encoding="utf-8") as file:
30+
data = json.load(file)
31+
32+
for descriptor_data in data:
33+
descriptor = descriptor_data["desc"]
34+
checksum = descriptor_data["checksum"]
35+
assert descriptor_checksum(descriptor) == checksum
36+
37+
38+
def test_invalid_charset():
39+
with pytest.raises(BTClibValueError):
40+
__descsum_expand("è")
41+
42+
43+
def test_addr():
44+
address = "bc1qnehtvnd4fedkwjq6axfgsrxgllwne3k58rhdh0"
45+
descriptor = "addr(bc1qnehtvnd4fedkwjq6axfgsrxgllwne3k58rhdh0)#s2y3vepm"
46+
assert descriptor_from_address(address) == descriptor

0 commit comments

Comments
 (0)