File: descriptor.py

package info (click to toggle)
python-ledger-bitcoin 0.4.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 720 kB
  • sloc: python: 9,357; makefile: 2
file content (384 lines) | stat: -rw-r--r-- 12,182 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
from io import BytesIO
from .. import script
from ..networks import NETWORKS
from .errors import DescriptorError
from .base import DescriptorBase
from .miniscript import Miniscript, Multi, Sortedmulti
from .arguments import Key
from .taptree import TapTree


class Descriptor(DescriptorBase):
    def __init__(
        self,
        miniscript=None,
        sh=False,
        wsh=True,
        key=None,
        wpkh=True,
        taproot=False,
        taptree=None,
    ):
        # TODO: add support for taproot scripts
        # Should:
        # - accept taptree without a key
        # - accept key without taptree
        # - raise if miniscript is not None, but taproot=True
        # - raise if taptree is not None, but taproot=False
        if key is None and miniscript is None and taptree is None:
            raise DescriptorError("Provide a key, miniscript or taptree")
        if miniscript is not None:
            # will raise if can't verify
            miniscript.verify()
            if miniscript.type != "B":
                raise DescriptorError("Top level miniscript should be 'B'")
            # check all branches have the same length
            branches = {
                len(k.branches) for k in miniscript.keys if k.branches is not None
            }
            if len(branches) > 1:
                raise DescriptorError("All branches should have the same length")
        self.sh = sh
        self.wsh = wsh
        self.key = key
        self.miniscript = miniscript
        self.wpkh = wpkh
        self.taproot = taproot
        self.taptree = taptree or TapTree()
        # make sure all keys are either taproot or not
        for k in self.keys:
            k.taproot = taproot

    @property
    def script_len(self):
        if self.taproot:
            return 34  # OP_1 <32:xonly>
        if self.miniscript:
            return len(self.miniscript)
        if self.wpkh:
            return 22  # 00 <20:pkh>
        return 25  # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG

    @property
    def num_branches(self):
        return max([k.num_branches for k in self.keys])

    def branch(self, branch_index=None):
        if self.miniscript:
            return type(self)(
                self.miniscript.branch(branch_index),
                self.sh,
                self.wsh,
                None,
                self.wpkh,
                self.taproot,
            )
        else:
            return type(self)(
                None,
                self.sh,
                self.wsh,
                self.key.branch(branch_index),
                self.wpkh,
                self.taproot,
                self.taptree.branch(branch_index),
            )

    @property
    def is_wildcard(self):
        return any([key.is_wildcard for key in self.keys])

    @property
    def is_wrapped(self):
        return self.sh and self.is_segwit

    @property
    def is_legacy(self):
        return not (self.is_segwit or self.is_taproot)

    @property
    def is_segwit(self):
        return (
            (self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot
        )

    @property
    def is_pkh(self):
        return self.key is not None and not self.taproot

    @property
    def is_taproot(self):
        return self.taproot

    @property
    def is_basic_multisig(self) -> bool:
        # TODO: should be true for taproot basic multisig with NUMS as internal key
        # Sortedmulti is subclass of Multi
        return bool(self.miniscript and isinstance(self.miniscript, Multi))

    @property
    def is_sorted(self) -> bool:
        return bool(self.is_basic_multisig and isinstance(self.miniscript, Sortedmulti))

    def scriptpubkey_type(self):
        if self.is_taproot:
            return "p2tr"
        if self.sh:
            return "p2sh"
        if self.is_pkh:
            if self.is_legacy:
                return "p2pkh"
            if self.is_segwit:
                return "p2wpkh"
        else:
            return "p2wsh"

    @property
    def brief_policy(self):
        if self.taptree:
            return "taptree"
        if self.key:
            return "single key"
        if self.is_basic_multisig:
            return (
                str(self.miniscript.args[0])
                + " of "
                + str(len(self.keys))
                + " multisig"
                + (" (sorted)" if self.is_sorted else "")
            )
        return "miniscript"

    @property
    def full_policy(self):
        if (self.key and not self.taptree) or self.is_basic_multisig:
            return self.brief_policy
        s = str(self.miniscript or self)
        for i, k in enumerate(self.keys):
            s = s.replace(str(k), chr(65 + i))
        return s

    def derive(self, idx, branch_index=None):
        if self.miniscript:
            return type(self)(
                self.miniscript.derive(idx, branch_index),
                self.sh,
                self.wsh,
                None,
                self.wpkh,
                self.taproot,
            )
        else:
            return type(self)(
                None,
                self.sh,
                self.wsh,
                self.key.derive(idx, branch_index),
                self.wpkh,
                self.taproot,
                self.taptree.derive(idx, branch_index),
            )

    def to_public(self):
        if self.miniscript:
            return type(self)(
                self.miniscript.to_public(),
                self.sh,
                self.wsh,
                None,
                self.wpkh,
                self.taproot,
            )
        else:
            return type(self)(
                None,
                self.sh,
                self.wsh,
                self.key.to_public(),
                self.wpkh,
                self.taproot,
                self.taptree.to_public(),
            )

    def owns(self, psbt_scope):
        """Checks if psbt input or output belongs to this descriptor"""
        # we can't check if we don't know script_pubkey
        if psbt_scope.script_pubkey is None:
            return False
        # quick check of script_pubkey type
        if psbt_scope.script_pubkey.script_type() != self.scriptpubkey_type():
            return False
        for pub, der in psbt_scope.bip32_derivations.items():
            # check of the fingerprints
            for k in self.keys:
                if not k.is_extended:
                    continue
                res = k.check_derivation(der)
                if res:
                    idx, branch_idx = res
                    sc = self.derive(idx, branch_index=branch_idx).script_pubkey()
                    # if derivation is found but scriptpubkey doesn't match - fail
                    return sc == psbt_scope.script_pubkey
        for pub, (leafs, der) in psbt_scope.taproot_bip32_derivations.items():
            # check of the fingerprints
            for k in self.keys:
                if not k.is_extended:
                    continue
                res = k.check_derivation(der)
                if res:
                    idx, branch_idx = res
                    sc = self.derive(idx, branch_index=branch_idx).script_pubkey()
                    # if derivation is found but scriptpubkey doesn't match - fail
                    return sc == psbt_scope.script_pubkey
        return False

    def check_derivation(self, derivation_path):
        for k in self.keys:
            # returns a tuple branch_idx, idx
            der = k.check_derivation(derivation_path)
            if der is not None:
                return der
        return None

    def witness_script(self):
        if self.wsh and self.miniscript is not None:
            return script.Script(self.miniscript.compile())

    def redeem_script(self):
        if not self.sh:
            return None
        if self.miniscript:
            if not self.wsh:
                return script.Script(self.miniscript.compile())
            else:
                return script.p2wsh(script.Script(self.miniscript.compile()))
        else:
            return script.p2wpkh(self.key)

    def script_pubkey(self):
        # covers sh-wpkh, sh and sh-wsh
        if self.taproot:
            return script.p2tr(self.key, self.taptree)
        if self.sh:
            return script.p2sh(self.redeem_script())
        if self.wsh:
            return script.p2wsh(self.witness_script())
        if self.miniscript:
            return script.Script(self.miniscript.compile())
        if self.wpkh:
            return script.p2wpkh(self.key)
        return script.p2pkh(self.key)

    def address(self, network=NETWORKS["main"]):
        return self.script_pubkey().address(network)

    @property
    def keys(self):
        if self.taptree and self.key:
            return [self.key] + self.taptree.keys
        elif self.taptree:
            return self.taptree.keys
        elif self.key:
            return [self.key]
        return self.miniscript.keys

    @classmethod
    def from_string(cls, desc):
        s = BytesIO(desc.encode())
        res = cls.read_from(s)
        left = s.read()
        if len(left) > 0 and not left.startswith(b"#"):
            raise DescriptorError("Unexpected characters after descriptor: %r" % left)
        return res

    @classmethod
    def read_from(cls, s):
        # starts with sh(wsh()), sh() or wsh()
        start = s.read(7)
        sh = False
        wsh = False
        wpkh = False
        is_miniscript = True
        taproot = False
        taptree = TapTree()
        if start.startswith(b"tr("):
            taproot = True
            s.seek(-4, 1)
        elif start.startswith(b"sh(wsh("):
            sh = True
            wsh = True
        elif start.startswith(b"wsh("):
            sh = False
            wsh = True
            s.seek(-3, 1)
        elif start.startswith(b"sh(wpkh"):
            is_miniscript = False
            sh = True
            wpkh = True
            assert s.read(1) == b"("
        elif start.startswith(b"wpkh("):
            is_miniscript = False
            wpkh = True
            s.seek(-2, 1)
        elif start.startswith(b"pkh("):
            is_miniscript = False
            s.seek(-3, 1)
        elif start.startswith(b"sh("):
            sh = True
            wsh = False
            s.seek(-4, 1)
        else:
            raise ValueError("Invalid descriptor (starts with '%s')" % start.decode())
        # taproot always has a key, and may have taptree miniscript
        if taproot:
            miniscript = None
            key = Key.read_from(s, taproot=True)
            nbrackets = 1
            c = s.read(1)
            # TODO: should it be ok to pass just taptree without a key?
            # check if we have taptree after the key
            if c != b",":
                s.seek(-1, 1)
            else:
                taptree = TapTree.read_from(s)
        elif is_miniscript:
            miniscript = Miniscript.read_from(s)
            key = None
            nbrackets = int(sh) + int(wsh)
        # single key for sure
        else:
            miniscript = None
            key = Key.read_from(s, taproot=taproot)
            nbrackets = 1 + int(sh)
        end = s.read(nbrackets)
        if end != b")" * nbrackets:
            raise ValueError(
                "Invalid descriptor (expected ')' but ends with '%s')" % end.decode()
            )
        return cls(
            miniscript,
            sh=sh,
            wsh=wsh,
            key=key,
            wpkh=wpkh,
            taproot=taproot,
            taptree=taptree,
        )

    def to_string(self):
        if self.taproot:
            if self.taptree:
                return "tr(%s,%s)" % (self.key, self.taptree)
            return "tr(%s)" % self.key
        if self.miniscript is not None:
            res = str(self.miniscript)
            if self.wsh:
                res = "wsh(%s)" % res
        else:
            if self.wpkh:
                res = "wpkh(%s)" % self.key
            else:
                res = "pkh(%s)" % self.key
        if self.sh:
            res = "sh(%s)" % res
        return res