├── bip380 ├── utils │ ├── __init__.py │ ├── hashes.py │ ├── bignum.py │ └── script.py ├── __init__.py ├── descriptors │ ├── errors.py │ ├── checksum.py │ ├── parsing.py │ ├── utils.py │ └── __init__.py ├── miniscript │ ├── __init__.py │ ├── errors.py │ ├── property.py │ ├── satisfaction.py │ ├── parsing.py │ └── fragments.py └── key.py ├── requirements.txt ├── tests ├── requirements.txt ├── data │ ├── invalid_samples.txt │ └── valid_samples.txt └── test_descriptors.py ├── .gitignore ├── setup.py ├── LICENSE └── README.md /bip380/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bip380/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.3" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bip32~=3.0 2 | coincurve~=18.0 3 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest~=8.0 2 | python-bitcointx==1.1.3 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | *venv* 3 | *tags* 4 | bip32.egg-info/ 5 | build/ 6 | dist/ 7 | -------------------------------------------------------------------------------- /bip380/descriptors/errors.py: -------------------------------------------------------------------------------- 1 | class DescriptorParsingError(ValueError): 2 | """Error while parsing a Bitcoin Output Descriptor from its string representation""" 3 | 4 | def __init__(self, message): 5 | self.message = message 6 | -------------------------------------------------------------------------------- /bip380/utils/hashes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common Bitcoin hashes. 3 | """ 4 | 5 | import hashlib 6 | 7 | 8 | def sha256(data): 9 | """{data} must be bytes, returns sha256(data)""" 10 | assert isinstance(data, bytes) 11 | return hashlib.sha256(data).digest() 12 | 13 | 14 | def hash160(data): 15 | """{data} must be bytes, returns ripemd160(sha256(data))""" 16 | assert isinstance(data, bytes) 17 | return hashlib.new("ripemd160", sha256(data)).digest() 18 | -------------------------------------------------------------------------------- /bip380/miniscript/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miniscript 3 | ========== 4 | 5 | Miniscript is an extension to Bitcoin Output Script descriptors. It is a language for \ 6 | writing (a subset of) Bitcoin Scripts in a structured way, enabling analysis, composition, \ 7 | generic signing and more. 8 | 9 | For more information about Miniscript, see https://bitcoin.sipa.be/miniscript. 10 | """ 11 | 12 | from .fragments import Node 13 | from .satisfaction import SatisfactionMaterial 14 | -------------------------------------------------------------------------------- /bip380/miniscript/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | All the exceptions raised when dealing with Miniscript. 3 | """ 4 | 5 | 6 | class MiniscriptMalformed(ValueError): 7 | def __init__(self, message): 8 | self.message = message 9 | 10 | 11 | class MiniscriptNodeCreationError(ValueError): 12 | def __init__(self, message): 13 | self.message = message 14 | 15 | 16 | class MiniscriptPropertyError(ValueError): 17 | def __init__(self, message): 18 | self.message = message 19 | 20 | # TODO: errors for type errors, parsing errors, etc.. 21 | -------------------------------------------------------------------------------- /tests/data/invalid_samples.txt: -------------------------------------------------------------------------------- 1 | l:older(0) 2 | l:older(2147483648) 3 | u:after(0) 4 | u:after(2147483648) 5 | andor(a:0,1,1) 6 | andor(0,a:1,a:1) 7 | andor(1,1,1) 8 | andor(or_i(0,after(1)),1,1) 9 | and_v(1,1) 10 | and_v(pk_k(02352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d5),1) 11 | and_v(v:1,a:1) 12 | and_b(1,1) 13 | and_b(v:1,a:1) 14 | and_b(a:1,a:1) 15 | and_b(pk_k(025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc),a:1) 16 | or_b(1,a:0) 17 | or_b(0,a:1) 18 | or_b(0,0) 19 | or_b(v:0,a:0) 20 | or_b(a:0,a:0) 21 | or_b(pk_k(025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc),a:0) 22 | t:or_c(a:0,v:1) 23 | t:or_c(1,v:1) 24 | t:or_c(or_i(0,after(1)),v:1) 25 | t:or_c(0,1) 26 | or_d(a:0,1) 27 | or_d(1,1) 28 | or_d(or_i(0,after(1)),1) 29 | or_d(0,v:1) 30 | or_i(a:1,a:1) 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | import bip380 3 | import io 4 | 5 | 6 | with io.open("README.md", encoding="utf-8") as f: 7 | long_description = f.read() 8 | 9 | with io.open("requirements.txt", encoding="utf-8") as f: 10 | requirements = [r for r in f.read().split('\n') if len(r)] 11 | 12 | setup(name="bip380", 13 | version=bip380.__version__, 14 | description="Bitcoin Output Script Descriptors (with Miniscript)", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="http://github.com/darosior/python-bip380", 18 | author="Antoine Poinsot", 19 | author_email="darosior@protonmail.com", 20 | license="MIT", 21 | packages=find_packages(), 22 | keywords=["bitcoin", "miniscript", "script", "descriptor"], 23 | install_requires=requirements) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Antoine Poinsot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bip380/utils/bignum.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2020 The Bitcoin Core developers 2 | # Copyright (c) 2021 Antoine Poinsot 3 | # Distributed under the MIT software license, see the accompanying 4 | # file LICENSE or http://www.opensource.org/licenses/mit-license.php. 5 | """Big number routines. 6 | 7 | This file is taken from the Bitcoin Core test framework. It was previously 8 | copied from python-bitcoinlib. 9 | """ 10 | 11 | import struct 12 | 13 | 14 | # generic big endian MPI format 15 | 16 | 17 | def bn_bytes(v, have_ext=False): 18 | ext = 0 19 | if have_ext: 20 | ext = 1 21 | return ((v.bit_length() + 7) // 8) + ext 22 | 23 | 24 | def bn2bin(v): 25 | s = bytearray() 26 | i = bn_bytes(v) 27 | while i > 0: 28 | s.append((v >> ((i - 1) * 8)) & 0xFF) 29 | i -= 1 30 | return s 31 | 32 | 33 | def bn2mpi(v): 34 | have_ext = False 35 | if v.bit_length() > 0: 36 | have_ext = (v.bit_length() & 0x07) == 0 37 | 38 | neg = False 39 | if v < 0: 40 | neg = True 41 | v = -v 42 | 43 | s = struct.pack(b">I", bn_bytes(v, have_ext)) 44 | ext = bytearray() 45 | if have_ext: 46 | ext.append(0) 47 | v_bin = bn2bin(v) 48 | if neg: 49 | if have_ext: 50 | ext[0] |= 0x80 51 | else: 52 | v_bin[0] |= 0x80 53 | return s + ext + v_bin 54 | 55 | 56 | # bitcoin-specific little endian format, with implicit size 57 | def mpi2vch(s): 58 | r = s[4:] # strip size 59 | r = r[::-1] # reverse string, converting BE->LE 60 | return r 61 | 62 | 63 | def bn2vch(v): 64 | return bytes(mpi2vch(bn2mpi(v))) 65 | -------------------------------------------------------------------------------- /bip380/descriptors/checksum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2019 Pieter Wuille 3 | # Distributed under the MIT software license, see the accompanying 4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 | """Utility functions related to output descriptors""" 6 | 7 | import re 8 | 9 | INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " 10 | CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 11 | GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD] 12 | 13 | 14 | def descsum_polymod(symbols): 15 | """Internal function that computes the descriptor checksum.""" 16 | chk = 1 17 | for value in symbols: 18 | top = chk >> 35 19 | chk = (chk & 0x7FFFFFFFF) << 5 ^ value 20 | for i in range(5): 21 | chk ^= GENERATOR[i] if ((top >> i) & 1) else 0 22 | return chk 23 | 24 | 25 | def descsum_expand(s): 26 | """Internal function that does the character to symbol expansion""" 27 | groups = [] 28 | symbols = [] 29 | for c in s: 30 | if not c in INPUT_CHARSET: 31 | return None 32 | v = INPUT_CHARSET.find(c) 33 | symbols.append(v & 31) 34 | groups.append(v >> 5) 35 | if len(groups) == 3: 36 | symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2]) 37 | groups = [] 38 | if len(groups) == 1: 39 | symbols.append(groups[0]) 40 | elif len(groups) == 2: 41 | symbols.append(groups[0] * 3 + groups[1]) 42 | return symbols 43 | 44 | 45 | def descsum_create(s): 46 | """Add a checksum to a descriptor without""" 47 | symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0] 48 | checksum = descsum_polymod(symbols) ^ 1 49 | return ( 50 | s 51 | + "#" 52 | + "".join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8)) 53 | ) 54 | 55 | 56 | def descsum_check(s): 57 | """Verify that the checksum is correct in a descriptor""" 58 | if s[-9] != "#": 59 | return False 60 | if not all(x in CHECKSUM_CHARSET for x in s[-8:]): 61 | return False 62 | symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]] 63 | return descsum_polymod(symbols) == 1 64 | 65 | 66 | def drop_origins(s): 67 | """Drop the key origins from a descriptor""" 68 | desc = re.sub(r"\[.+?\]", "", s) 69 | if "#" in s: 70 | desc = desc[: desc.index("#")] 71 | return descsum_create(desc) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Script Descriptors, with Miniscript extension 2 | 3 | ## About 4 | 5 | In Bitcoin, output [Script](https://en.bitcoin.it/wiki/Script)s are used to express the conditions 6 | by which the amount associated with the output may be spent. 7 | 8 | **Output Script Descriptors** are a simple language which can be used to describe such Scripts 9 | generally and precisely. Bitcoin Output Script Descriptors are defined in a set of 10 | [BIP](https://github.com/bitcoin/bips/blob/master/bip-0001.mediawiki)s, the main one being 11 | [bip-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki). 12 | 13 | **Miniscript** is a language for writing (a subset of) Bitcoin Scripts in a structured way. It is an 14 | extension to Output Script Descriptors and is currently only defined within P2WSH context. 15 | 16 | Miniscript permits: 17 | - To safely extend the Output Descriptor language to many more scripting features thanks to the 18 | typing system (composition). 19 | - Statical analysis of spending conditions, maximum spending cost of each branch, security 20 | properties, third-party malleability. 21 | - General satisfaction of any correctly typed ("valid") Miniscript. The satisfaction itself is 22 | also analyzable. 23 | - To extend the possibilities of external signers, because of all of the above and since it carries 24 | enough metadata. 25 | 26 | Miniscript guarantees: 27 | - That for any statically-analyzed as "safe" Script, a witness can be constructed in the bounds of 28 | the consensus and standardness rules (standardness complete). 29 | - That unless the conditions of the Miniscript are met, no witness can be created for the Script 30 | (consensus sound). 31 | - Third-party malleability protection for the satisfaction of a sane Miniscript, which is too 32 | complex to summarize here. 33 | 34 | This library provides an implementation of Segwit-native Output Descriptors and of Miniscript (to be 35 | used within `wsh()` descriptors), with a minimal amount of dependencies. 36 | 37 | 38 | ## WIP: this is not ready for any real use! 39 | 40 | This library is still a work in progress. It contains known bugs (in the satisfier or the 41 | Taproot-Miniscript implementation for instance) and there are probably many unknown ones. See [this 42 | issue](https://github.com/darosior/python-bip380/issues/27) for a discussion about the state of this 43 | library. 44 | 45 | Still, it's ready for hacking around and contributions are welcome. See the [issue 46 | tracker](https://github.com/darosior/python-miniscript/issues) for ideas on where to start. 47 | -------------------------------------------------------------------------------- /bip380/miniscript/property.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 The Bitcoin Core developers 2 | # Copyright (c) 2021 Antoine Poinsot 3 | # Distributed under the MIT software license, see the accompanying 4 | # file LICENSE or http://www.opensource.org/licenses/mit-license.php. 5 | 6 | from .errors import MiniscriptPropertyError 7 | 8 | 9 | # TODO: implement __eq__ 10 | class Property: 11 | """Miniscript expression property""" 12 | 13 | # "B": Base type 14 | # "V": Verify type 15 | # "K": Key type 16 | # "W": Wrapped type 17 | # "z": Zero-arg property 18 | # "o": One-arg property 19 | # "n": Nonzero arg property 20 | # "d": Dissatisfiable property 21 | # "u": Unit property 22 | types = "BVKW" 23 | props = "zondu" 24 | 25 | def __init__(self, property_str=""): 26 | """Create a property, optionally from a str of property and types""" 27 | allowed = self.types + self.props 28 | invalid = set(property_str).difference(set(allowed)) 29 | 30 | if invalid: 31 | raise MiniscriptPropertyError( 32 | f"Invalid property/type character(s) '{''.join(invalid)}'" 33 | f" (allowed: '{allowed}')" 34 | ) 35 | 36 | for literal in allowed: 37 | setattr(self, literal, literal in property_str) 38 | 39 | self.check_valid() 40 | 41 | def __repr__(self): 42 | """Generate string representation of property""" 43 | return "".join([c for c in self.types + self.props if getattr(self, c)]) 44 | 45 | def has_all(self, properties): 46 | """Given a str of types and properties, return whether we have all of them""" 47 | return all([getattr(self, pt) for pt in properties]) 48 | 49 | def has_any(self, properties): 50 | """Given a str of types and properties, return whether we have at least one of them""" 51 | return any([getattr(self, pt) for pt in properties]) 52 | 53 | def check_valid(self): 54 | """Raises a MiniscriptPropertyError if the types/properties conflict""" 55 | # Can only be of a single type. 56 | if len(self.type()) > 1: 57 | raise MiniscriptPropertyError(f"A Miniscript fragment can only be of a single type, got '{self.type()}'") 58 | 59 | # Check for conflicts in type & properties. 60 | checks = [ 61 | # (type/property, must_be, must_not_be) 62 | ("K", "u", ""), 63 | ("V", "", "du"), 64 | ("z", "", "o"), 65 | ("n", "", "z"), 66 | ] 67 | conflicts = [] 68 | 69 | for (attr, must_be, must_not_be) in checks: 70 | if not getattr(self, attr): 71 | continue 72 | if not self.has_all(must_be): 73 | conflicts.append(f"{attr} must be {must_be}") 74 | if self.has_any(must_not_be): 75 | conflicts.append(f"{attr} must not be {must_not_be}") 76 | if conflicts: 77 | raise MiniscriptPropertyError(f"Conflicting types and properties: {', '.join(conflicts)}") 78 | 79 | def type(self): 80 | return "".join(filter(lambda x: x in self.types, str(self))) 81 | 82 | def properties(self): 83 | return "".join(filter(lambda x: x in self.props, str(self))) 84 | -------------------------------------------------------------------------------- /bip380/descriptors/parsing.py: -------------------------------------------------------------------------------- 1 | import bip380.descriptors as descriptors 2 | 3 | from bip380.descriptors.checksum import descsum_check 4 | from bip380.key import DescriptorKey, DescriptorKeyError 5 | from bip380.miniscript import Node 6 | from bip380.miniscript.parsing import parse_one 7 | 8 | from .errors import DescriptorParsingError 9 | from .utils import TreeNode 10 | 11 | 12 | def split_checksum(desc_str, strict=False): 13 | """Removes and check the provided checksum. 14 | If not told otherwise, this won't fail on a missing checksum. 15 | 16 | :param strict: whether to require the presence of the checksum. 17 | """ 18 | desc_split = desc_str.split("#") 19 | if len(desc_split) != 2: 20 | if strict: 21 | raise DescriptorParsingError("Missing checksum") 22 | return desc_split[0] 23 | 24 | descriptor, checksum = desc_split 25 | if not descsum_check(desc_str): 26 | raise DescriptorParsingError( 27 | f"Checksum '{checksum}' is invalid for '{descriptor}'" 28 | ) 29 | 30 | return descriptor 31 | 32 | 33 | def parse_tree_inner(tree_str): 34 | """Recursively called function to parse a tree exp. Returns a tuple (res, remaining) where 35 | res is the expression that was parsed (may be a Taproot tree node or a Miniscript) and remaining 36 | what's left to parse as a string. 37 | """ 38 | if len(tree_str) == 0: 39 | raise DescriptorParsingError("Invalid Taproot tree expression") 40 | # (From BIP386) 41 | # A Tree Expression is: 42 | # - Any Script Expression that is allowed at the level this Tree Expression is in. 43 | # - A pair of Tree Expressions consisting of: 44 | # - An open brace { 45 | # - A Tree Expression 46 | # - A comma , 47 | # - A Tree Expression 48 | # - A closing brace } 49 | if tree_str[0] != "{": 50 | return parse_one(tree_str, is_taproot=True) 51 | if len(tree_str) < 5 or tree_str[-1] != "}": 52 | raise DescriptorParsingError("Invalid Taproot tree expression") 53 | left_child, remaining = parse_tree_inner(tree_str[1:]) 54 | right_child, remaining = parse_tree_inner(remaining[1:]) 55 | return TreeNode(left_child, right_child), remaining[1:] 56 | 57 | 58 | def parse_tree_exp(tree_str): 59 | """Parse a tree expression as defined in BIP386.""" 60 | tree, remaining = parse_tree_inner(tree_str) 61 | assert len(remaining) == 0, remaining 62 | return tree 63 | 64 | 65 | def descriptor_from_str(desc_str, strict=False): 66 | """Parse a Bitcoin Output Script Descriptor from its string representation. 67 | 68 | :param strict: whether to require the presence of a checksum. 69 | """ 70 | desc_str = split_checksum(desc_str, strict=strict) 71 | 72 | if desc_str.startswith("wsh(") and desc_str.endswith(")"): 73 | # TODO: decent errors in the Miniscript module to be able to catch them here. 74 | ms = Node.from_str(desc_str[4:-1], is_taproot=False) 75 | return descriptors.WshDescriptor(ms) 76 | 77 | if desc_str.startswith("wpkh(") and desc_str.endswith(")"): 78 | try: 79 | pubkey = DescriptorKey(desc_str[5:-1]) 80 | except DescriptorKeyError as e: 81 | raise DescriptorParsingError(str(e)) 82 | return descriptors.WpkhDescriptor(pubkey) 83 | 84 | if desc_str.startswith("tr(") and desc_str.endswith(")"): 85 | # First parse the key expression 86 | try: 87 | comma_index = desc_str.find(",") 88 | if comma_index == -1: 89 | pubkey = DescriptorKey(desc_str[3:-1], x_only=True) 90 | else: 91 | pubkey = DescriptorKey(desc_str[3:comma_index], x_only=True) 92 | except DescriptorKeyError as e: 93 | raise DescriptorParsingError(str(e)) 94 | 95 | # Then the tree expression if it exists. 96 | tree = None 97 | if comma_index != -1: 98 | try: 99 | tree = parse_tree_exp(desc_str[comma_index + 1 : -1]) 100 | # TODO: have proper exceptions.. 101 | except Exception as e: 102 | raise DescriptorParsingError(str(e)) 103 | 104 | return descriptors.TrDescriptor(pubkey, tree) 105 | 106 | raise DescriptorParsingError(f"Unknown descriptor fragment: {desc_str}") 107 | -------------------------------------------------------------------------------- /bip380/descriptors/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for working with descriptors.""" 2 | 3 | import coincurve 4 | import hashlib 5 | 6 | from bip380.miniscript import Node 7 | from bip380.utils.script import CScript 8 | 9 | 10 | def compact_size(byte_arr): 11 | """The size prefix for this byte array encoded as little-endian. 12 | 13 | See https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer. 14 | """ 15 | size = len(byte_arr) 16 | if size < 253: 17 | return size.to_bytes(1, "little") 18 | if size < 2**16: 19 | return b"\xfd" + size.to_bytes(2, "little") 20 | if size < 2**32: 21 | return b"\xfe" + size.to_bytes(4, "little") 22 | return b"\xff" + size.to_bytes(8, "little") 23 | 24 | 25 | def tagged_hash(tag, data): 26 | ss = hashlib.sha256(tag.encode("utf-8")).digest() 27 | ss += ss 28 | ss += data 29 | return hashlib.sha256(ss).digest() 30 | 31 | 32 | def tapleaf_hash(leaf): 33 | """Compute the hash of a Taproot leaf as defined in BIP341.""" 34 | assert isinstance(leaf, CScript) 35 | script = bytes(leaf) 36 | return tagged_hash("TapLeaf", b"\xc0" + compact_size(script) + script) 37 | 38 | 39 | def tapbranch_hash(left_hash, right_hash): 40 | """Compute the Taproot branch hash for left and right child hashes. 41 | This takes care of the sorting as per BIP341. 42 | """ 43 | assert all(isinstance(h, bytes) for h in (left_hash, right_hash)) 44 | if right_hash < left_hash: 45 | return tagged_hash("TapBranch", right_hash + left_hash) 46 | return tagged_hash("TapBranch", left_hash + right_hash) 47 | 48 | 49 | def taproot_tweak(pubkey_bytes, merkle_root): 50 | """Compute the tweak to get the output key of a Taproot, as per BIP341.""" 51 | assert isinstance(pubkey_bytes, bytes) and len(pubkey_bytes) == 32 52 | assert isinstance(merkle_root, bytes) 53 | 54 | t = tagged_hash("TapTweak", pubkey_bytes + merkle_root) 55 | xonly_pubkey = coincurve.PublicKeyXOnly(pubkey_bytes) 56 | xonly_pubkey.tweak_add(t) # TODO: error handling 57 | 58 | return xonly_pubkey 59 | 60 | 61 | class TreeNode: 62 | """A node in a Taproot tree""" 63 | 64 | def __init__(self, left_child, right_child): 65 | """Instanciate a Taproot tree node with its two child. Each may be a leaf node.""" 66 | assert all(isinstance(c, (TreeNode, Node)) for c in (left_child, right_child)) 67 | self.left_child = left_child 68 | self.right_child = right_child 69 | 70 | # Cached merkle root of the tree as per BIP341 71 | self._merkle_root = None 72 | 73 | def __repr__(self): 74 | return f"{{{self.left_child},{self.right_child}}}" 75 | 76 | def _compute_merkle_proofs(self, merkle_proof=[]): 77 | """Internal method to compute the leaf-to-merkle-proof mapping.""" 78 | if isinstance(self.left_child, Node) and isinstance(self.right_child, Node): 79 | return { 80 | self.left_child: [tapleaf_hash(self.right_child.script)] + merkle_proof, 81 | self.right_child: [tapleaf_hash(self.left_child.script)] + merkle_proof, 82 | } 83 | if isinstance(self.left_child, Node): 84 | return { 85 | self.left_child: [self.right_child.merkle_root()] + merkle_proof, 86 | **self.right_child._compute_merkle_proofs( 87 | [tapleaf_hash(self.left_child.script)] + merkle_proof 88 | ), 89 | } 90 | if isinstance(self.right_child, Node): 91 | return { 92 | self.right_child: [self.left_child.merkle_root()] + merkle_proof, 93 | **self.left_child._compute_merkle_proofs( 94 | [tapleaf_hash(self.right_child.script)] + merkle_proof 95 | ), 96 | } 97 | return { 98 | **self.left_child._compute_merkle_proofs( 99 | [self.right_child.merkle_root()] + merkle_proof 100 | ), 101 | **self.right_child._compute_merkle_proofs( 102 | [self.left_child.merkle_root()] + merkle_proof 103 | ), 104 | } 105 | 106 | def merkle_proofs(self): 107 | """Get a mapping from each leaf to its merkle proof.""" 108 | return self._compute_merkle_proofs() 109 | 110 | def leaves(self): 111 | """Get the list of all the leaves.""" 112 | if isinstance(self.left_child, Node) and isinstance(self.right_child, Node): 113 | return [self.left_child, self.right_child] 114 | if isinstance(self.left_child, Node): 115 | return [self.left_child] + self.right_child.leaves() 116 | if isinstance(self.right_child, Node): 117 | return self.left_child.leaves() + [self.right_child] 118 | return self.left_child.leaves() + self.right_child.leaves() 119 | 120 | def _child_hash(self, child): 121 | """The hash of a child depending on whether it's a leaf or not.""" 122 | if isinstance(child, Node): 123 | return tapleaf_hash(child.script) 124 | assert isinstance(child, TreeNode) 125 | return child.merkle_root() 126 | 127 | def _compute_merkle_root(self): 128 | left_hash = self._child_hash(self.left_child) 129 | right_hash = self._child_hash(self.right_child) 130 | return tapbranch_hash(left_hash, right_hash) 131 | 132 | def merkle_root(self): 133 | if self._merkle_root is None: 134 | self._merkle_root = self._compute_merkle_root() 135 | return self._merkle_root 136 | 137 | 138 | class TaplefSat: 139 | """A satisfaction for a Taptree leaf.""" 140 | 141 | def __init__(self, merkle_proof, script_sat, script): 142 | assert isinstance(merkle_proof, list) 143 | assert isinstance(script_sat, list) 144 | assert isinstance(script, CScript) 145 | 146 | # The merkle proof to the leaf 147 | self.merkle_proof = merkle_proof 148 | # The depth of the leaf in the tree. Used to compute the cost of the whole 149 | # satisfaction. 150 | self.depth = len(merkle_proof) 151 | # The script represented by this leaf 152 | self.script = script 153 | self.script_len = len(bytes(script)) 154 | # The witness for satisfying this script 155 | self.script_sat = script_sat 156 | self.sat_size = sum(len(elem) for elem in script_sat) 157 | 158 | def __lt__(self, other): 159 | """Whether this satisfaction is smaller (ie less expensive) than the other one.""" 160 | return (self.depth + self.sat_size + self.script_len) < ( 161 | other.depth + other.sat_size + other.script_len 162 | ) 163 | 164 | def witness(self, internal_key, output_key_parity): 165 | """Get the full witness for satisfying this leaf.""" 166 | control_block = ( 167 | bytes([0xC0 | output_key_parity]) 168 | + internal_key.bytes() 169 | + b"".join(self.merkle_proof) 170 | ) 171 | return self.script_sat + [bytes(self.script), control_block] 172 | -------------------------------------------------------------------------------- /bip380/descriptors/__init__.py: -------------------------------------------------------------------------------- 1 | from bip380.key import DescriptorKey 2 | from bip380.miniscript import Node 3 | from bip380.utils.hashes import sha256, hash160 4 | from bip380.utils.script import ( 5 | CScript, 6 | OP_1, 7 | OP_DUP, 8 | OP_HASH160, 9 | OP_EQUALVERIFY, 10 | OP_CHECKSIG, 11 | ) 12 | 13 | from .checksum import descsum_create 14 | from .errors import DescriptorParsingError 15 | from .parsing import descriptor_from_str 16 | from .utils import tapleaf_hash, taproot_tweak, TaplefSat, TreeNode 17 | 18 | 19 | class Descriptor: 20 | """A Bitcoin Output Script Descriptor.""" 21 | 22 | def from_str(desc_str, strict=False): 23 | """Parse a Bitcoin Output Script Descriptor from its string representation. 24 | 25 | :param strict: whether to require the presence of a checksum. 26 | """ 27 | desc = descriptor_from_str(desc_str, strict) 28 | 29 | # BIP389 prescribes that no two multipath key expressions in a single descriptor 30 | # have different length. 31 | multipath_len = None 32 | for key in desc.keys: 33 | if key.is_multipath(): 34 | m_len = len(key.path.paths) 35 | if multipath_len is None: 36 | multipath_len = m_len 37 | elif multipath_len != m_len: 38 | raise DescriptorParsingError( 39 | f"Descriptor contains multipath key expressions with varying length: '{desc_str}'." 40 | ) 41 | 42 | return desc 43 | 44 | @property 45 | def script_pubkey(self): 46 | """Get the ScriptPubKey (output 'locking' Script) for this descriptor.""" 47 | # To be implemented by derived classes 48 | raise NotImplementedError 49 | 50 | @property 51 | def script_sighash(self): 52 | """Get the Script to be committed to by the signature hash of a spending transaction.""" 53 | # To be implemented by derived classes 54 | raise NotImplementedError 55 | 56 | @property 57 | def keys(self): 58 | """Get the list of all keys from this descriptor, in order of apparition.""" 59 | # To be implemented by derived classes 60 | raise NotImplementedError 61 | 62 | def derive(self, index): 63 | """Derive the key at the given derivation index. 64 | 65 | A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened 66 | wildcard". 67 | """ 68 | assert isinstance(index, int) 69 | for key in self.keys: 70 | key.derive(index) 71 | 72 | def satisfy(self, *args, **kwargs): 73 | """Get the witness stack to spend from this descriptor. 74 | 75 | Various data may need to be passed as parameters to meet the locking 76 | conditions set by the Script. 77 | """ 78 | # To be implemented by derived classes 79 | raise NotImplementedError 80 | 81 | def copy(self): 82 | """Get a copy of this descriptor.""" 83 | # FIXME: do something nicer than roundtripping through string ser 84 | return Descriptor.from_str(str(self)) 85 | 86 | def is_multipath(self): 87 | """Whether this descriptor contains multipath key expression(s).""" 88 | return any(k.is_multipath() for k in self.keys) 89 | 90 | def singlepath_descriptors(self): 91 | """Get a list of descriptors that only contain keys that don't have multiple 92 | derivation paths. 93 | """ 94 | singlepath_descs = [self.copy()] 95 | 96 | # First figure out the number of descriptors there will be 97 | for key in self.keys: 98 | if key.is_multipath(): 99 | singlepath_descs += [ 100 | self.copy() for _ in range(len(key.path.paths) - 1) 101 | ] 102 | break 103 | 104 | # Return early if there was no multipath key expression 105 | if len(singlepath_descs) == 1: 106 | return singlepath_descs 107 | 108 | # Then use one path for each 109 | for i, desc in enumerate(singlepath_descs): 110 | for key in desc.keys: 111 | if key.is_multipath(): 112 | assert len(key.path.paths) == len(singlepath_descs) 113 | key.path.paths = key.path.paths[i : i + 1] 114 | 115 | assert all(not d.is_multipath() for d in singlepath_descs) 116 | return singlepath_descs 117 | 118 | 119 | # TODO: add methods to give access to all the Miniscript analysis 120 | class WshDescriptor(Descriptor): 121 | """A Segwit v0 P2WSH Output Script Descriptor.""" 122 | 123 | def __init__(self, witness_script): 124 | assert isinstance(witness_script, Node) 125 | self.witness_script = witness_script 126 | 127 | def __repr__(self): 128 | return descsum_create(f"wsh({self.witness_script})") 129 | 130 | @property 131 | def script_pubkey(self): 132 | witness_program = sha256(self.witness_script.script) 133 | return CScript([0, witness_program]) 134 | 135 | @property 136 | def script_sighash(self): 137 | return self.witness_script.script 138 | 139 | @property 140 | def keys(self): 141 | return self.witness_script.keys 142 | 143 | def satisfy(self, sat_material=None): 144 | """Get the witness stack to spend from this descriptor. 145 | 146 | :param sat_material: a miniscript.satisfaction.SatisfactionMaterial with data 147 | available to fulfill the conditions set by the Script. 148 | """ 149 | sat = self.witness_script.satisfy(sat_material) 150 | if sat is not None: 151 | return sat + [self.witness_script.script] 152 | 153 | 154 | class WpkhDescriptor(Descriptor): 155 | """A Segwit v0 P2WPKH Output Script Descriptor.""" 156 | 157 | def __init__(self, pubkey): 158 | assert isinstance(pubkey, DescriptorKey) 159 | self.pubkey = pubkey 160 | 161 | def __repr__(self): 162 | return descsum_create(f"wpkh({self.pubkey})") 163 | 164 | @property 165 | def script_pubkey(self): 166 | witness_program = hash160(self.pubkey.bytes()) 167 | return CScript([0, witness_program]) 168 | 169 | @property 170 | def script_sighash(self): 171 | key_hash = hash160(self.pubkey.bytes()) 172 | return CScript([OP_DUP, OP_HASH160, key_hash, OP_EQUALVERIFY, OP_CHECKSIG]) 173 | 174 | @property 175 | def keys(self): 176 | return [self.pubkey] 177 | 178 | def satisfy(self, signature): 179 | """Get the witness stack to spend from this descriptor. 180 | 181 | :param signature: a signature (in bytes) for the pubkey from the descriptor. 182 | """ 183 | assert isinstance(signature, bytes) 184 | return [signature, self.pubkey.bytes()] 185 | 186 | 187 | class TrDescriptor(Descriptor): 188 | """A Pay-to-Taproot Output Script Descriptor.""" 189 | 190 | def __init__(self, internal_key, tree=None): 191 | assert isinstance(internal_key, DescriptorKey) and internal_key.x_only 192 | assert tree is None or isinstance(tree, (TreeNode, Node)) 193 | self.internal_key = internal_key 194 | self.tree = tree 195 | 196 | def __repr__(self): 197 | if self.tree is not None: 198 | return descsum_create(f"tr({self.internal_key},{self.tree})") 199 | return descsum_create(f"tr({self.internal_key})") 200 | 201 | def output_key(self): 202 | if isinstance(self.tree, TreeNode): 203 | merkle_root = self.tree.merkle_root() 204 | elif isinstance(self.tree, Node): 205 | merkle_root = tapleaf_hash(self.tree.script) 206 | else: 207 | assert self.tree is None 208 | # "If the spending conditions do not require a script path, the output key 209 | # should commit to an unspendable script path" (see BIP341, BIP386) 210 | merkle_root = b"" 211 | return taproot_tweak(self.internal_key.bytes(), merkle_root) 212 | 213 | @property 214 | def script_pubkey(self): 215 | return CScript([OP_1, self.output_key().format()]) 216 | 217 | @property 218 | def keys(self): 219 | if isinstance(self.tree, Node): 220 | leaves_keys = self.tree.keys 221 | elif isinstance(self.tree, TreeNode): 222 | leaves_keys = [k for leaf in self.tree.leaves() for k in leaf.keys] 223 | else: 224 | assert self.tree is None 225 | leaves_keys = [] 226 | return [self.internal_key] + leaves_keys 227 | 228 | def satisfy(self, sat_material=None): 229 | """Get the witness stack to spend from this descriptor. 230 | 231 | :param sat_material: a miniscript.satisfaction.SatisfactionMaterial with data 232 | available to spend from the key path or any of the leaves. 233 | """ 234 | # First, try to satisfy using key-path spend 235 | out_key = self.output_key() 236 | raw_out_key = out_key.format() 237 | if raw_out_key in sat_material.signatures: 238 | return [sat_material.signatures[raw_out_key]] 239 | 240 | # Then, look for satisfiable leaves. Use the less expensive available. 241 | if self.tree is None: 242 | return 243 | merkle_proofs = self.tree.merkle_proofs() 244 | best_sat = None 245 | for leaf, merkle_proof in merkle_proofs.items(): 246 | print(merkle_proof) 247 | sat = leaf.satisfy(sat_material) 248 | if sat is None: 249 | continue 250 | sat = TaplefSat(merkle_proof, sat, leaf.script) 251 | if best_sat is None or sat < best_sat: 252 | best_sat = sat 253 | 254 | if best_sat is not None: 255 | return best_sat.witness(self.internal_key, out_key.parity) 256 | 257 | return 258 | -------------------------------------------------------------------------------- /bip380/key.py: -------------------------------------------------------------------------------- 1 | import coincurve 2 | import copy 3 | 4 | from bip32 import BIP32, HARDENED_INDEX 5 | from bip32.utils import _deriv_path_str_to_list 6 | from bip380.utils.hashes import hash160 7 | from enum import Enum, auto 8 | 9 | 10 | def is_raw_key(obj): 11 | return isinstance(obj, (coincurve.PublicKey, coincurve.PublicKeyXOnly)) 12 | 13 | 14 | class DescriptorKeyError(Exception): 15 | def __init__(self, message): 16 | self.message = message 17 | 18 | 19 | class DescriporKeyOrigin: 20 | """The origin of a key in a descriptor. 21 | 22 | See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions. 23 | """ 24 | 25 | def __init__(self, fingerprint, path): 26 | assert isinstance(fingerprint, bytes) and isinstance(path, list) 27 | 28 | self.fingerprint = fingerprint 29 | self.path = path 30 | 31 | def from_str(origin_str): 32 | # Origing starts and ends with brackets 33 | if not origin_str.startswith("[") or not origin_str.endswith("]"): 34 | raise DescriptorKeyError(f"Insane origin: '{origin_str}'") 35 | # At least 8 hex characters + brackets 36 | if len(origin_str) < 10: 37 | raise DescriptorKeyError(f"Insane origin: '{origin_str}'") 38 | 39 | # For the fingerprint, just read the 4 bytes. 40 | try: 41 | fingerprint = bytes.fromhex(origin_str[1:9]) 42 | except ValueError: 43 | raise DescriptorKeyError(f"Insane fingerprint in origin: '{origin_str}'") 44 | # For the path, we (how bad) reuse an internal helper from python-bip32. 45 | path = [] 46 | if len(origin_str) > 10: 47 | if origin_str[9] != "/": 48 | raise DescriptorKeyError(f"Insane path in origin: '{origin_str}'") 49 | # The helper operates on "m/10h/11/12'/13", so give it a "m". 50 | dummy = "m" 51 | try: 52 | path = _deriv_path_str_to_list(dummy + origin_str[9:-1]) 53 | except ValueError: 54 | raise DescriptorKeyError(f"Insane path in origin: '{origin_str}'") 55 | 56 | return DescriporKeyOrigin(fingerprint, path) 57 | 58 | 59 | class KeyPathKind(Enum): 60 | FINAL = auto() 61 | WILDCARD_UNHARDENED = auto() 62 | WILDCARD_HARDENED = auto() 63 | 64 | def is_wildcard(self): 65 | return self in [KeyPathKind.WILDCARD_HARDENED, KeyPathKind.WILDCARD_UNHARDENED] 66 | 67 | 68 | def parse_index(index_str): 69 | """Parse a derivation index, as contained in a derivation path.""" 70 | assert isinstance(index_str, str) 71 | 72 | try: 73 | # if HARDENED 74 | if index_str[-1:] in ["'", "h", "H"]: 75 | return int(index_str[:-1]) + HARDENED_INDEX 76 | else: 77 | return int(index_str) 78 | except ValueError as e: 79 | raise DescriptorKeyError(f"Invalid derivation index {index_str}: '{e}'") 80 | 81 | 82 | class DescriptorKeyPath: 83 | """The derivation path of a key in a descriptor. 84 | 85 | See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions 86 | as well as BIP389 for multipath expressions. 87 | """ 88 | 89 | def __init__(self, paths, kind): 90 | assert ( 91 | isinstance(paths, list) 92 | and isinstance(kind, KeyPathKind) 93 | and len(paths) > 0 94 | and all(isinstance(p, list) for p in paths) 95 | ) 96 | 97 | self.paths = paths 98 | self.kind = kind 99 | 100 | def is_multipath(self): 101 | """Whether this derivation path actually contains multiple of them.""" 102 | return len(self.paths) > 1 103 | 104 | def from_str(path_str): 105 | if len(path_str) < 2: 106 | raise DescriptorKeyError(f"Insane key path: '{path_str}'") 107 | if path_str[0] != "/": 108 | raise DescriptorKeyError(f"Insane key path: '{path_str}'") 109 | 110 | # Determine whether this key may be derived. 111 | kind = KeyPathKind.FINAL 112 | if len(path_str) > 2 and path_str[-3:] in ["/*'", "/*h", "/*H"]: 113 | kind = KeyPathKind.WILDCARD_HARDENED 114 | path_str = path_str[:-3] 115 | elif len(path_str) > 1 and path_str[-2:] == "/*": 116 | kind = KeyPathKind.WILDCARD_UNHARDENED 117 | path_str = path_str[:-2] 118 | 119 | paths = [[]] 120 | if len(path_str) == 0: 121 | return DescriptorKeyPath(paths, kind) 122 | 123 | for index in path_str[1:].split("/"): 124 | # If this is a multipath expression, of the form '' 125 | if ( 126 | index.startswith("<") 127 | and index.endswith(">") 128 | and ";" in index 129 | and len(index) >= 5 130 | ): 131 | # Can't have more than one multipath expression 132 | if len(paths) > 1: 133 | raise DescriptorKeyError( 134 | f"May only have a single multipath step in derivation path: '{path_str}'" 135 | ) 136 | indexes = index[1:-1].split(";") 137 | paths = [copy.copy(paths[0]) for _ in indexes] 138 | for i, der_index in enumerate(indexes): 139 | paths[i].append(parse_index(der_index)) 140 | else: 141 | # This is a "single index" expression. 142 | for path in paths: 143 | path.append(parse_index(index)) 144 | return DescriptorKeyPath(paths, kind) 145 | 146 | 147 | class DescriptorKey: 148 | """A Bitcoin key to be used in Output Script Descriptors. 149 | 150 | May be an extended or raw public key. 151 | """ 152 | 153 | def __init__(self, key, x_only=False): 154 | # Information about the origin of this key. 155 | self.origin = None 156 | # If it is an xpub, a path toward a child key of that xpub. 157 | self.path = None 158 | # Whether to only create x-only public keys. 159 | self.x_only = x_only 160 | # Whether to serialize to string representation without the sign byte. 161 | # This is necessary to roundtrip 33-bytes keys under Taproot context. 162 | self.ser_x_only = x_only 163 | 164 | if isinstance(key, bytes): 165 | if len(key) == 32: 166 | key_cls = coincurve.PublicKeyXOnly 167 | self.x_only = True 168 | self.ser_x_only = True 169 | elif len(key) == 33: 170 | key_cls = coincurve.PublicKey 171 | self.ser_x_only = False 172 | else: 173 | raise DescriptorKeyError( 174 | "Only compressed and x-only keys are supported" 175 | ) 176 | try: 177 | self.key = key_cls(key) 178 | except ValueError as e: 179 | raise DescriptorKeyError(f"Public key parsing error: '{str(e)}'") 180 | 181 | elif isinstance(key, BIP32): 182 | self.key = key 183 | 184 | elif isinstance(key, str): 185 | # Try parsing an optional origin prepended to the key 186 | splitted_key = key.split("]", maxsplit=1) 187 | if len(splitted_key) == 2: 188 | origin, key = splitted_key 189 | self.origin = DescriporKeyOrigin.from_str(origin + "]") 190 | 191 | # Is it a raw key? 192 | if len(key) in (64, 66): 193 | pk_cls = coincurve.PublicKey 194 | if len(key) == 64: 195 | pk_cls = coincurve.PublicKeyXOnly 196 | self.x_only = True 197 | self.ser_x_only = True 198 | else: 199 | self.ser_x_only = False 200 | try: 201 | self.key = pk_cls(bytes.fromhex(key)) 202 | except ValueError as e: 203 | raise DescriptorKeyError(f"Public key parsing error: '{str(e)}'") 204 | # If not it must be an xpub. 205 | else: 206 | # There may be an optional path appended to the xpub. 207 | splitted_key = key.split("/", maxsplit=1) 208 | if len(splitted_key) == 2: 209 | key, path = splitted_key 210 | self.path = DescriptorKeyPath.from_str("/" + path) 211 | 212 | try: 213 | self.key = BIP32.from_xpub(key) 214 | except ValueError as e: 215 | raise DescriptorKeyError(f"Xpub parsing error: '{str(e)}'") 216 | 217 | else: 218 | raise DescriptorKeyError( 219 | "Invalid parameter type: expecting bytes, hex str or BIP32 instance." 220 | ) 221 | 222 | def __repr__(self): 223 | key = "" 224 | 225 | def ser_index(key, der_index): 226 | # If this a hardened step, deduce the threshold and mark it. 227 | if der_index < HARDENED_INDEX: 228 | return str(der_index) 229 | else: 230 | return f"{der_index - 2**31}'" 231 | 232 | def ser_paths(key, paths): 233 | assert len(paths) > 0 234 | 235 | for i, der_index in enumerate(paths[0]): 236 | # If this is a multipath expression, write the multi-index step accordingly 237 | if len(paths) > 1 and paths[1][i] != der_index: 238 | key += "/<" 239 | for j, path in enumerate(paths): 240 | key += ser_index(key, path[i]) 241 | if j < len(paths) - 1: 242 | key += ";" 243 | key += ">" 244 | else: 245 | key += "/" + ser_index(key, der_index) 246 | 247 | return key 248 | 249 | if self.origin is not None: 250 | key += f"[{self.origin.fingerprint.hex()}" 251 | key = ser_paths(key, [self.origin.path]) 252 | key += "]" 253 | 254 | if isinstance(self.key, BIP32): 255 | key += self.key.get_xpub() 256 | else: 257 | assert is_raw_key(self.key) 258 | raw_key = self.key.format() 259 | if len(raw_key) == 33 and self.ser_x_only: 260 | raw_key = raw_key[1:] 261 | key += raw_key.hex() 262 | 263 | if self.path is not None: 264 | key = ser_paths(key, self.path.paths) 265 | if self.path.kind == KeyPathKind.WILDCARD_UNHARDENED: 266 | key += "/*" 267 | elif self.path.kind == KeyPathKind.WILDCARD_HARDENED: 268 | key += "/*'" 269 | 270 | return key 271 | 272 | def is_multipath(self): 273 | """Whether this key contains more than one derivation path.""" 274 | return self.path is not None and self.path.is_multipath() 275 | 276 | def derivation_path(self): 277 | """Get the single derivation path for this key. 278 | 279 | Will raise if it has multiple, and return None if it doesn't have any. 280 | """ 281 | if self.path is None: 282 | return None 283 | if self.path.is_multipath(): 284 | raise DescriptorKeyError( 285 | f"Key has multiple derivation paths: {self.path.paths}" 286 | ) 287 | return self.path.paths[0] 288 | 289 | def bytes(self): 290 | """Get this key as raw bytes. 291 | 292 | Will raise if this key contains multiple derivation paths. 293 | """ 294 | if is_raw_key(self.key): 295 | raw = self.key.format() 296 | if self.x_only and len(raw) == 33: 297 | return raw[1:] 298 | assert len(raw) == 32 or not self.x_only 299 | return raw 300 | else: 301 | assert isinstance(self.key, BIP32) 302 | path = self.derivation_path() 303 | if path is None: 304 | return self.key.pubkey 305 | assert not self.path.kind.is_wildcard() # TODO: real errors 306 | return self.key.get_pubkey_from_path(path) 307 | 308 | def derive(self, index): 309 | """Derive the key at the given index. 310 | 311 | Will raise if this key contains multiple derivation paths. 312 | A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened 313 | wildcard". 314 | """ 315 | assert isinstance(index, int) 316 | if ( 317 | self.path is None 318 | or self.path.is_multipath() 319 | or self.path.kind == KeyPathKind.FINAL 320 | ): 321 | return 322 | assert isinstance(self.key, BIP32) 323 | 324 | if self.path.kind == KeyPathKind.WILDCARD_HARDENED: 325 | index += 2 ** 31 326 | assert index < 2 ** 32 327 | 328 | if self.origin is None: 329 | fingerprint = hash160(self.key.pubkey)[:4] 330 | self.origin = DescriporKeyOrigin(fingerprint, [index]) 331 | else: 332 | self.origin.path.append(index) 333 | 334 | # This can't fail now. 335 | path = self.derivation_path() 336 | # TODO(bip32): have a way to derive without roundtripping through string ser. 337 | self.key = BIP32.from_xpub(self.key.get_xpub_from_path(path + [index])) 338 | self.path = None 339 | -------------------------------------------------------------------------------- /bip380/utils/script.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2020 The Bitcoin Core developers 2 | # Copyright (c) 2021 Antoine Poinsot 3 | # Distributed under the MIT software license, see the accompanying 4 | # file LICENSE or http://www.opensource.org/licenses/mit-license.php. 5 | """Script utilities 6 | 7 | This file was taken from Bitcoin Core test framework, and was previously 8 | modified from python-bitcoinlib. 9 | """ 10 | import struct 11 | 12 | from .bignum import bn2vch 13 | 14 | 15 | OPCODE_NAMES = {} 16 | 17 | 18 | class CScriptOp(int): 19 | """A single script opcode""" 20 | 21 | __slots__ = () 22 | 23 | @staticmethod 24 | def encode_op_pushdata(d): 25 | """Encode a PUSHDATA op, returning bytes""" 26 | if len(d) < 0x4C: 27 | return b"" + bytes([len(d)]) + d # OP_PUSHDATA 28 | elif len(d) <= 0xFF: 29 | return b"\x4c" + bytes([len(d)]) + d # OP_PUSHDATA1 30 | elif len(d) <= 0xFFFF: 31 | return b"\x4d" + struct.pack(b" 4: 215 | raise ScriptNumError("Too large push") 216 | 217 | if size == 0: 218 | return 0 219 | 220 | # We always check for minimal encoding 221 | if (data[size - 1] & 0x7f) == 0: 222 | if size == 1 or (data[size - 2] & 0x80) == 0: 223 | raise ScriptNumError("Non minimal encoding") 224 | 225 | res = int.from_bytes(data, byteorder="little") 226 | 227 | # Remove the sign bit if set, and negate the result 228 | if data[size - 1] & 0x80: 229 | return -(res & ~(0x80 << (size - 1))) 230 | return res 231 | 232 | 233 | class CScriptInvalidError(Exception): 234 | """Base class for CScript exceptions""" 235 | 236 | pass 237 | 238 | 239 | class CScriptTruncatedPushDataError(CScriptInvalidError): 240 | """Invalid pushdata due to truncation""" 241 | 242 | def __init__(self, msg, data): 243 | self.data = data 244 | super(CScriptTruncatedPushDataError, self).__init__(msg) 245 | 246 | 247 | # This is used, eg, for blockchain heights in coinbase scripts (bip34) 248 | class CScriptNum: 249 | __slots__ = ("value",) 250 | 251 | def __init__(self, d=0): 252 | self.value = d 253 | 254 | @staticmethod 255 | def encode(obj): 256 | r = bytearray(0) 257 | if obj.value == 0: 258 | return bytes(r) 259 | neg = obj.value < 0 260 | absvalue = -obj.value if neg else obj.value 261 | while absvalue: 262 | r.append(absvalue & 0xFF) 263 | absvalue >>= 8 264 | if r[-1] & 0x80: 265 | r.append(0x80 if neg else 0) 266 | elif neg: 267 | r[-1] |= 0x80 268 | return bytes([len(r)]) + r 269 | 270 | @staticmethod 271 | def decode(vch): 272 | result = 0 273 | # We assume valid push_size and minimal encoding 274 | value = vch[1:] 275 | if len(value) == 0: 276 | return result 277 | for i, byte in enumerate(value): 278 | result |= int(byte) << 8 * i 279 | if value[-1] >= 0x80: 280 | # Mask for all but the highest result bit 281 | num_mask = (2 ** (len(value) * 8) - 1) >> 1 282 | result &= num_mask 283 | result *= -1 284 | return result 285 | 286 | 287 | class CScript(bytes): 288 | """Serialized script 289 | 290 | A bytes subclass, so you can use this directly whenever bytes are accepted. 291 | Note that this means that indexing does *not* work - you'll get an index by 292 | byte rather than opcode. This format was chosen for efficiency so that the 293 | general case would not require creating a lot of little CScriptOP objects. 294 | 295 | iter(script) however does iterate by opcode. 296 | """ 297 | 298 | __slots__ = () 299 | 300 | @classmethod 301 | def __coerce_instance(cls, other): 302 | # Coerce other into bytes 303 | if isinstance(other, CScriptOp): 304 | other = bytes([other]) 305 | elif isinstance(other, CScriptNum): 306 | if other.value == 0: 307 | other = bytes([CScriptOp(OP_0)]) 308 | else: 309 | other = CScriptNum.encode(other) 310 | elif isinstance(other, int): 311 | if 0 <= other <= 16: 312 | other = bytes([CScriptOp.encode_op_n(other)]) 313 | elif other == -1: 314 | other = bytes([OP_1NEGATE]) 315 | else: 316 | other = CScriptOp.encode_op_pushdata(bn2vch(other)) 317 | elif isinstance(other, (bytes, bytearray)): 318 | other = CScriptOp.encode_op_pushdata(other) 319 | return other 320 | 321 | def __add__(self, other): 322 | # Do the coercion outside of the try block so that errors in it are 323 | # noticed. 324 | other = self.__coerce_instance(other) 325 | 326 | try: 327 | # bytes.__add__ always returns bytes instances unfortunately 328 | return CScript(super(CScript, self).__add__(other)) 329 | except TypeError: 330 | raise TypeError("Can not add a %r instance to a CScript" % other.__class__) 331 | 332 | def join(self, iterable): 333 | # join makes no sense for a CScript() 334 | raise NotImplementedError 335 | 336 | def __new__(cls, value=b""): 337 | if isinstance(value, bytes) or isinstance(value, bytearray): 338 | return super(CScript, cls).__new__(cls, value) 339 | else: 340 | 341 | def coerce_iterable(iterable): 342 | for instance in iterable: 343 | yield cls.__coerce_instance(instance) 344 | 345 | # Annoyingly on both python2 and python3 bytes.join() always 346 | # returns a bytes instance even when subclassed. 347 | return super(CScript, cls).__new__(cls, b"".join(coerce_iterable(value))) 348 | 349 | def raw_iter(self): 350 | """Raw iteration 351 | 352 | Yields tuples of (opcode, data, sop_idx) so that the different possible 353 | PUSHDATA encodings can be accurately distinguished, as well as 354 | determining the exact opcode byte indexes. (sop_idx) 355 | """ 356 | i = 0 357 | while i < len(self): 358 | sop_idx = i 359 | opcode = self[i] 360 | i += 1 361 | 362 | if opcode > OP_PUSHDATA4: 363 | yield (opcode, None, sop_idx) 364 | else: 365 | datasize = None 366 | pushdata_type = None 367 | if opcode < OP_PUSHDATA1: 368 | pushdata_type = "PUSHDATA(%d)" % opcode 369 | datasize = opcode 370 | 371 | elif opcode == OP_PUSHDATA1: 372 | pushdata_type = "PUSHDATA1" 373 | if i >= len(self): 374 | raise CScriptInvalidError("PUSHDATA1: missing data length") 375 | datasize = self[i] 376 | i += 1 377 | 378 | elif opcode == OP_PUSHDATA2: 379 | pushdata_type = "PUSHDATA2" 380 | if i + 1 >= len(self): 381 | raise CScriptInvalidError("PUSHDATA2: missing data length") 382 | datasize = self[i] + (self[i + 1] << 8) 383 | i += 2 384 | 385 | elif opcode == OP_PUSHDATA4: 386 | pushdata_type = "PUSHDATA4" 387 | if i + 3 >= len(self): 388 | raise CScriptInvalidError("PUSHDATA4: missing data length") 389 | datasize = ( 390 | self[i] 391 | + (self[i + 1] << 8) 392 | + (self[i + 2] << 16) 393 | + (self[i + 3] << 24) 394 | ) 395 | i += 4 396 | 397 | else: 398 | assert False # shouldn't happen 399 | 400 | data = bytes(self[i : i + datasize]) 401 | 402 | # Check for truncation 403 | if len(data) < datasize: 404 | raise CScriptTruncatedPushDataError( 405 | "%s: truncated data" % pushdata_type, data 406 | ) 407 | 408 | i += datasize 409 | 410 | yield (opcode, data, sop_idx) 411 | 412 | def __iter__(self): 413 | """'Cooked' iteration 414 | 415 | Returns either a CScriptOP instance, an integer, or bytes, as 416 | appropriate. 417 | 418 | See raw_iter() if you need to distinguish the different possible 419 | PUSHDATA encodings. 420 | """ 421 | for (opcode, data, sop_idx) in self.raw_iter(): 422 | if data is not None: 423 | yield data 424 | else: 425 | opcode = CScriptOp(opcode) 426 | 427 | if opcode.is_small_int(): 428 | yield opcode.decode_op_n() 429 | else: 430 | yield CScriptOp(opcode) 431 | 432 | def __repr__(self): 433 | def _repr(o): 434 | if isinstance(o, bytes): 435 | return "x('%s')" % o.hex() 436 | else: 437 | return repr(o) 438 | 439 | ops = [] 440 | i = iter(self) 441 | while True: 442 | op = None 443 | try: 444 | op = _repr(next(i)) 445 | except CScriptTruncatedPushDataError as err: 446 | op = "%s..." % (_repr(err.data), err) 447 | break 448 | except CScriptInvalidError as err: 449 | op = "" % err 450 | break 451 | except StopIteration: 452 | break 453 | finally: 454 | if op is not None: 455 | ops.append(op) 456 | 457 | return "CScript([%s])" % ", ".join(ops) 458 | 459 | def GetSigOpCount(self, fAccurate): 460 | """Get the SigOp count. 461 | 462 | fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details. 463 | 464 | Note that this is consensus-critical. 465 | """ 466 | n = 0 467 | lastOpcode = OP_INVALIDOPCODE 468 | for (opcode, data, sop_idx) in self.raw_iter(): 469 | if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY): 470 | n += 1 471 | elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): 472 | if fAccurate and (OP_1 <= lastOpcode <= OP_16): 473 | n += opcode.decode_op_n() 474 | else: 475 | n += 20 476 | lastOpcode = opcode 477 | return n 478 | -------------------------------------------------------------------------------- /tests/data/valid_samples.txt: -------------------------------------------------------------------------------- 1 | l:older(1) 63006751b268 2 | l:older(2147483647) 63006704ffffff7fb268 3 | u:after(1) 6351b1670068 4 | u:after(2147483647) 6304ffffff7fb1670068 5 | andor(0,1,1) 006451675168 6 | andor(n:or_i(0,after(1)),1,1) 63006751b168926451675168 7 | c:andor(0,pk_k(03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7),pk_k(036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00)) 006421036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00672103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c768ac 8 | t:andor(0,v:1,v:1) 006451696751696851 9 | and_v(v:1,1) 516951 10 | t:and_v(v:1,v:1) 5169516951 11 | c:and_v(v:1,pk_k(036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00)) 516921036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00ac 12 | and_b(1,a:1) 516b516c9a 13 | or_b(0,a:0) 006b006c9b 14 | t:or_c(0,v:1) 006451696851 15 | t:or_c(n:or_i(0,after(1)),v:1) 63006751b168926451696851 16 | or_d(0,1) 0073645168 17 | or_d(n:or_i(0,after(1)),1) 63006751b1689273645168 18 | or_i(1,1) 6351675168 19 | t:or_i(v:1,v:1) 6351696751696851 20 | c:or_i(pk_k(03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7),pk_k(036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00)) 632103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c76721036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a0068ac 21 | or_b(l:after(100),al:after(1000000000)) 6300670164b1686b6300670400ca9a3bb1686c9b 22 | and_b(after(100),a:after(1000000000)) 0164b16b0400ca9a3bb16c9a 23 | lltvln:after(1231488000) 6300676300676300670400046749b1926869516868 24 | uuj:and_v(v:multi(2,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a,025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc),after(1231488000)) 6363829263522103d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a21025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc52af0400046749b168670068670068 25 | or_b(un:multi(2,03daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729,024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97),al:older(16)) 63522103daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee872921024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c9752ae926700686b63006760b2686c9b 26 | j:and_v(vdv:after(1567547623),older(2016)) 829263766304e7e06e5db169686902e007b268 27 | t:and_v(vu:hash256(131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b),v:sha256(ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)) 6382012088aa20131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b876700686982012088a820ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc58851 28 | t:andor(multi(3,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556,02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),v:older(4194305),v:sha256(9267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2)) 532102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e2103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a14602975562102e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1353ae6482012088a8209267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2886703010040b2696851 29 | or_d(multi(1,02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9),or_b(multi(3,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,032fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a),su:after(500000))) 512102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f951ae73645321022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a0121032fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f2103d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a53ae7c630320a107b16700689b68 30 | or_d(sha256(38df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b6),and_n(un:after(499999999),older(4194305))) 82012088a82038df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b68773646304ff64cd1db19267006864006703010040b26868 31 | and_v(or_i(v:multi(2,02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb),v:multi(2,03e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)),sha256(d1ec675902ef1633427ca360b290b0b3045a0d9058ddb5e648b4c3c3224c5c68)) 63522102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee52103774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb52af67522103e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a21025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc52af6882012088a820d1ec675902ef1633427ca360b290b0b3045a0d9058ddb5e648b4c3c3224c5c6887 32 | j:and_b(multi(2,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97),s:or_i(older(1),older(4252898))) 82926352210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179821024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c9752ae7c6351b26703e2e440b2689a68 33 | and_b(older(16),s:or_d(sha256(e38990d0c7fc009880a9c07c23842e886c6bbdc964ce6bdd5817ad357335ee6f),n:after(1567547623))) 60b27c82012088a820e38990d0c7fc009880a9c07c23842e886c6bbdc964ce6bdd5817ad357335ee6f87736404e7e06e5db192689a 34 | j:and_v(v:hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),or_d(sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),older(16))) 82926382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d078882012088a82096de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c4787736460b26868 35 | and_b(hash256(32ba476771d01e37807990ead8719f08af494723de1d228f2c2c07cc0aa40bac),a:and_b(hash256(131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b),a:older(1))) 82012088aa2032ba476771d01e37807990ead8719f08af494723de1d228f2c2c07cc0aa40bac876b82012088aa20131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b876b51b26c9a6c9a 36 | thresh(2,multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00),a:multi(1,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00),ac:pk_k(022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01)) 522103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c721036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a0052ae6b5121036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a0051ae6c936b21022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01ac6c935287 37 | and_n(sha256(d1ec675902ef1633427ca360b290b0b3045a0d9058ddb5e648b4c3c3224c5c68),t:or_i(v:older(4252898),v:older(144))) 82012088a820d1ec675902ef1633427ca360b290b0b3045a0d9058ddb5e648b4c3c3224c5c68876400676303e2e440b26967029000b269685168 38 | or_d(nd:and_v(v:older(4252898),v:older(4252898)),sha256(38df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b6)) 766303e2e440b26903e2e440b2696892736482012088a82038df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b68768 39 | c:and_v(or_c(sha256(9267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2),v:multi(1,02c44d12c7065d812e8acf28d7cbb19f9011ecd9e9fdf281b0e6a3b5e87d22e7db)),pk_k(03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe)) 82012088a8209267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed28764512102c44d12c7065d812e8acf28d7cbb19f9011ecd9e9fdf281b0e6a3b5e87d22e7db51af682103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbeac 40 | c:and_v(or_c(multi(2,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00,02352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d5),v:ripemd160(1b0f3c404d12075c68c938f9f60ebea4f74941a0)),pk_k(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)) 5221036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a002102352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d552ae6482012088a6141b0f3c404d12075c68c938f9f60ebea4f74941a088682103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556ac 41 | and_v(andor(hash256(8a35d9ca92a48eaade6f53a64985e9e2afeb74dcf8acb4c3721e0dc7e4294b25),v:hash256(939894f70e6c3a25da75da0cc2071b4076d9b006563cf635986ada2e93c0d735),v:older(50000)),after(499999999)) 82012088aa208a35d9ca92a48eaade6f53a64985e9e2afeb74dcf8acb4c3721e0dc7e4294b2587640350c300b2696782012088aa20939894f70e6c3a25da75da0cc2071b4076d9b006563cf635986ada2e93c0d735886804ff64cd1db1 42 | andor(hash256(5f8d30e655a7ba0d7596bb3ddfb1d2d20390d23b1845000e1e118b3be1b3f040),j:and_v(v:hash160(3a2bff0da9d96868e66abc4427bea4691cf61ccd),older(4194305)),ripemd160(44d90e2d3714c8663b632fcf0f9d5f22192cc4c8)) 82012088aa205f8d30e655a7ba0d7596bb3ddfb1d2d20390d23b1845000e1e118b3be1b3f040876482012088a61444d90e2d3714c8663b632fcf0f9d5f22192cc4c8876782926382012088a9143a2bff0da9d96868e66abc4427bea4691cf61ccd8803010040b26868 43 | or_i(c:and_v(v:after(500000),pk_k(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)),sha256(d9147961436944f43cd99d28b2bbddbf452ef872b30c8279e255e7daafc7f946)) 630320a107b1692102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ac6782012088a820d9147961436944f43cd99d28b2bbddbf452ef872b30c8279e255e7daafc7f9468768 44 | thresh(2,c:pk_h(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc),s:sha256(e38990d0c7fc009880a9c07c23842e886c6bbdc964ce6bdd5817ad357335ee6f),a:hash160(dd69735817e0e3f6f826a9238dc2e291184f0131)) 76a9145dedfbf9ea599dd4e3ca6a80b333c472fd0b3f6988ac7c82012088a820e38990d0c7fc009880a9c07c23842e886c6bbdc964ce6bdd5817ad357335ee6f87936b82012088a914dd69735817e0e3f6f826a9238dc2e291184f0131876c935287 45 | and_n(sha256(9267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2),uc:and_v(v:older(144),pk_k(03fe72c435413d33d48ac09c9161ba8b09683215439d62b7940502bda8b202e6ce))) 82012088a8209267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed28764006763029000b2692103fe72c435413d33d48ac09c9161ba8b09683215439d62b7940502bda8b202e6ceac67006868 46 | and_n(c:pk_k(03daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729),and_b(l:older(4252898),a:older(16))) 2103daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729ac64006763006703e2e440b2686b60b26c9a68 47 | c:or_i(and_v(v:older(16),pk_h(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)),pk_h(026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4)) 6360b26976a9149fc5dbe5efdce10374a4dd4053c93af540211718886776a9142fbd32c8dd59ee7c17e66cb6ebea7e9846c3040f8868ac 48 | or_d(c:pk_h(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),andor(c:pk_k(024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97),older(2016),after(1567547623))) 76a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac736421024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97ac6404e7e06e5db16702e007b26868 49 | c:andor(ripemd160(6ad07d21fd5dfc646f0b30577045ce201616b9ba),pk_h(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e),and_v(v:hash256(8a35d9ca92a48eaade6f53a64985e9e2afeb74dcf8acb4c3721e0dc7e4294b25),pk_h(03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))) 82012088a6146ad07d21fd5dfc646f0b30577045ce201616b9ba876482012088aa208a35d9ca92a48eaade6f53a64985e9e2afeb74dcf8acb4c3721e0dc7e4294b258876a914dd100be7d9aea5721158ebde6d6a1fd8fff93bb1886776a9149fc5dbe5efdce10374a4dd4053c93af5402117188868ac 50 | c:andor(u:ripemd160(6ad07d21fd5dfc646f0b30577045ce201616b9ba),pk_h(03daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729),or_i(pk_h(022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01),pk_h(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798))) 6382012088a6146ad07d21fd5dfc646f0b30577045ce201616b9ba87670068646376a9149652d86bedf43ad264362e6e6eba6eb764508127886776a914751e76e8199196d454941c45d1b3a323f1433bd688686776a91420d637c1a6404d2227f3561fdbaff5a680dba6488868ac 51 | c:or_i(andor(c:pk_h(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),pk_h(022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01),pk_h(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)),pk_k(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)) 6376a914fcd35ddacad9f2d5be5e464639441c6065e6955d88ac6476a91406afd46bcdfd22ef94ac122aa11f241244a37ecc886776a9149652d86bedf43ad264362e6e6eba6eb7645081278868672102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e68ac 52 | thresh(1,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(1000000000),altv:after(100)) 2103d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b6300670400ca9a3bb16951686c936b6300670164b16951686c935187 53 | thresh(2,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),ac:pk_k(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),altv:after(1000000000),altv:after(100)) 2103d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b2103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556ac6c936b6300670400ca9a3bb16951686c936b6300670164b16951686c935287 54 | pk(028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa) 21028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caaac 55 | multi(3,028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa,03ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e2,039729247032c0dfcf45b4841fcd72f6e9a2422631fc3466cf863e87154754dd40,032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa,0289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff) 5321028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa2103ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e221039729247032c0dfcf45b4841fcd72f6e9a2422631fc3466cf863e87154754dd4021032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa210289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff55ae 56 | or_d(multi(2,028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa,03ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e2),and_v(v:multi(2,032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa,0289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff),older(10000))) 5221028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa2103ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e252ae73645221032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa210289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff52af021027b268 57 | older(921) 029903b2 58 | sha256(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) 82012088a820e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85587 59 | multi(3,028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa,03ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e2,039729247032c0dfcf45b4841fcd72f6e9a2422631fc3466cf863e87154754dd40,032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa,0289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff) 5321028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa2103ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e221039729247032c0dfcf45b4841fcd72f6e9a2422631fc3466cf863e87154754dd4021032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa210289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff55ae 60 | t:and_v(vu:hash256(131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b),v:sha256(ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)) 6382012088aa20131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b876700686982012088a820ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc58851 61 | and_n(pk(03daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729),and_b(l:older(4252898),a:older(16))) 2103daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729ac64006763006703e2e440b2686b60b26c9a68 62 | t:andor(multi(3,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556,02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),v:older(4194305),v:sha256(9267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2)) 532102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e2103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a14602975562102e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1353ae6482012088a8209267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2886703010040b2696851 63 | t:and_v(vu:hash256(131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b),v:sha256(ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)) 6382012088aa20131772552c01444cd81360818376a040b7c3b2b7b0a53550ee3edde216cec61b876700686982012088a820ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc58851 64 | tv:thresh(1,pk(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)) 2102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080eac518851 65 | t:or_c(pk(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e),and_v(v:pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),or_c(pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),v:hash160(131772552c01444cd81360818376a040b7c3b2b7)))) 2102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080eac642103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556ad2103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556ac6482012088a914131772552c01444cd81360818376a040b7c3b2b788686851 66 | -------------------------------------------------------------------------------- /bip380/miniscript/satisfaction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miniscript satisfaction. 3 | 4 | This module contains logic for "signing for" a Miniscript (constructing a valid witness 5 | that meets the conditions set by the Script) and analysis of such satisfaction(s) (eg the 6 | maximum cost in a given resource). 7 | This is currently focused on non-malleable satisfaction. We take shortcuts to not care about 8 | non-canonical (dis)satisfactions. 9 | """ 10 | 11 | 12 | def add_optional(a, b): 13 | """Add two numbers that may be None together.""" 14 | if a is None or b is None: 15 | return None 16 | return a + b 17 | 18 | 19 | def max_optional(a, b): 20 | """Return the maximum of two numbers that may be None.""" 21 | if a is None: 22 | return b 23 | if b is None: 24 | return a 25 | return max(a, b) 26 | 27 | 28 | class SatisfactionMaterial: 29 | """Data that may be needed in order to satisfy a Minsicript fragment.""" 30 | 31 | def __init__( 32 | self, preimages={}, signatures={}, max_sequence=2 ** 32, max_lock_time=2 ** 32 33 | ): 34 | """ 35 | :param preimages: Mapping from a hash (as bytes), to its 32-bytes preimage. 36 | :param signatures: Mapping from a public key (as bytes), to a signature for this key. 37 | :param max_sequence: The maximum relative timelock possible (coin age). 38 | :param max_lock_time: The maximum absolute timelock possible (block height). 39 | """ 40 | self.preimages = preimages 41 | self.signatures = signatures 42 | self.max_sequence = max_sequence 43 | self.max_lock_time = max_lock_time 44 | 45 | def clear(self): 46 | self.preimages.clear() 47 | self.signatures.clear() 48 | self.max_sequence = 0 49 | self.max_lock_time = 0 50 | 51 | def __repr__(self): 52 | return ( 53 | f"SatisfactionMaterial(preimages: {self.preimages}, signatures: " 54 | f"{self.signatures}, max_sequence: {self.max_sequence}, max_lock_time: " 55 | f"{self.max_lock_time}" 56 | ) 57 | 58 | 59 | class Satisfaction: 60 | """All information about a satisfaction.""" 61 | 62 | def __init__(self, witness, has_sig=False): 63 | assert isinstance(witness, list) or witness is None 64 | self.witness = witness 65 | self.has_sig = has_sig 66 | # TODO: we probably need to take into account non-canon sats, as the algorithm 67 | # described on the website mandates it: 68 | # > Iterate over all the valid satisfactions/dissatisfactions in the table above 69 | # > (including the non-canonical ones), 70 | 71 | def __add__(self, other): 72 | """Concatenate two satisfactions together.""" 73 | witness = add_optional(self.witness, other.witness) 74 | has_sig = self.has_sig or other.has_sig 75 | return Satisfaction(witness, has_sig) 76 | 77 | def __or__(self, other): 78 | """Choose between two (dis)satisfactions.""" 79 | assert isinstance(other, Satisfaction) 80 | 81 | # If one isn't available, return the other one. 82 | if self.witness is None: 83 | return other 84 | if other.witness is None: 85 | return self 86 | 87 | # > If among all valid solutions (including DONTUSE ones) more than one does not 88 | # > have the HASSIG marker, return DONTUSE, as this is malleable because of reason 89 | # > 1. 90 | # TODO 91 | # if not (self.has_sig or other.has_sig): 92 | # return Satisfaction.unavailable() 93 | 94 | # > If instead exactly one does not have the HASSIG marker, return that solution 95 | # > because of reason 2. 96 | if self.has_sig and not other.has_sig: 97 | return other 98 | if not self.has_sig and other.has_sig: 99 | return self 100 | 101 | # > Otherwise, all not-DONTUSE options are valid, so return the smallest one (in 102 | # > terms of witness size). 103 | if self.size() > other.size(): 104 | return other 105 | 106 | # > If all valid solutions have the HASSIG marker, but all of them are DONTUSE, return DONTUSE-HASSIG. 107 | # TODO 108 | 109 | return self 110 | 111 | def unavailable(): 112 | return Satisfaction(witness=None) 113 | 114 | def is_unavailable(self): 115 | return self.witness is None 116 | 117 | def size(self): 118 | return len(self.witness) + sum(len(elem) for elem in self.witness) 119 | 120 | def from_concat(sat_material, sub_a, sub_b, disjunction=False): 121 | """Get the satisfaction for a Miniscript whose Script corresponds to a 122 | concatenation of two subscripts A and B. 123 | 124 | :param sub_a: The sub-fragment A. 125 | :param sub_b: The sub-fragment B. 126 | :param disjunction: Whether this fragment has an 'or()' semantic. 127 | """ 128 | if disjunction: 129 | return (sub_b.dissatisfaction() + sub_a.satisfaction(sat_material)) | ( 130 | sub_b.satisfaction(sat_material) + sub_a.dissatisfaction() 131 | ) 132 | return sub_b.satisfaction(sat_material) + sub_a.satisfaction(sat_material) 133 | 134 | def from_or_uneven(sat_material, sub_a, sub_b): 135 | """Get the satisfaction for a Miniscript which unconditionally executes a first 136 | sub A and only executes B if A was dissatisfied. 137 | 138 | :param sub_a: The sub-fragment A. 139 | :param sub_b: The sub-fragment B. 140 | """ 141 | return sub_a.satisfaction(sat_material) | ( 142 | sub_b.satisfaction(sat_material) + sub_a.dissatisfaction() 143 | ) 144 | 145 | def from_thresh(sat_material, k, subs): 146 | """Get the satisfaction for a Miniscript which satisfies k of the given subs, 147 | and dissatisfies all the others. 148 | 149 | :param sat_material: The material to satisfy the challenges. 150 | :param k: The number of subs that need to be satisfied. 151 | :param subs: The list of all subs of the threshold. 152 | """ 153 | # Pick the k sub-fragments to satisfy, prefering (in order): 154 | # 1. Fragments that don't require a signature to be satisfied 155 | # 2. Fragments whose satisfaction's size is smaller 156 | # Record the unavailable (in either way) ones as we go. 157 | arbitrage, unsatisfiable, undissatisfiable = [], [], [] 158 | for sub in subs: 159 | sat, dissat = sub.satisfaction(sat_material), sub.dissatisfaction() 160 | if sat.witness is None: 161 | unsatisfiable.append(sub) 162 | elif dissat.witness is None: 163 | undissatisfiable.append(sub) 164 | else: 165 | arbitrage.append( 166 | (int(sat.has_sig), len(sat.witness) - len(dissat.witness), sub) 167 | ) 168 | 169 | # If not enough (dis)satisfactions are available, fail. 170 | if len(unsatisfiable) > len(subs) - k or len(undissatisfiable) > k: 171 | return Satisfaction.unavailable() 172 | 173 | # Otherwise, satisfy the k most optimal ones. 174 | arbitrage = sorted(arbitrage, key=lambda x: x[:2]) 175 | optimal_sat = undissatisfiable + [a[2] for a in arbitrage] + unsatisfiable 176 | to_satisfy = set(optimal_sat[:k]) 177 | return sum( 178 | [ 179 | sub.satisfaction(sat_material) 180 | if sub in to_satisfy 181 | else sub.dissatisfaction() 182 | for sub in subs[::-1] 183 | ], 184 | start=Satisfaction(witness=[]), 185 | ) 186 | 187 | 188 | # TODO: under Taproot we can also hit the maximum stack size. 189 | class ExecutionInfo: 190 | """Information about the execution of a Miniscript.""" 191 | 192 | def __init__(self, stat_ops, _dyn_ops, sat_size, dissat_size): 193 | # The *maximum* number of *always* executed non-PUSH Script OPs to satisfy this 194 | # Miniscript fragment non-malleably. 195 | self._static_ops_count = stat_ops 196 | # The maximum possible number of counted-as-executed-by-interpreter OPs if this 197 | # fragment is executed. 198 | # It is only >0 for an executed multi() branch. That is, for a CHECKMULTISIG that 199 | # is not part of an unexecuted branch of an IF .. ENDIF. 200 | self._dyn_ops_count = _dyn_ops 201 | # The *maximum* number of stack elements to satisfy this Miniscript fragment 202 | # non-malleably. 203 | self.sat_elems = sat_size 204 | # The *maximum* number of stack elements to dissatisfy this Miniscript fragment 205 | # non-malleably. 206 | self.dissat_elems = dissat_size 207 | 208 | @property 209 | def ops_count(self): 210 | """ 211 | The worst-case number of OPs that would be considered executed by the Script 212 | interpreter. 213 | Note it is considered alone and not necessarily coherent with the other maxima. 214 | """ 215 | return self._static_ops_count + self._dyn_ops_count 216 | 217 | def is_dissatisfiable(self): 218 | """Whether the Miniscript is *non-malleably* dissatisfiable.""" 219 | return self.dissat_elems is not None 220 | 221 | def set_undissatisfiable(self): 222 | """Set the Miniscript as being impossible to dissatisfy.""" 223 | self.dissat_elems = None 224 | 225 | def from_concat(sub_a, sub_b, ops_count=0, disjunction=False): 226 | """Compute the execution info from a Miniscript whose Script corresponds to 227 | a concatenation of two subscript A and B. 228 | 229 | :param sub_a: The execution information of the subscript A. 230 | :param sub_b: The execution information of the subscript B. 231 | :param ops_count: The added number of static OPs added on top. 232 | :param disjunction: Whether this fragment has an 'or()' semantic. 233 | """ 234 | # Number of static OPs is simple, they are all executed. 235 | static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count 236 | # Same for the dynamic ones, there is no conditional branch here. 237 | dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count 238 | # If this is an 'or', only one needs to be satisfied. Pick the most expensive 239 | # satisfaction/dissatisfaction pair. 240 | # If not, both need to be anyways. 241 | if disjunction: 242 | first = add_optional(sub_a.sat_elems, sub_b.dissat_elems) 243 | second = add_optional(sub_a.dissat_elems, sub_b.sat_elems) 244 | sat_elems = max_optional(first, second) 245 | else: 246 | sat_elems = add_optional(sub_a.sat_elems, sub_b.sat_elems) 247 | # In any case dissatisfying the fragment requires dissatisfying both concatenated 248 | # subs. 249 | dissat_elems = add_optional(sub_a.dissat_elems, sub_b.dissat_elems) 250 | 251 | return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) 252 | 253 | def from_or_uneven(sub_a, sub_b, ops_count=0): 254 | """Compute the execution info from a Miniscript which always executes A and only 255 | executes B depending on the outcome of A's execution. 256 | 257 | :param sub_a: The execution information of the subscript A. 258 | :param sub_b: The execution information of the subscript B. 259 | :param ops_count: The added number of static OPs added on top. 260 | """ 261 | # Number of static OPs is simple, they are all executed. 262 | static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count 263 | # If the first sub is non-malleably dissatisfiable, the worst case is executing 264 | # both. Otherwise it is necessarily satisfying only the first one. 265 | if sub_a.is_dissatisfiable(): 266 | dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count 267 | else: 268 | dyn_ops = sub_a._dyn_ops_count 269 | # Either we satisfy A, or satisfy B (and thereby dissatisfy A). Pick the most 270 | # expensive. 271 | first = sub_a.sat_elems 272 | second = add_optional(sub_a.dissat_elems, sub_b.sat_elems) 273 | sat_elems = max_optional(first, second) 274 | # We only take canonical dissatisfactions into account. 275 | dissat_elems = add_optional(sub_a.dissat_elems, sub_b.dissat_elems) 276 | 277 | return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) 278 | 279 | def from_or_even(sub_a, sub_b, ops_count): 280 | """Compute the execution info from a Miniscript which executes either A or B, but 281 | never both. 282 | 283 | :param sub_a: The execution information of the subscript A. 284 | :param sub_b: The execution information of the subscript B. 285 | :param ops_count: The added number of static OPs added on top. 286 | """ 287 | # Number of static OPs is simple, they are all executed. 288 | static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count 289 | # Only one of the branch is executed, pick the most expensive one. 290 | dyn_ops = max(sub_a._dyn_ops_count, sub_b._dyn_ops_count) 291 | # Same. Also, we add a stack element used to tell which branch to take. 292 | sat_elems = add_optional(max_optional(sub_a.sat_elems, sub_b.sat_elems), 1) 293 | # Same here. 294 | dissat_elems = add_optional( 295 | max_optional(sub_a.dissat_elems, sub_b.dissat_elems), 1 296 | ) 297 | 298 | return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) 299 | 300 | def from_andor_uneven(sub_a, sub_b, sub_c, ops_count=0): 301 | """Compute the execution info from a Miniscript which always executes A, and then 302 | executes B if A returned True else executes C. Semantic: or(and(A,B), C). 303 | 304 | :param sub_a: The execution information of the subscript A. 305 | :param sub_b: The execution information of the subscript B. 306 | :param sub_b: The execution information of the subscript C. 307 | :param ops_count: The added number of static OPs added on top. 308 | """ 309 | # Number of static OPs is simple, they are all executed. 310 | static_ops = ( 311 | sum(sub._static_ops_count for sub in [sub_a, sub_b, sub_c]) + ops_count 312 | ) 313 | # If the first sub is non-malleably dissatisfiable, the worst case is executing 314 | # it and the most expensive between B and C. 315 | # If it isn't the worst case is then necessarily to execute A and B. 316 | if sub_a.is_dissatisfiable(): 317 | dyn_ops = sub_a._dyn_ops_count + max( 318 | sub_b._dyn_ops_count, sub_c._dyn_ops_count 319 | ) 320 | else: 321 | # If the first isn't non-malleably dissatisfiable, the worst case is 322 | # satisfying it (and necessarily satisfying the second one too) 323 | dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count 324 | # Same for the number of stack elements (implicit from None here). 325 | first = add_optional(sub_a.sat_elems, sub_b.sat_elems) 326 | second = add_optional(sub_a.dissat_elems, sub_c.sat_elems) 327 | sat_elems = max_optional(first, second) 328 | # The only canonical dissatisfaction is dissatisfying A and C. 329 | dissat_elems = add_optional(sub_a.dissat_elems, sub_c.dissat_elems) 330 | 331 | return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) 332 | 333 | # TODO: i think it'd be possible to not have this be special-cased to 'thresh()' 334 | def from_thresh(k, subs): 335 | """Compute the execution info from a Miniscript 'thresh()' fragment. Specialized 336 | to this specifc fragment for now. 337 | 338 | :param k: The actual threshold of the 'thresh()' fragment. 339 | :param subs: All the possible sub scripts. 340 | """ 341 | # All the OPs from the subs + n-1 * OP_ADD + 1 * OP_EQUAL 342 | static_ops = sum(sub._static_ops_count for sub in subs) + len(subs) 343 | # dyn_ops = sum(sorted([sub._dyn_ops_count for sub in subs], reverse=True)[:k]) 344 | # All subs are executed, there is no OP_IF branch. 345 | dyn_ops = sum([sub._dyn_ops_count for sub in subs]) 346 | 347 | # In order to estimate the worst case we simulate to satisfy the k subs whose 348 | # sat/dissat ratio is the largest, and dissatisfy the others. 349 | # We do so by iterating through all the subs, recording their sat-dissat "score" 350 | # and those that either cannot be satisfied or dissatisfied. 351 | arbitrage, unsatisfiable, undissatisfiable = [], [], [] 352 | for sub in subs: 353 | if sub.sat_elems is None: 354 | unsatisfiable.append(sub) 355 | elif sub.dissat_elems is None: 356 | undissatisfiable.append(sub) 357 | else: 358 | arbitrage.append((sub.sat_elems - sub.dissat_elems, sub)) 359 | # Of course, if too many can't be (dis)satisfied, we have a problem. 360 | # Otherwise, simulate satisfying first the subs that must be (no dissatisfaction) 361 | # then the most expensive ones, and then dissatisfy all the others. 362 | if len(unsatisfiable) > len(subs) - k or len(undissatisfiable) > k: 363 | sat_elems = None 364 | else: 365 | arbitrage = sorted(arbitrage, key=lambda x: x[0], reverse=True) 366 | worst_sat = undissatisfiable + [a[1] for a in arbitrage] + unsatisfiable 367 | sat_elems = sum( 368 | [sub.sat_elems for sub in worst_sat[:k]] 369 | + [sub.dissat_elems for sub in worst_sat[k:]] 370 | ) 371 | if len(undissatisfiable) > 0: 372 | dissat_elems = None 373 | else: 374 | dissat_elems = sum([sub.dissat_elems for sub in subs]) 375 | 376 | return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) 377 | 378 | def from_wrap(sub, ops_count, dyn=0, sat=0, dissat=0): 379 | """Compute the execution info from a Miniscript which always executes a subscript 380 | but adds some logic around. 381 | 382 | :param sub: The execution information of the single subscript. 383 | :param ops_count: The added number of static OPs added on top. 384 | :param dyn: The added number of dynamic OPs added on top. 385 | :param sat: The added number of satisfaction stack elements added on top. 386 | :param dissat: The added number of dissatisfcation stack elements added on top. 387 | """ 388 | return ExecutionInfo( 389 | sub._static_ops_count + ops_count, 390 | sub._dyn_ops_count + dyn, 391 | add_optional(sub.sat_elems, sat), 392 | add_optional(sub.dissat_elems, dissat), 393 | ) 394 | 395 | def from_wrap_dissat(sub, ops_count, dyn=0, sat=0, dissat=0): 396 | """Compute the execution info from a Miniscript which always executes a subscript 397 | but adds some logic around. 398 | 399 | :param sub: The execution information of the single subscript. 400 | :param ops_count: The added number of static OPs added on top. 401 | :param dyn: The added number of dynamic OPs added on top. 402 | :param sat: The added number of satisfaction stack elements added on top. 403 | :param dissat: The added number of dissatisfcation stack elements added on top. 404 | """ 405 | return ExecutionInfo( 406 | sub._static_ops_count + ops_count, 407 | sub._dyn_ops_count + dyn, 408 | add_optional(sub.sat_elems, sat), 409 | dissat, 410 | ) 411 | -------------------------------------------------------------------------------- /bip380/miniscript/parsing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities to parse Miniscript from string and Script representations. 3 | """ 4 | 5 | import bip380.miniscript.fragments as fragments 6 | 7 | from bip380.key import DescriptorKey 8 | from bip380.miniscript.errors import MiniscriptMalformed 9 | from bip380.utils.script import ( 10 | CScriptOp, 11 | OP_ADD, 12 | OP_BOOLAND, 13 | OP_BOOLOR, 14 | OP_CHECKSIGADD, 15 | OP_CHECKSIGVERIFY, 16 | OP_CHECKMULTISIGVERIFY, 17 | OP_EQUALVERIFY, 18 | OP_DUP, 19 | OP_ELSE, 20 | OP_ENDIF, 21 | OP_EQUAL, 22 | OP_FROMALTSTACK, 23 | OP_IFDUP, 24 | OP_IF, 25 | OP_CHECKLOCKTIMEVERIFY, 26 | OP_CHECKMULTISIG, 27 | OP_CHECKSEQUENCEVERIFY, 28 | OP_CHECKSIG, 29 | OP_HASH160, 30 | OP_HASH256, 31 | OP_NOTIF, 32 | OP_NUMEQUAL, 33 | OP_RIPEMD160, 34 | OP_SHA256, 35 | OP_SIZE, 36 | OP_SWAP, 37 | OP_TOALTSTACK, 38 | OP_VERIFY, 39 | OP_0NOTEQUAL, 40 | ScriptNumError, 41 | read_script_number, 42 | ) 43 | 44 | 45 | def stack_item_to_int(item): 46 | """ 47 | Convert a stack item to an integer depending on its type. 48 | May raise an exception if the item is bytes, otherwise return None if it 49 | cannot perform the conversion. 50 | """ 51 | if isinstance(item, bytes): 52 | return read_script_number(item) 53 | 54 | if isinstance(item, fragments.Node): 55 | if isinstance(item, fragments.Just1): 56 | return 1 57 | if isinstance(item, fragments.Just0): 58 | return 0 59 | 60 | if isinstance(item, int): 61 | return item 62 | 63 | return None 64 | 65 | 66 | def decompose_script(script): 67 | """Create a list of Script element from a CScript, decomposing the compact 68 | -VERIFY opcodes into the non-VERIFY OP and an OP_VERIFY. 69 | """ 70 | elems = [] 71 | for elem in script: 72 | if elem == OP_CHECKSIGVERIFY: 73 | elems += [OP_CHECKSIG, OP_VERIFY] 74 | elif elem == OP_CHECKMULTISIGVERIFY: 75 | elems += [OP_CHECKMULTISIG, OP_VERIFY] 76 | elif elem == OP_EQUALVERIFY: 77 | elems += [OP_EQUAL, OP_VERIFY] 78 | else: 79 | elems.append(elem) 80 | return elems 81 | 82 | 83 | def parse_term_single_elem(expr_list, idx, is_taproot): 84 | """ 85 | Try to parse a terminal node from the element of {expr_list} at {idx}. 86 | """ 87 | # Match against pk_k(key). 88 | if ( 89 | isinstance(expr_list[idx], bytes) 90 | and len(expr_list[idx]) == 33 91 | and expr_list[idx][0] in [2, 3] 92 | ): 93 | expr_list[idx] = fragments.Pk(expr_list[idx], is_taproot) 94 | 95 | # Match against JUST_1 and JUST_0. 96 | if expr_list[idx] == 1: 97 | expr_list[idx] = fragments.Just1() 98 | if expr_list[idx] == b"": 99 | expr_list[idx] = fragments.Just0() 100 | 101 | 102 | def parse_term_2_elems(expr_list, idx): 103 | """ 104 | Try to parse a terminal node from two elements of {expr_list}, starting 105 | from {idx}. 106 | Return the new expression list on success, None if there was no match. 107 | """ 108 | elem_a = expr_list[idx] 109 | elem_b = expr_list[idx + 1] 110 | 111 | # Only older() and after() as term with 2 stack items 112 | if not isinstance(elem_b, CScriptOp): 113 | return 114 | try: 115 | n = stack_item_to_int(elem_a) 116 | if n is None: 117 | return 118 | except ScriptNumError: 119 | return 120 | 121 | if n <= 0 or n >= 2 ** 31: 122 | return 123 | 124 | if elem_b == OP_CHECKSEQUENCEVERIFY: 125 | node = fragments.Older(n) 126 | expr_list[idx : idx + 2] = [node] 127 | return expr_list 128 | 129 | if elem_b == OP_CHECKLOCKTIMEVERIFY: 130 | node = fragments.After(n) 131 | expr_list[idx : idx + 2] = [node] 132 | return expr_list 133 | 134 | 135 | def parse_term_5_elems(expr_list, idx, pkh_preimages={}): 136 | """ 137 | Try to parse a terminal node from five elements of {expr_list}, starting 138 | from {idx}. 139 | Return the new expression list on success, None if there was no match. 140 | """ 141 | # The only 3 items node is pk_h 142 | if expr_list[idx : idx + 2] != [OP_DUP, OP_HASH160]: 143 | return 144 | if not isinstance(expr_list[idx + 2], bytes): 145 | return 146 | if len(expr_list[idx + 2]) != 20: 147 | return 148 | if expr_list[idx + 3 : idx + 5] != [OP_EQUAL, OP_VERIFY]: 149 | return 150 | 151 | key_hash = expr_list[idx + 2] 152 | key = pkh_preimages.get(key_hash) 153 | assert key is not None # TODO: have a real error here 154 | node = fragments.Pkh(key, is_taproot) 155 | expr_list[idx : idx + 5] = [node] 156 | return expr_list 157 | 158 | 159 | def parse_term_7_elems(expr_list, idx): 160 | """ 161 | Try to parse a terminal node from seven elements of {expr_list}, starting 162 | from {idx}. 163 | Return the new expression list on success, None if there was no match. 164 | """ 165 | # Note how all the hashes are 7 elems because the VERIFY was decomposed 166 | # Match against sha256. 167 | if ( 168 | expr_list[idx : idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_SHA256] 169 | and isinstance(expr_list[idx + 5], bytes) 170 | and len(expr_list[idx + 5]) == 32 171 | and expr_list[idx + 6] == OP_EQUAL 172 | ): 173 | node = fragments.Sha256(expr_list[idx + 5]) 174 | expr_list[idx : idx + 7] = [node] 175 | return expr_list 176 | 177 | # Match against hash256. 178 | if ( 179 | expr_list[idx : idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_HASH256] 180 | and isinstance(expr_list[idx + 5], bytes) 181 | and len(expr_list[idx + 5]) == 32 182 | and expr_list[idx + 6] == OP_EQUAL 183 | ): 184 | node = fragments.Hash256(expr_list[idx + 5]) 185 | expr_list[idx : idx + 7] = [node] 186 | return expr_list 187 | 188 | # Match against ripemd160. 189 | if ( 190 | expr_list[idx : idx + 5] 191 | == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_RIPEMD160] 192 | and isinstance(expr_list[idx + 5], bytes) 193 | and len(expr_list[idx + 5]) == 20 194 | and expr_list[idx + 6] == OP_EQUAL 195 | ): 196 | node = fragments.Ripemd160(expr_list[idx + 5]) 197 | expr_list[idx : idx + 7] = [node] 198 | return expr_list 199 | 200 | # Match against hash160. 201 | if ( 202 | expr_list[idx : idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_HASH160] 203 | and isinstance(expr_list[idx + 5], bytes) 204 | and len(expr_list[idx + 5]) == 20 205 | and expr_list[idx + 6] == OP_EQUAL 206 | ): 207 | node = fragments.Hash160(expr_list[idx + 5]) 208 | expr_list[idx : idx + 7] = [node] 209 | return expr_list 210 | 211 | 212 | def parse_nonterm_2_elems(expr_list, idx): 213 | """ 214 | Try to parse a non-terminal node from two elements of {expr_list}, starting 215 | from {idx}. 216 | Return the new expression list on success, None if there was no match. 217 | """ 218 | elem_a = expr_list[idx] 219 | elem_b = expr_list[idx + 1] 220 | 221 | if isinstance(elem_a, fragments.Node): 222 | # Match against and_v. 223 | if isinstance(elem_b, fragments.Node) and elem_a.p.V and elem_b.p.has_any("BKV"): 224 | # Is it a special case of t: wrapper? 225 | if isinstance(elem_b, fragments.Just1): 226 | node = fragments.WrapT(elem_a) 227 | else: 228 | node = fragments.AndV(elem_a, elem_b) 229 | expr_list[idx : idx + 2] = [node] 230 | return expr_list 231 | 232 | # Match against c wrapper. 233 | if elem_b == OP_CHECKSIG and elem_a.p.K: 234 | node = fragments.WrapC(elem_a) 235 | expr_list[idx : idx + 2] = [node] 236 | return expr_list 237 | 238 | # Match against v wrapper. 239 | if elem_b == OP_VERIFY and elem_a.p.B: 240 | node = fragments.WrapV(elem_a) 241 | expr_list[idx : idx + 2] = [node] 242 | return expr_list 243 | 244 | # Match against n wrapper. 245 | if elem_b == OP_0NOTEQUAL and elem_a.p.B: 246 | node = fragments.WrapN(elem_a) 247 | expr_list[idx : idx + 2] = [node] 248 | return expr_list 249 | 250 | # Match against s wrapper. 251 | if isinstance(elem_b, fragments.Node) and elem_a == OP_SWAP and elem_b.p.has_all("Bo"): 252 | node = fragments.WrapS(elem_b) 253 | expr_list[idx : idx + 2] = [node] 254 | return expr_list 255 | 256 | 257 | def parse_nonterm_3_elems(expr_list, idx, is_taproot): 258 | """ 259 | Try to parse a non-terminal node from *at least* three elements of 260 | {expr_list}, starting from {idx}. 261 | Return the new expression list on success, None if there was no match. 262 | """ 263 | elem_a = expr_list[idx] 264 | elem_b = expr_list[idx + 1] 265 | elem_c = expr_list[idx + 2] 266 | 267 | if isinstance(elem_a, fragments.Node) and isinstance(elem_b, fragments.Node): 268 | # Match against and_b. 269 | if elem_c == OP_BOOLAND and elem_a.p.B and elem_b.p.W: 270 | node = fragments.AndB(elem_a, elem_b) 271 | expr_list[idx : idx + 3] = [node] 272 | return expr_list 273 | 274 | # Match against or_b. 275 | if elem_c == OP_BOOLOR and elem_a.p.has_all("Bd") and elem_b.p.has_all("Wd"): 276 | node = fragments.OrB(elem_a, elem_b) 277 | expr_list[idx : idx + 3] = [node] 278 | return expr_list 279 | 280 | # Match against a wrapper. 281 | if ( 282 | elem_a == OP_TOALTSTACK 283 | and isinstance(elem_b, fragments.Node) 284 | and elem_b.p.B 285 | and elem_c == OP_FROMALTSTACK 286 | ): 287 | node = fragments.WrapA(elem_b) 288 | expr_list[idx : idx + 3] = [node] 289 | return expr_list 290 | 291 | # FIXME: multi is a terminal! 292 | # Match against a multi. 293 | try: 294 | k = stack_item_to_int(expr_list[idx]) 295 | except ScriptNumError: 296 | return 297 | if k is None: 298 | return 299 | # ()* CHECKMULTISIG 300 | if k > len(expr_list[idx + 1 :]) - 2: 301 | return 302 | # Get the keys 303 | keys = [] 304 | i = idx + 1 305 | while idx < len(expr_list) - 2: 306 | if not isinstance(expr_list[i], fragments.Pk): 307 | break 308 | keys.append(expr_list[i].pubkey) 309 | i += 1 310 | if expr_list[i + 1] == OP_CHECKMULTISIG: 311 | if k > len(keys): 312 | return 313 | try: 314 | m = stack_item_to_int(expr_list[i]) 315 | except ScriptNumError: 316 | return 317 | if m is None or m != len(keys): 318 | return 319 | assert not is_taproot, "multi() only available for P2WSH" # TODO: real errors 320 | node = fragments.Multi(k, keys) 321 | expr_list[idx : i + 2] = [node] 322 | return expr_list 323 | 324 | 325 | def parse_nonterm_4_elems(expr_list, idx, is_taproot): 326 | """ 327 | Try to parse a non-terminal node from at least four elements of {expr_list}, 328 | starting from {idx}. 329 | Return the new expression list on success, None if there was no match. 330 | """ 331 | (it_a, it_b, it_c, it_d) = expr_list[idx : idx + 4] 332 | 333 | # Match against thresh. It's of the form [X] ([X] ADD)* k EQUAL 334 | if isinstance(it_a, fragments.Node) and it_a.p.has_all("Bdu"): 335 | subs = [it_a] 336 | # The first matches, now do all the ([X] ADD)s and return 337 | # if a pair is of the form (k, EQUAL). 338 | for i in range(idx + 1, len(expr_list) - 1, 2): 339 | if ( 340 | isinstance(expr_list[i], fragments.Node) 341 | and expr_list[i].p.has_all("Wdu") 342 | and expr_list[i + 1] == OP_ADD 343 | ): 344 | subs.append(expr_list[i]) 345 | continue 346 | elif expr_list[i + 1] == OP_EQUAL: 347 | try: 348 | k = stack_item_to_int(expr_list[i]) 349 | if len(subs) >= k >= 1: 350 | node = fragments.Thresh(k, subs) 351 | expr_list[idx : i + 1 + 1] = [node] 352 | return expr_list 353 | except ScriptNumError: 354 | break 355 | else: 356 | break 357 | 358 | # Match against or_c. 359 | if ( 360 | isinstance(it_a, fragments.Node) 361 | and it_a.p.has_all("Bdu") 362 | and it_b == OP_NOTIF 363 | and isinstance(it_c, fragments.Node) 364 | and it_c.p.V 365 | and it_d == OP_ENDIF 366 | ): 367 | node = fragments.OrC(it_a, it_c) 368 | expr_list[idx : idx + 4] = [node] 369 | return expr_list 370 | 371 | # Match against d wrapper. 372 | if ( 373 | [it_a, it_b] == [OP_DUP, OP_IF] 374 | and isinstance(it_c, fragments.Node) 375 | and it_c.p.has_all("Vz") 376 | and it_d == OP_ENDIF 377 | ): 378 | node = fragments.WrapD(it_c, is_taproot) 379 | expr_list[idx : idx + 4] = [node] 380 | return expr_list 381 | 382 | 383 | def parse_nonterm_5_elems(expr_list, idx): 384 | """ 385 | Try to parse a non-terminal node from five elements of {expr_list}, starting 386 | from {idx}. 387 | Return the new expression list on success, None if there was no match. 388 | """ 389 | (it_a, it_b, it_c, it_d, it_e) = expr_list[idx : idx + 5] 390 | 391 | # Match against or_d. 392 | if ( 393 | isinstance(it_a, fragments.Node) 394 | and it_a.p.has_all("Bdu") 395 | and [it_b, it_c] == [OP_IFDUP, OP_NOTIF] 396 | and isinstance(it_d, fragments.Node) 397 | and it_d.p.B 398 | and it_e == OP_ENDIF 399 | ): 400 | node = fragments.OrD(it_a, it_d) 401 | expr_list[idx : idx + 5] = [node] 402 | return expr_list 403 | 404 | # Match against or_i. 405 | if ( 406 | it_a == OP_IF 407 | and isinstance(it_b, fragments.Node) 408 | and it_b.p.has_any("BKV") 409 | and it_c == OP_ELSE 410 | and isinstance(it_d, fragments.Node) 411 | and it_d.p.has_any("BKV") 412 | and it_e == OP_ENDIF 413 | ): 414 | if isinstance(it_b, fragments.Just0): 415 | node = fragments.WrapL(it_d) 416 | elif isinstance(it_d, fragments.Just0): 417 | node = fragments.WrapU(it_b) 418 | else: 419 | node = fragments.OrI(it_b, it_d) 420 | expr_list[idx : idx + 5] = [node] 421 | return expr_list 422 | 423 | # Match against j wrapper. 424 | if ( 425 | [it_a, it_b, it_c] == [OP_SIZE, OP_0NOTEQUAL, OP_IF] 426 | and isinstance(it_d, fragments.Node) 427 | and it_e == OP_ENDIF 428 | ): 429 | node = fragments.WrapJ(expr_list[idx + 3]) 430 | expr_list[idx : idx + 5] = [node] 431 | return expr_list 432 | 433 | 434 | def parse_nonterm_6_elems(expr_list, idx): 435 | """ 436 | Try to parse a non-terminal node from six elements of {expr_list}, starting 437 | from {idx}. 438 | Return the new expression list on success, None if there was no match. 439 | """ 440 | (it_a, it_b, it_c, it_d, it_e, it_f) = expr_list[idx : idx + 6] 441 | 442 | # Match against andor. 443 | if ( 444 | isinstance(it_a, fragments.Node) 445 | and it_a.p.has_all("Bdu") 446 | and it_b == OP_NOTIF 447 | and isinstance(it_c, fragments.Node) 448 | and it_c.p.has_any("BKV") 449 | and it_d == OP_ELSE 450 | and isinstance(it_e, fragments.Node) 451 | and it_e.p.has_any("BKV") 452 | and it_f == OP_ENDIF 453 | ): 454 | if isinstance(it_c, fragments.Just0): 455 | node = fragments.AndN(it_a, it_e) 456 | else: 457 | node = fragments.AndOr(it_a, it_e, it_c) 458 | expr_list[idx : idx + 6] = [node] 459 | return expr_list 460 | 461 | 462 | def parse_expr_list(expr_list, is_taproot): 463 | """Parse a node from a list of Script elements.""" 464 | # Every recursive call must progress the AST construction, 465 | # until it is complete (single root node remains). 466 | expr_list_len = len(expr_list) 467 | 468 | # Root node reached. 469 | if expr_list_len == 1 and isinstance(expr_list[0], fragments.Node): 470 | return expr_list[0] 471 | 472 | # Step through each list index and match against templates. 473 | idx = expr_list_len - 1 474 | while idx >= 0: 475 | if expr_list_len - idx >= 2: 476 | new_expr_list = parse_nonterm_2_elems(expr_list, idx) 477 | if new_expr_list is not None: 478 | return parse_expr_list(new_expr_list, is_taproot) 479 | 480 | if expr_list_len - idx >= 3: 481 | new_expr_list = parse_nonterm_3_elems(expr_list, idx, is_taproot) 482 | if new_expr_list is not None: 483 | return parse_expr_list(new_expr_list, is_taproot) 484 | 485 | if expr_list_len - idx >= 4: 486 | new_expr_list = parse_nonterm_4_elems(expr_list, idx, is_taproot) 487 | if new_expr_list is not None: 488 | return parse_expr_list(new_expr_list, is_taproot) 489 | 490 | if expr_list_len - idx >= 5: 491 | new_expr_list = parse_nonterm_5_elems(expr_list, idx) 492 | if new_expr_list is not None: 493 | return parse_expr_list(new_expr_list, is_taproot) 494 | 495 | if expr_list_len - idx >= 6: 496 | new_expr_list = parse_nonterm_6_elems(expr_list, idx) 497 | if new_expr_list is not None: 498 | return parse_expr_list(new_expr_list, is_taproot) 499 | 500 | # Right-to-left parsing. 501 | # Step one position left. 502 | idx -= 1 503 | 504 | # No match found. 505 | raise MiniscriptMalformed(f"{expr_list}") 506 | 507 | 508 | def parse_xonly_key(ser_key): 509 | """Parse a public key from bytes. Raises if it wasn't serialized as x-only.""" 510 | 511 | key = DescriptorKey(ser_key, x_only=True) 512 | if not key.ser_x_only: 513 | raise MiniscriptMalformed("Keys must be serialized as 32 bytes in multi_a") 514 | return key 515 | 516 | 517 | def parse_multi_a(expr_list, is_taproot): 518 | """Try to parse a multi_a fragment from a list of at least 4 elements. 519 | 520 | Returns True on success, False otherwise. Modifies the list in place. 521 | """ 522 | assert len(expr_list) >= 4 523 | 524 | # Parse the threshold ( OP_NUMEQUAL) 525 | if expr_list[-1] != OP_NUMEQUAL: 526 | return False 527 | try: 528 | k = stack_item_to_int(expr_list[-2]) 529 | except ScriptNumError: 530 | return False 531 | 532 | # Now parse the second to nth public keys ( CSA CSA ... CSA) 533 | pubkeys = [] 534 | i = 1 535 | while len(expr_list) > 2 + i + 1 and expr_list[-2 - i] == OP_CHECKSIGADD: 536 | pubkeys.append(parse_xonly_key(expr_list[-2 - i - 1])) 537 | i += 2 538 | 539 | # Finally parse the first publick key ( CHECKSIG) 540 | if expr_list[-2 - i] != OP_CHECKSIG: 541 | return False 542 | pubkeys.append(parse_xonly_key(expr_list[-2 - i - 1])) 543 | 544 | # Perform the Taproot check only here so we can be sure they actually used a multi_a. 545 | assert is_taproot, "multi_a() is only available for Taproot" # TODO: real errors 546 | 547 | # Success. Replace the opcodes with the fragment. 548 | expr_list[-2 - i - 1 :] = [fragments.MultiA(k, pubkeys[::-1])] 549 | return True 550 | 551 | 552 | def miniscript_from_script(script, is_taproot, pkh_preimages={}): 553 | """Construct miniscript node from script. 554 | 555 | :param script: The Bitcoin Script to decode. 556 | :param pkh_preimage: A mapping from keyhash to key to decode pk_h() fragments. 557 | """ 558 | assert isinstance(is_taproot, bool) 559 | 560 | expr_list = decompose_script(script) 561 | expr_list_len = len(expr_list) 562 | 563 | # We first parse terminal expressions. 564 | idx = 0 565 | while idx < expr_list_len: 566 | # Try to parse a multi_a fragment. This needs to be done first or the CHECKSIG 567 | # expressions within multi_a fragments would be parsed as pk_k()s! 568 | if expr_list_len - idx >= 4 and parse_multi_a(expr_list, is_taproot): 569 | expr_list_len = len(expr_list) 570 | 571 | parse_term_single_elem(expr_list, idx, is_taproot) 572 | 573 | if expr_list_len - idx >= 2: 574 | new_expr_list = parse_term_2_elems(expr_list, idx) 575 | if new_expr_list is not None: 576 | expr_list = new_expr_list 577 | expr_list_len = len(expr_list) 578 | 579 | if expr_list_len - idx >= 5: 580 | new_expr_list = parse_term_5_elems(expr_list, idx, pkh_preimages) 581 | if new_expr_list is not None: 582 | expr_list = new_expr_list 583 | expr_list_len = len(expr_list) 584 | 585 | if expr_list_len - idx >= 7: 586 | new_expr_list = parse_term_7_elems(expr_list, idx) 587 | if new_expr_list is not None: 588 | expr_list = new_expr_list 589 | expr_list_len = len(expr_list) 590 | 591 | idx += 1 592 | 593 | # fragments.And then recursively parse non-terminal ones. 594 | return parse_expr_list(expr_list, is_taproot) 595 | 596 | 597 | def split_params(string): 598 | """Read a list of values before the next ')'. Split the result by comma.""" 599 | i = string.find(")") 600 | assert i >= 0 601 | 602 | params, remaining = string[:i], string[i:] 603 | if len(remaining) > 0: 604 | return params.split(","), remaining[1:] 605 | else: 606 | return params.split(","), "" 607 | 608 | 609 | def parse_many(string, is_taproot): 610 | """Read a list of nodes before the next ')'.""" 611 | subs = [] 612 | remaining = string 613 | while True: 614 | sub, remaining = parse_one(remaining, is_taproot) 615 | subs.append(sub) 616 | if remaining[0] == ")": 617 | return subs, remaining[1:] 618 | assert remaining[0] == "," # TODO: real errors 619 | remaining = remaining[1:] 620 | 621 | 622 | def parse_one_num(string): 623 | """Read an integer before the next comma.""" 624 | i = string.find(",") 625 | assert i >= 0 626 | 627 | return int(string[:i]), string[i + 1 :] 628 | 629 | 630 | def parse_one(string, is_taproot): 631 | """Read a node and its subs recursively from a string. 632 | Returns the node and the part of the string not consumed. 633 | """ 634 | 635 | # We special case fragments.Just1 and fragments.Just0 since they are the only one which don't 636 | # have a function syntax. 637 | if string[0] == "0": 638 | return fragments.Just0(), string[1:] 639 | if string[0] == "1": 640 | return fragments.Just1(), string[1:] 641 | 642 | # Now, find the separator for all functions. 643 | for i, char in enumerate(string): 644 | if char in ["(", ":"]: 645 | break 646 | # For wrappers, we may have many of them. 647 | if char == ":" and i > 1: 648 | tag, remaining = string[0], string[1:] 649 | else: 650 | tag, remaining = string[:i], string[i + 1 :] 651 | 652 | # fragments.Wrappers 653 | if char == ":": 654 | sub, remaining = parse_one(remaining, is_taproot) 655 | if tag == "a": 656 | return fragments.WrapA(sub), remaining 657 | 658 | if tag == "s": 659 | return fragments.WrapS(sub), remaining 660 | 661 | if tag == "c": 662 | return fragments.WrapC(sub), remaining 663 | 664 | if tag == "t": 665 | return fragments.WrapT(sub), remaining 666 | 667 | if tag == "d": 668 | return fragments.WrapD(sub, is_taproot), remaining 669 | 670 | if tag == "v": 671 | return fragments.WrapV(sub), remaining 672 | 673 | if tag == "j": 674 | return fragments.WrapJ(sub), remaining 675 | 676 | if tag == "n": 677 | return fragments.WrapN(sub), remaining 678 | 679 | if tag == "l": 680 | return fragments.WrapL(sub), remaining 681 | 682 | if tag == "u": 683 | return fragments.WrapU(sub), remaining 684 | 685 | assert False, (tag, sub, remaining) # TODO: real errors 686 | 687 | # Terminal elements other than 0 and 1 688 | if tag in [ 689 | "pk", 690 | "pkh", 691 | "pk_k", 692 | "pk_h", 693 | "sha256", 694 | "hash256", 695 | "ripemd160", 696 | "hash160", 697 | "older", 698 | "after", 699 | "multi", 700 | "multi_a", 701 | ]: 702 | params, remaining = split_params(remaining) 703 | 704 | if tag == "0": 705 | return fragments.Just0(), remaining 706 | 707 | if tag == "1": 708 | return fragments.Just1(), remaining 709 | 710 | if tag == "pk": 711 | return fragments.WrapC(fragments.Pk(params[0], is_taproot)), remaining 712 | 713 | if tag == "pk_k": 714 | return fragments.Pk(params[0], is_taproot), remaining 715 | 716 | if tag == "pkh": 717 | return fragments.WrapC(fragments.Pkh(params[0], is_taproot)), remaining 718 | 719 | if tag == "pk_h": 720 | return fragments.Pkh(params[0], is_taproot), remaining 721 | 722 | if tag == "older": 723 | value = int(params[0]) 724 | return fragments.Older(value), remaining 725 | 726 | if tag == "after": 727 | value = int(params[0]) 728 | return fragments.After(value), remaining 729 | 730 | if tag in ["sha256", "hash256", "ripemd160", "hash160"]: 731 | digest = bytes.fromhex(params[0]) 732 | if tag == "sha256": 733 | return fragments.Sha256(digest), remaining 734 | if tag == "hash256": 735 | return fragments.Hash256(digest), remaining 736 | if tag == "ripemd160": 737 | return fragments.Ripemd160(digest), remaining 738 | return fragments.Hash160(digest), remaining 739 | 740 | if tag == "multi": 741 | assert not is_taproot, "multi() only available for P2WSH" # TODO: real errors 742 | k = int(params.pop(0)) 743 | key_n = [] 744 | for param in params: 745 | key_obj = DescriptorKey(param) 746 | key_n.append(key_obj) 747 | return fragments.Multi(k, key_n), remaining 748 | 749 | if tag == "multi_a": 750 | assert is_taproot, "multi_a() is only available for Taproot" # TODO: real errors 751 | k = int(params.pop(0)) 752 | keys = [DescriptorKey(p, x_only=True) for p in params] 753 | return fragments.MultiA(k, keys), remaining 754 | 755 | assert False, (tag, params, remaining) 756 | 757 | # Non-terminal elements (connectives) 758 | # We special case fragments.Thresh, as its first sub is an integer. 759 | if tag == "thresh": 760 | k, remaining = parse_one_num(remaining) 761 | # TODO: real errors in place of unpacking 762 | subs, remaining = parse_many(remaining, is_taproot) 763 | 764 | if tag == "and_v": 765 | return fragments.AndV(*subs), remaining 766 | 767 | if tag == "and_b": 768 | return fragments.AndB(*subs), remaining 769 | 770 | if tag == "and_n": 771 | return fragments.AndN(*subs), remaining 772 | 773 | if tag == "or_b": 774 | return fragments.OrB(*subs), remaining 775 | 776 | if tag == "or_c": 777 | return fragments.OrC(*subs), remaining 778 | 779 | if tag == "or_d": 780 | return fragments.OrD(*subs), remaining 781 | 782 | if tag == "or_i": 783 | return fragments.OrI(*subs), remaining 784 | 785 | if tag == "andor": 786 | return fragments.AndOr(*subs), remaining 787 | 788 | if tag == "thresh": 789 | return fragments.Thresh(k, subs), remaining 790 | 791 | assert False, (tag, subs, remaining) # TODO 792 | 793 | 794 | def miniscript_from_str(ms_str, is_taproot): 795 | """Construct miniscript node from string representation""" 796 | assert isinstance(is_taproot, bool) 797 | 798 | node, remaining = parse_one(ms_str, is_taproot) 799 | assert remaining == "" 800 | return node 801 | -------------------------------------------------------------------------------- /tests/test_descriptors.py: -------------------------------------------------------------------------------- 1 | import coincurve 2 | import os 3 | import pytest 4 | 5 | from bip32 import BIP32, HARDENED_INDEX 6 | from bitcointx.core import ( 7 | CMutableTxIn, 8 | CMutableTxOut, 9 | CMutableTransaction, 10 | COutPoint, 11 | ) 12 | from bitcointx.core.bitcoinconsensus import ( 13 | ConsensusVerifyScript, 14 | BITCOINCONSENSUS_ACCEPTED_FLAGS, 15 | ) 16 | from bitcointx.core.script import ( 17 | CScript as CScriptBitcoinTx, 18 | CScriptWitness, 19 | RawBitcoinSignatureHash, 20 | SIGVERSION_WITNESS_V0, 21 | ) 22 | from bip380.descriptors import Descriptor 23 | from bip380.key import DescriptorKey, KeyPathKind, DescriptorKeyError 24 | from bip380.miniscript import Node, SatisfactionMaterial, fragments 25 | from bip380.descriptors.errors import DescriptorParsingError 26 | from bip380.utils.hashes import sha256 27 | 28 | 29 | def sign_dummy_tx( 30 | descriptor, 31 | keypairs, 32 | ): 33 | """Create and sign a dummy transaction with the given keys.""" 34 | amount = 10_000 35 | txid = bytes.fromhex( 36 | "652c60ec08280356e8c78be9bf4d44276acef3189ba8223e426b757aeabd66ad" 37 | ) 38 | txin = CMutableTxIn(COutPoint(txid, 0)) 39 | txout = CMutableTxOut(amount - 1_000, descriptor.script_pubkey) 40 | tx = CMutableTransaction([txin], [txout]) 41 | 42 | sighash = RawBitcoinSignatureHash( 43 | script=descriptor.script_sighash, 44 | txTo=tx, 45 | inIdx=0, 46 | hashtype=1, # SIGHASH_ALL 47 | amount=amount, 48 | sigversion=SIGVERSION_WITNESS_V0, 49 | )[0] 50 | signatures = {} 51 | for pubkey, privkey in keypairs.items(): 52 | sig = coincurve.PrivateKey(privkey).sign(sighash, hasher=None) 53 | signatures[pubkey] = sig + b"\x01" # SIGHASH_ALL 54 | 55 | return tx, signatures, amount 56 | 57 | 58 | def verify_tx(descriptor, tx, witness_stack, amount): 59 | """Test a transaction's first input spending a given descriptor against libbitcoinconsensus.""" 60 | ConsensusVerifyScript( 61 | scriptSig=tx.vin[0].scriptSig, 62 | scriptPubKey=CScriptBitcoinTx(iter(descriptor.script_pubkey)), 63 | txTo=tx, 64 | inIdx=0, 65 | amount=amount, 66 | witness=CScriptWitness(witness_stack), 67 | # NOTE: that's missing Taproot flags 68 | flags=BITCOINCONSENSUS_ACCEPTED_FLAGS, 69 | ) 70 | 71 | 72 | def roundtrip_desc(desc_str): 73 | desc = Descriptor.from_str(desc_str) 74 | assert str(desc) == desc_str 75 | return desc 76 | 77 | 78 | def test_wsh_sanity_checks(): 79 | """Sanity check we can parse a wsh descriptor and satisfy it.""" 80 | hd = BIP32.from_seed(os.urandom(32)) 81 | pubkey, privkey = hd.get_pubkey_from_path("m"), hd.get_privkey_from_path("m") 82 | preimage = os.urandom(32) 83 | digest = sha256(preimage) 84 | 85 | desc_str = f"wsh(and_b(pk({pubkey.hex()}),a:sha256({digest.hex()})))" 86 | desc = Descriptor.from_str(desc_str) 87 | 88 | sat_material = SatisfactionMaterial(preimages={digest: preimage}) 89 | tx, signatures, amount = sign_dummy_tx(desc, keypairs={pubkey: privkey}) 90 | sat_material.signatures = signatures 91 | 92 | stack = desc.satisfy(sat_material) 93 | verify_tx(desc, tx, stack, amount) 94 | 95 | 96 | def test_wpkh_sanity_checks(): 97 | """Sanity check we can parse a wpkh descriptor and satisfy it.""" 98 | hd = BIP32.from_seed(os.urandom(32)) 99 | pubkey, privkey = hd.get_pubkey_from_path("m"), hd.get_privkey_from_path("m") 100 | 101 | desc_str = f"wpkh({pubkey.hex()})" 102 | desc = Descriptor.from_str(desc_str) 103 | 104 | tx, signatures, amount = sign_dummy_tx(desc, keypairs={pubkey: privkey}) 105 | stack = desc.satisfy(list(signatures.values())[0]) 106 | verify_tx(desc, tx, stack, amount) 107 | 108 | 109 | def test_key_parsing(): 110 | """Roundtrip keys with various metadata.""" 111 | keys = [ 112 | "[aabbccdd]xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa", 113 | "[aabbccdd/0/1'/2]xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa", 114 | "xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa/1'/2", 115 | "xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa/145/*", 116 | "[aabbccdd/0/1'/2]xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa/1'/2/*", 117 | "xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa/*'", 118 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>", 119 | "[aabbccdd/0/1'/2]tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>", 120 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10", 121 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*", 122 | "[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*", 123 | "[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1'>/8'/*'", 124 | "02cc24adfed5a481b000192042b2399087437d8eb16095c3dda1d45a4fbf868017", 125 | "[0011bbdd/534/789'/34]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a", 126 | "cc24adfed5a481b000192042b2399087437d8eb16095c3dda1d45a4fbf868017", 127 | "[0011bbdd/534/789'/34]3d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a", 128 | ] 129 | for key in keys: 130 | assert str(DescriptorKey(key)) == key 131 | 132 | tpub = DescriptorKey( 133 | "[abcdef00/0'/1]tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1';420>/8'/*'" 134 | ) 135 | assert tpub.path.paths == [ 136 | [9478 + HARDENED_INDEX, 0 + HARDENED_INDEX, 8 + HARDENED_INDEX], 137 | [9478 + HARDENED_INDEX, 1 + HARDENED_INDEX, 8 + HARDENED_INDEX], 138 | [9478 + HARDENED_INDEX, 420, 8 + HARDENED_INDEX], 139 | ] 140 | assert tpub.path.kind == KeyPathKind.WILDCARD_HARDENED 141 | assert tpub.origin.fingerprint == bytes.fromhex("abcdef00") 142 | assert tpub.origin.path == [0 + HARDENED_INDEX, 1] 143 | 144 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 145 | DescriptorKey( 146 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854" 147 | ) 148 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 149 | DescriptorKey( 150 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/0;1;42;9854>" 151 | ) 152 | with pytest.raises( 153 | DescriptorKeyError, match="May only have a single multipath step" 154 | ): 155 | DescriptorKey( 156 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1>/96/<0;1>" 157 | ) 158 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 159 | DescriptorKey( 160 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0>" 161 | ) 162 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 163 | DescriptorKey( 164 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;>" 165 | ) 166 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 167 | DescriptorKey( 168 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<;1>" 169 | ) 170 | with pytest.raises(DescriptorKeyError, match="Invalid derivation index"): 171 | DescriptorKey( 172 | "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>" 173 | ) 174 | 175 | 176 | def test_descriptor_parsing(): 177 | """Misc descriptor parsing checks.""" 178 | # Without origin 179 | Descriptor.from_str( 180 | "wpkh(033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 181 | ) 182 | Descriptor.from_str( 183 | "wpkh(xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu)" 184 | ) 185 | Descriptor.from_str( 186 | "wsh(pkh(033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a))" 187 | ) 188 | Descriptor.from_str( 189 | "wsh(pkh(xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu))" 190 | ) 191 | 192 | # With origin, only fingerprint 193 | Descriptor.from_str( 194 | "wpkh([00aabbcc]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 195 | ) 196 | Descriptor.from_str( 197 | "wpkh([00aabbcc]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu)" 198 | ) 199 | Descriptor.from_str( 200 | "wsh(pkh([00aabbcc]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a))" 201 | ) 202 | Descriptor.from_str( 203 | "wsh(pkh([00aabbcc]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu))" 204 | ) 205 | 206 | # With origin, fingerprint + various derivation paths 207 | desc = Descriptor.from_str( 208 | "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 209 | ) 210 | assert desc.keys[0].origin.fingerprint == bytes.fromhex("00aabbcc") and desc.keys[ 211 | 0 212 | ].origin.path == [0] 213 | desc = Descriptor.from_str( 214 | "wpkh([00aabbcc/0/1']xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu)" 215 | ) 216 | assert desc.keys[0].origin.fingerprint == bytes.fromhex("00aabbcc") and desc.keys[ 217 | 0 218 | ].origin.path == [0, 2 ** 31 + 1] 219 | desc = Descriptor.from_str( 220 | "wsh(pkh([00aabbcc/0h/1]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a))" 221 | ) 222 | assert desc.keys[0].origin.fingerprint == bytes.fromhex("00aabbcc") and desc.keys[ 223 | 0 224 | ].origin.path == [2 ** 31, 1] 225 | desc = Descriptor.from_str( 226 | "wsh(pkh([00aabbcc/108765H/578h/9897'/23]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu))" 227 | ) 228 | assert desc.keys[0].origin.fingerprint == bytes.fromhex("00aabbcc") and desc.keys[ 229 | 0 230 | ].origin.path == [2 ** 31 + 108765, 2 ** 31 + 578, 2 ** 31 + 9897, 23] 231 | 232 | # With checksum 233 | Descriptor.from_str( 234 | "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)#g6gm8u7v" 235 | ) 236 | Descriptor.from_str( 237 | "wpkh([00aabbcc/1]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu)#2h49p59p" 238 | ) 239 | Descriptor.from_str( 240 | "wsh(pkh([00aabbcc/2]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a))#nue4wg6d" 241 | ) 242 | Descriptor.from_str( 243 | "wsh(pkh([00aabbcc/3]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu))#zv3q322g" 244 | ) 245 | 246 | # With key derivation path 247 | desc = Descriptor.from_str( 248 | "wpkh([00aabbcc/1]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu/*)" 249 | ) 250 | assert desc.keys[0].path.paths == [[]] 251 | assert desc.keys[0].path.kind == KeyPathKind.WILDCARD_UNHARDENED 252 | desc = Descriptor.from_str( 253 | "wpkh([00aabbcc/1]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu/0/2/3242/5H/2'/*h)" 254 | ) 255 | assert desc.keys[0].path.paths == [[0, 2, 3242, 5 + 2 ** 31, 2 + 2 ** 31]] 256 | assert desc.keys[0].path.kind == KeyPathKind.WILDCARD_HARDENED 257 | 258 | # Multiple origins 259 | with pytest.raises(DescriptorParsingError): 260 | Descriptor.from_str( 261 | "wpkh([00aabbcc/0][00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 262 | ) 263 | # Too long fingerprint 264 | with pytest.raises(DescriptorParsingError): 265 | Descriptor.from_str( 266 | "wpkh([00aabbccd/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 267 | ) 268 | # Insane deriv path 269 | with pytest.raises(DescriptorParsingError): 270 | Descriptor.from_str( 271 | "wpkh([00aabbcc/0//]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 272 | ) 273 | # Absent checksum while required 274 | with pytest.raises(DescriptorParsingError): 275 | Descriptor.from_str( 276 | "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)", 277 | strict=True, 278 | ) 279 | # Invalid checksum 280 | with pytest.raises(DescriptorParsingError): 281 | Descriptor.from_str( 282 | "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)#2h49p5pp" 283 | ) 284 | # Invalid key path 285 | with pytest.raises(DescriptorParsingError): 286 | Descriptor.from_str( 287 | "wpkh([00aabbcc/1]xpub6BsJ4SAX3CYhcZVV9bFVvmGJ7cyboy4LJqbRJJEziPvm9Pq7v7cWkBAa1LixG9vJybxHDuWcHTtq3K4tsaKG1jMJcpZmkiacFuc7LkzUCWu/0/2//1)" 288 | ) 289 | # Key path for a raw key 290 | with pytest.raises(DescriptorParsingError): 291 | Descriptor.from_str( 292 | "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a/0/1)" 293 | ) 294 | 295 | # Deriving a raw key is a no-op 296 | desc_str = ( 297 | "wpkh(033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 298 | ) 299 | desc, desc2 = Descriptor.from_str(desc_str), Descriptor.from_str(desc_str) 300 | desc2.derive(10) 301 | assert str(desc2) == str(desc) 302 | 303 | # Deriving a raw key is a no-op, even if it has an origin 304 | desc_str = "wpkh([00aabbcc/0]033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a)" 305 | desc, desc2 = Descriptor.from_str(desc_str), Descriptor.from_str(desc_str) 306 | desc2.derive(10) 307 | assert str(desc2) == str(desc) 308 | 309 | # Deriving an xpub will derive it if it is a wildcard 310 | desc_str = "wsh(pkh(xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa/*))" 311 | desc, desc2 = Descriptor.from_str(desc_str), Descriptor.from_str(desc_str) 312 | desc2.derive(1001) 313 | assert desc2.keys[0].origin.path == [1001] 314 | assert ( 315 | str(desc2).split("#")[0].split("]")[1] 316 | == "xpub68Raazrdpq1a2PhmuPMr59H5eT3axiWPVnbN6t6xJj5YvWRTJhdJr2V9ye7v4VG3yKaPb4qbW2zrHsEHCAzMSUskzNvksL4vtG7DGv12Nj6))" 317 | ) 318 | assert desc2.keys[0].bytes() == bytes.fromhex( 319 | "03c6844a957551c64e780783fc95b1aeeb040d160f84535b4810f932072db12f25" 320 | ) 321 | 322 | # Deriving an xpub will NOT derive it if it was not a wildcard 323 | desc_str = "wsh(pkh(xpub661MyMwAqRbcGC7awXn2f36qPMLE2x42cQM5qHrSRg3Q8X7qbDEG1aKS4XAA1PcWTZn7c4Y2WJKCvcivjpZBXTo8fpCRrxtmNKW4H1rpACa))" 324 | desc, desc2 = Descriptor.from_str(desc_str), Descriptor.from_str(desc_str) 325 | desc2.derive(1001) 326 | assert str(desc2) == str(desc) 327 | 328 | # Test against a Revault deposit descriptor using rust-miniscript 329 | xpub = BIP32.from_xpub( 330 | "tpubD6NzVbkrYhZ4YgUwLbJjHAo4khrBPHJfZ1nzeeWxaTpYHzvM7SaEFLnuWjcRt8aM3LicBzeqVcN4fKsbTzHSkUJn388HSc5Xxpd1tPSmDYQ" 331 | ) 332 | assert ( 333 | xpub.get_pubkey_from_path([0]).hex() 334 | == "02cc24adfed5a481b000192042b2399087437d8eb16095c3dda1d45a4fbf868017" 335 | ) 336 | desc_str = "wsh(multi(5,tpubD6NzVbkrYhZ4YgUwLbJjHAo4khrBPHJfZ1nzeeWxaTpYHzvM7SaEFLnuWjcRt8aM3LicBzeqVcN4fKsbTzHSkUJn388HSc5Xxpd1tPSmDYQ/*,tpubD6NzVbkrYhZ4X9w1pgeFqiDm7o4dkvEku1ibW6frK5n3vWsSGjxoo3DESgwwZW5N8eN72vCywJzmbezhQQHMbpUytZcxYYTAEaQzUntBEtP/*,tpubD6NzVbkrYhZ4X2M619JZbwnPoQ65e5qzosWPtXYMnMtevcQTwVHq6HFbu5whCAp4PpynzrE65MXk2kgqUb22aE2V5NPZJautw8vXDmVMGuz/*,tpubD6NzVbkrYhZ4YNHo23GAaYnfs8xzyhxpaWZsHJ72a9RPwiQd36BtyHnpRSQFYAJMLK2tWb6i7QJcjNuko4b4V3kGyhe6Z4TxZGXJfEvTU12/*,tpubD6NzVbkrYhZ4YdMUbJuBi6mhtYAC53MevdrFtpQicPavbnYDni6YsAD62NUhQxHYYJpAniVk4Ba9Q2GiptSZPz8ugbo3zgecm2aXQRFny4a/*))#339j7vh3" 337 | rust_bitcoin_desc_str = "wsh(multi(5,[7fba6fe6/0]02cc24adfed5a481b000192042b2399087437d8eb16095c3dda1d45a4fbf868017,[d7724f76/0]039b2b68caf451ba88afe617cb57f2e9840511bedb0ac8ffa2dc2b25d4ea84adf1,[0c39ed43/0]03f7c1d37ff5dfd5a8b5326533810cef71f7f724fd53d2a88f49e3c63edc5f9688,[e69af179/0]0296209843f0f4dd7b1f3a072e72e7b4edd2e3ff416afc862a7a7aa0b9d40d2de6,[e42852b6/0]03427930b60ba45aeb5c7e03fc3b6b7b22637bec5d355c55204678d7dd8a029981))#vatx0fxr" 338 | desc = Descriptor.from_str(desc_str) 339 | assert str(desc) == desc_str 340 | desc.derive(0) 341 | # In the string representation they would use raw keys. Do the same for asserting. 342 | for key in desc.keys: 343 | key.key = coincurve.PublicKey(key.key.pubkey) 344 | assert str(desc) == str(rust_bitcoin_desc_str) 345 | 346 | # Same, but to check that the Script is actually being derived too... 347 | desc_str = "wsh(multi(5,tpubD6NzVbkrYhZ4Yb5yyh2qqUnfGzyakvyzYei3qf2roEMuP7DFB47CDhcUW93YjFGGpwXgUbjeFfoapYyXyyUD2cT1tTzdBCMAhsNTmEJxLM2/*,tpubD6NzVbkrYhZ4Wn1byYeaSwqq6aHni5hQmzHmha8WUgQFH7H5mQ4NZXM8dTs52kqsaxFuau7edrm27ZXNbyp6V5vRJxLZ9oxB92F1dVVAnTn/*,tpubD6NzVbkrYhZ4XLQ56KtSZs1ezkUfD2f1QsUPRvVRqmoo1xsJ9DM6Yao4XKqkEDxGHenroWaooEbpjDTzr7W2LB5CYVPn83eacD1swW38W5G/*,tpubD6NzVbkrYhZ4Ys7ii3MvAhZVowvQRPHwT9uctEnxEmnXR7KtBqyEofT6LmvXov5tpMLDcMhNCC3pi4NrLq1vG51rPcsFGtP5MDHq2F9Bj5Z/*,tpubD6NzVbkrYhZ4WmzxsFZByU1tKop9SWd5YHH81b2gbT5ycGAkZfthcwNAcQZmxswzTvpjBaswKgbcEKksbkGW65wbQsA4DEaCq9c7SqUZ9oi/*))#p26mhq70, deriv index 0, desc wsh(multi(5,[838f8104/0]02de76d54f7e28d731f403f5d1fad4da1df208e1d6e00dbe6dfbadd804461c2743,[24e59fd4/0]02f65c2812d2a8d1da479d0cf32d4ca717263bcdadd4b3f11a014b8cc27f73ec44,[cf0b5330/0]024bf97e1bfc4b5c1de90172d50d92fe072da40a8ccd0f89cd5e858b9dc1226623,[cecf756b/0]023bdc599713ea7b982dc3f439aad24f6c6c8b1a4617f339ba976a48d9067a7d67,[04458729/0]0245cca25b3ecea1a82157bc98b9c35caa53d0f65b9ecb5bfdbb80749d22357c45))#h88gukn3" 348 | desc_a = Descriptor.from_str(desc_str) 349 | desc_a.derive(0) 350 | desc_b = Descriptor.from_str(desc_str) 351 | desc_b.derive(1) 352 | assert desc_a.script_pubkey != desc_b.script_pubkey 353 | assert ( 354 | desc_a.script_pubkey.hex() 355 | == "002076fb586cb821ac94fbe094e012b93d82cc42925bcf543415416f42aa3ba1822c" 356 | ) 357 | assert ( 358 | desc_a.witness_script.script.hex() 359 | == "552102de76d54f7e28d731f403f5d1fad4da1df208e1d6e00dbe6dfbadd804461c27432102f65c2812d2a8d1da479d0cf32d4ca717263bcdadd4b3f11a014b8cc27f73ec4421024bf97e1bfc4b5c1de90172d50d92fe072da40a8ccd0f89cd5e858b9dc122662321023bdc599713ea7b982dc3f439aad24f6c6c8b1a4617f339ba976a48d9067a7d67210245cca25b3ecea1a82157bc98b9c35caa53d0f65b9ecb5bfdbb80749d22357c4555ae" 360 | ) 361 | 362 | # An Unvault descriptor from Revault 363 | desc_str = "wsh(andor(thresh(1,pk(tpubD6NzVbkrYhZ4Wu1wWF6gEL8tAZvATeGodn1ymPeC3eo9XGdj6fats9QdMG88KZ23FjV4SyTn5LAHLLwRmor4n6yWBH5ccJLnj7LWcuyPuDQ/*)),and_v(v:multi(2,0227cb9432f93edc3ba82ca70c75bda335553a999e6ab885bc337fcb837aa18f4a,02ed00f0a17f220c7b2179ab9610ea2cccaf290c0f726ce472ab959b2528d2b9de),older(9990)),thresh(2,pkh(tpubD6NzVbkrYhZ4Y1KSo5w1yFPreF7THiygs775SRLyMKJZ8ACgtkLJPNb9UiDk4L4MJuYPsdViWfY65tteiub51YZtqjjv6kLKdKH5WSdH7Br/*),a:pkh(tpubD6NzVbkrYhZ4Xhspiqm3eot2TddA2XmcPmqHyRftxFaKkZWuePH4RXw3Af6CpPfnhRBKPjz7TveUGi91EXTph5V7qHYJ4ijG3NtCjrCKPRH/*))))#se46h9uw" 364 | Descriptor.from_str(desc_str) 365 | 366 | # We can parse a multipath descriptors, and make it into separate single-path descriptors. 367 | multipath_desc = Descriptor.from_str( 368 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<7';8h;20>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/<0;1;987>/*)))" 369 | ) 370 | assert multipath_desc.is_multipath() 371 | single_path_descs = multipath_desc.singlepath_descriptors() 372 | assert [str(d) for d in single_path_descs] == [ 373 | str( 374 | Descriptor.from_str( 375 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/7'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/0/*)))" 376 | ) 377 | ), 378 | str( 379 | Descriptor.from_str( 380 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/8h/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/1/*)))" 381 | ) 382 | ), 383 | str( 384 | Descriptor.from_str( 385 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/20/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/987/*)))" 386 | ) 387 | ), 388 | ] 389 | 390 | # Minisafe descriptor 391 | Descriptor.from_str( 392 | "wsh(or_d(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/<0;1>/*),older(65000))))" 393 | ) 394 | 395 | # Even if only one of the keys is multipath 396 | multipath_desc = Descriptor.from_str( 397 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))" 398 | ) 399 | assert multipath_desc.is_multipath() 400 | single_path_descs = multipath_desc.singlepath_descriptors() 401 | assert [str(d) for d in single_path_descs] == [ 402 | str( 403 | Descriptor.from_str( 404 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/0/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))" 405 | ) 406 | ), 407 | str( 408 | Descriptor.from_str( 409 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/1/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))" 410 | ) 411 | ), 412 | ] 413 | 414 | # We can detect regular singlepath descs 415 | desc = Descriptor.from_str( 416 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))" 417 | ) 418 | assert not desc.is_multipath() 419 | 420 | # We refuse to parse descriptor with multipath key expressions of varying length 421 | with pytest.raises( 422 | DescriptorParsingError, 423 | match="Descriptor contains multipath key expressions with varying length", 424 | ): 425 | Descriptor.from_str( 426 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2;3;4>/*)))" 427 | ) 428 | with pytest.raises( 429 | DescriptorParsingError, 430 | match="Descriptor contains multipath key expressions with varying length", 431 | ): 432 | Descriptor.from_str( 433 | "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1;2;3>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2>/*)))" 434 | ) 435 | 436 | # Liana Taproot descriptor. 437 | desc = Descriptor.from_str( 438 | "tr(xpub661MyMwAqRbcFERisZuMzFcfg3Ur3dKB17kb8iEG89ZJYMHTWqKQGRdLjTXC6Byr8kjKo6JabFfRCm3ETM4woq7DxUXuUxxRFHfog4Peh41/<0;1>/*,{and_v(v:multi_a(2,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*),older(26352)),multi_a(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*)})#tugn7xtx" 439 | ) 440 | assert desc.is_multipath() 441 | 442 | 443 | def test_taproot_key_path(): 444 | def sanity_check_spk(desc_str, spk_hex): 445 | desc = Descriptor.from_str(desc_str) 446 | assert desc.script_pubkey.hex() == spk_hex 447 | 448 | # Taken from Bitcoin Core unit tests. 449 | sanity_check_spk( 450 | "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", 451 | "512077aab6e066f8a7419c5ab714c12c67d25007ed55a43cadcacb4d7a970a093f11", 452 | ) 453 | roundtrip_desc( 454 | "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)#dh4fyxrd" 455 | ) 456 | # Taken from rust-miniscript unit tests. 457 | sanity_check_spk( 458 | "tr(02e20e746af365e86647826397ba1c0e0d5cb685752976fe2f326ab76bdc4d6ee9)", 459 | "51209c19294f03757da3dc235a5960631e3c55751632f5889b06b7a053bdc0bcfbcb", 460 | ) 461 | roundtrip_desc( 462 | "tr(02e20e746af365e86647826397ba1c0e0d5cb685752976fe2f326ab76bdc4d6ee9)#f7yg99rk" 463 | ) 464 | 465 | # Works for derived keys too. Taken and adapted from rust-miniscript unit tests. 466 | desc = Descriptor.from_str( 467 | "tr(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)" 468 | ) 469 | desc.derive(0) 470 | assert ( 471 | str(desc) 472 | == "tr([a7bea80d/0]xpub6H3W6JmYJXN49h5TfcVjLC3onS6uPeUTTJoVvRC8oG9vsTn2J8LwigLzq5tHbrwAzH9DGo6ThGUdWsqce8dGfwHVBxSbixjDADGGdzF7t2B)#dx6zghxv" 473 | ) 474 | for key in desc.keys: 475 | key.key = coincurve.PublicKeyXOnly(key.key.pubkey[1:]) 476 | assert ( 477 | str(desc) 478 | == "tr([a7bea80d/0]cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115)#26vl7d0e" 479 | ) 480 | 481 | 482 | def test_taproot_script_path(): 483 | # Badly-formatted tree expressions 484 | with pytest.raises(DescriptorParsingError): 485 | Descriptor.from_str( 486 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,)" 487 | ) 488 | with pytest.raises(DescriptorParsingError): 489 | Descriptor.from_str( 490 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{)" 491 | ) 492 | with pytest.raises(DescriptorParsingError): 493 | Descriptor.from_str( 494 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,})" 495 | ) 496 | with pytest.raises(DescriptorParsingError): 497 | Descriptor.from_str( 498 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{})" 499 | ) 500 | with pytest.raises(DescriptorParsingError): 501 | Descriptor.from_str( 502 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{,})" 503 | ) 504 | with pytest.raises(DescriptorParsingError): 505 | Descriptor.from_str( 506 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{0,})" 507 | ) 508 | with pytest.raises(DescriptorParsingError): 509 | Descriptor.from_str( 510 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{,1})" 511 | ) 512 | 513 | # Parsing of various format of internal keys when there is also a tree expression 514 | desc = roundtrip_desc( 515 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{0,1})#pxx8z3sd" 516 | ) 517 | assert str(desc.tree.left_child) == str(Node.from_str("0")) 518 | roundtrip_desc( 519 | "tr(02cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{0,1})#chhcv82t" 520 | ) 521 | roundtrip_desc( 522 | "tr([a7bea80d/0]cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{0,1})#kuyl0wph" 523 | ) 524 | roundtrip_desc( 525 | "tr([a7bea80d/0]02cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{0,1})#582tu5pl" 526 | ) 527 | roundtrip_desc( 528 | "tr(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*,{0,1})#ph24rkjw" 529 | ) 530 | roundtrip_desc( 531 | "tr([a7bea80d/9875763'/0]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*,{0,1})#rc52p8hy" 532 | ) 533 | 534 | # Verify the computation of the merkle root. Checked against Bitcoin Core and rust-miniscript. 535 | # Single leaf 536 | desc = roundtrip_desc( 537 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,pk(1af85df7c89b9d7b8d7ed881c508df243895c37c2a4ef1a945374d468944da57))#dx2xu7f8" 538 | ) 539 | assert ( 540 | desc.output_key().format().hex() 541 | == "49d60cd8db4481ba726e89d9925097949a313e009a6cd81549dcad410f5c69c2" 542 | ) 543 | # Depth 1, balanced. 544 | desc = roundtrip_desc( 545 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{pk(30925c62aa5db756f2441f18372a22f99e84f3e1db754e4e8d1cf7ff9227556d),pk(1af85df7c89b9d7b8d7ed881c508df243895c37c2a4ef1a945374d468944da57)})#4cly7ykp" 546 | ) 547 | assert ( 548 | desc.output_key().format().hex() 549 | == "364033633d10c0bb6af6515778b7245d615546197bfae4f9bceeda4cd55c06f9" 550 | ) 551 | # Depth 2, imbalanced. 552 | desc = roundtrip_desc( 553 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{pk(30925c62aa5db756f2441f18372a22f99e84f3e1db754e4e8d1cf7ff9227556d),{pk(1af85df7c89b9d7b8d7ed881c508df243895c37c2a4ef1a945374d468944da57),pk(af7453eeac1fc57201cd7813c722c06e12929d7be23c1c025d3afacf2e0b0cfa)}})#ycrkgmjm" 554 | ) 555 | assert ( 556 | desc.output_key().format().hex() 557 | == "3baf6fbd5fd8f853feb0bc06b43babcc11aa4583aa423f48047f1ada780a0a6b" 558 | ) 559 | # Depth 2, imbalanced the other side. 560 | desc = roundtrip_desc( 561 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{{pk(30925c62aa5db756f2441f18372a22f99e84f3e1db754e4e8d1cf7ff9227556d),pk(1af85df7c89b9d7b8d7ed881c508df243895c37c2a4ef1a945374d468944da57)},pk(af7453eeac1fc57201cd7813c722c06e12929d7be23c1c025d3afacf2e0b0cfa)})#xtk7tcz0" 562 | ) 563 | assert ( 564 | desc.output_key().format().hex() 565 | == "50858e1c2167b6860b8b1d602a7926e95f77e4e5741c8cb057f767cba69edc29" 566 | ) 567 | # Depth 2, balanced. 568 | desc = roundtrip_desc( 569 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{{pk(30925c62aa5db756f2441f18372a22f99e84f3e1db754e4e8d1cf7ff9227556d),pk(1af85df7c89b9d7b8d7ed881c508df243895c37c2a4ef1a945374d468944da57)},{pk(af7453eeac1fc57201cd7813c722c06e12929d7be23c1c025d3afacf2e0b0cfa),pk(6cb7bbba9f9f455ddb3e5bd9ac2156dda063706105fe55c5eb0a8457fed32915)}})#e59qvuzs" 570 | ) 571 | assert ( 572 | desc.output_key().format().hex() 573 | == "00ca439c5b5eadfdb4c85353a5b76850d28c9ce410cf59f8b04afbc77a2ede6b" 574 | ) 575 | 576 | # Can't use a multi() under Taproot. 577 | with pytest.raises(DescriptorParsingError, match="only available for P2WSH"): 578 | desc = Descriptor.from_str( 579 | f"tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,multi(2,0227cb9432f93edc3ba82ca70c75bda335553a999e6ab885bc337fcb837aa18f4a,02ed00f0a17f220c7b2179ab9610ea2cccaf290c0f726ce472ab959b2528d2b9de))" 580 | ) 581 | 582 | # A multi_a made verify, and back Wdu, for the purpose of exercising various 583 | # fragments. 584 | multi_frag = fragments.MultiA( 585 | 2, 586 | [ 587 | DescriptorKey( 588 | "cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115" 589 | ), 590 | DescriptorKey( 591 | "a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd" 592 | ), 593 | ], 594 | ) 595 | convoluted_multi_a = fragments.WrapA( 596 | fragments.AndB( 597 | fragments.WrapU(fragments.WrapT(fragments.WrapV(multi_frag))), 598 | fragments.WrapA(fragments.WrapL(fragments.Just1())), 599 | ), 600 | ) 601 | pk_frag = fragments.WrapC( 602 | fragments.Pk( 603 | DescriptorKey( 604 | "02cc24adfed5a481b000192042b2399087437d8eb16095c3dda1d45a4fbf868017" 605 | ), 606 | is_taproot=True, 607 | ) 608 | ) 609 | pkh_frag = fragments.WrapC( 610 | fragments.Pkh( 611 | DescriptorKey( 612 | "033d65a099daf8d973422e75f78c29504e5e53bfb81f3b08d9bb161cdfb3c3ee9a" 613 | ), 614 | is_taproot=True, 615 | ) 616 | ) 617 | thresh_frag = fragments.Thresh( 618 | 1, [pkh_frag, fragments.WrapS(pk_frag), convoluted_multi_a] 619 | ) 620 | 621 | # Single-script tree expressions 622 | desc = roundtrip_desc( 623 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,0)#jfxxeqdf" 624 | ) 625 | assert str(desc.tree) == str(Node.from_str("0")) 626 | desc = roundtrip_desc( 627 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,1)#ggpdnrdk" 628 | ) 629 | assert str(desc.tree) == str(Node.from_str("1")) 630 | 631 | # Deep tree (NOTE: it's ok to repeat keys across leaves) 632 | desc = roundtrip_desc( 633 | "tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,\ 634 | {\ 635 | {\ 636 | {\ 637 | pk(6e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea286),\ 638 | pkh(6e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea286)\ 639 | },\ 640 | {\ 641 | pkh(022f4401b9fbf2f8b3d491cfa307eb6d895b2a5632276461450fb4dc0045f22329),\ 642 | pk(022f4401b9fbf2f8b3d491cfa307eb6d895b2a5632276461450fb4dc0045f22329)\ 643 | }\ 644 | },\ 645 | {\ 646 | {\ 647 | pkh(368045d7d65e1d57c47a5c40ff8b8f382c2d3ef0f1eaf199c1ffac15bb381b95),\ 648 | pkh(03e1f7ab99d16384daa9ccee2c39ac5de3ff73df288fd8b8752962c8aa975cd8ae)\ 649 | },\ 650 | {\ 651 | pk(026e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea286),\ 652 | pk(026e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea286)\ 653 | }\ 654 | }\ 655 | }\ 656 | )#0zdzegs3".replace( 657 | " ", "" 658 | ) 659 | ) 660 | 661 | # Imbalanced trees 662 | desc = roundtrip_desc( 663 | f"tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{{{{{pk_frag},{pkh_frag}}},{convoluted_multi_a}}})#a7dcefu7" 664 | ) 665 | assert str(desc.tree.left_child.left_child) == str(pk_frag) 666 | assert str(desc.tree.left_child.right_child) == str(pkh_frag) 667 | assert str(desc.tree.right_child) == str(convoluted_multi_a) 668 | desc = roundtrip_desc( 669 | f"tr(cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115,{{{pk_frag},{{{pkh_frag},{convoluted_multi_a}}}}})#67yr0cff" 670 | ) 671 | assert str(desc.tree.left_child) == str(pk_frag) 672 | assert str(desc.tree.right_child.left_child) == str(pkh_frag) 673 | assert str(desc.tree.right_child.right_child) == str(convoluted_multi_a) 674 | 675 | 676 | def test_taproot_satisfaction(): 677 | """Cross check the satisfaction against""" 678 | 679 | desc = Descriptor.from_str( 680 | "tr(e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922,{{{pk(6f3083e8d6e468fc5db3ec3301a259d73110b22310e0640c3b106fda8a5773cc),pkh(4228e97dbded4aab222af59b862cd36bc6756f21f55eadbbee8fb2a6c41a4561)},{pk(f2c463aeda45b31314a5fa8a98970647df2517f2b9786255d5c66dd3520e6b30),pk(fff4b58834e5ff31b2d9c30ec18b9e92de9c299484ce2a4e7ac6d8e3c0062571)}},{{pk(45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78),pk(541522cc80f357d28aa9d3883aacaa312310f915a3237ba52b8107ea33a6bbc6)},{pkh(b0e7f16f04d8fab675658197058f116eca2c3c1b162b8aafb90e066615c51f99),pkh(3ae686f1a11c6d54e99f450e52148a9c38209e9010165c125b0558490e0d766a)}}})#qhxcthy5" 681 | ) 682 | assert ( 683 | desc.tree.merkle_root().hex() 684 | == "f24b40c4a4790c55b26d306e64178047d3fb8322e8d0aefe0823be80b9c0a86b" 685 | ) 686 | dummy_sig = bytes(64) 687 | desc_keys = set(k.bytes().hex() for k in desc.keys) 688 | 689 | # First test the key path 690 | material = SatisfactionMaterial(signatures={desc.output_key().format(): dummy_sig}) 691 | assert [e.hex() for e in desc.satisfy(material)] == [ 692 | dummy_sig.hex(), 693 | ] 694 | 695 | # Then the script path for all the keys 696 | key_cb = [ 697 | ( 698 | "45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78", 699 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922482065cba2919aa9ca1c62f3943b3a091d82d154a89e15bcddc23f78ad9e62cd2a86719442ce543b69b84c6ccdb58eaab3f0a8803e1d48865a20067dac61d1d4795c638fed8e10481e4874ed3c676a8a51a42875bc1ba435c08fc92ab5027c56", 700 | ), 701 | ( 702 | "541522cc80f357d28aa9d3883aacaa312310f915a3237ba52b8107ea33a6bbc6", 703 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f0292243974653ab9357fb76569ac62e8e4bef31a0be7678dbec93585591850a95d9b92a86719442ce543b69b84c6ccdb58eaab3f0a8803e1d48865a20067dac61d1d4795c638fed8e10481e4874ed3c676a8a51a42875bc1ba435c08fc92ab5027c56", 704 | ), 705 | ( 706 | "6f3083e8d6e468fc5db3ec3301a259d73110b22310e0640c3b106fda8a5773cc", 707 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922da83bb9bdf7a5a3b0d45a5f811d90fb88495cc4c984ab1b60618a4012833e42782bbc5be7826c9493cfd897c3deea90f0a380b7610657bcdc66b3e0662048fe8a90d03ef4486e773622d8779920cc966db3b0ccbe439c2b86b5f7bb69c9d4ef8", 708 | ), 709 | ( 710 | "f2c463aeda45b31314a5fa8a98970647df2517f2b9786255d5c66dd3520e6b30", 711 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f029223513f769ff3e4ec0279cc10f43f1146b5d6bfed72291133ef69ef7357cff5adcc759e7319868d906769d5c29080f5bb195bc75c3f9a22436b6b1f903cb75ebc1a90d03ef4486e773622d8779920cc966db3b0ccbe439c2b86b5f7bb69c9d4ef8", 712 | ), 713 | ( 714 | "fff4b58834e5ff31b2d9c30ec18b9e92de9c299484ce2a4e7ac6d8e3c0062571", 715 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f029220e456c80b89ca89d82b09b15464d4f9d157330ea8494193b623b9ce147939c8cc759e7319868d906769d5c29080f5bb195bc75c3f9a22436b6b1f903cb75ebc1a90d03ef4486e773622d8779920cc966db3b0ccbe439c2b86b5f7bb69c9d4ef8", 716 | ), 717 | ] 718 | for key, control_block in key_cb: 719 | assert key in desc_keys 720 | material = SatisfactionMaterial(signatures={bytes.fromhex(key): dummy_sig}) 721 | assert [e.hex() for e in desc.satisfy(material)] == [ 722 | dummy_sig.hex(), 723 | "20" + key + "ac", # PUSHDATA for 64 bytes, the key, then OP_CHECKSIG 724 | control_block, 725 | ] 726 | 727 | # Then the script path for all the key hashes 728 | key_h_cb = [ 729 | ( 730 | "4228e97dbded4aab222af59b862cd36bc6756f21f55eadbbee8fb2a6c41a4561", 731 | "74e908762a50669f5f170ecac952023ce4a68a43", 732 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922ee0d7007dce3440c2a083167d8ef0536c3c622699f058f6720e9666fa85430dd82bbc5be7826c9493cfd897c3deea90f0a380b7610657bcdc66b3e0662048fe8a90d03ef4486e773622d8779920cc966db3b0ccbe439c2b86b5f7bb69c9d4ef8", 733 | ), 734 | ( 735 | "b0e7f16f04d8fab675658197058f116eca2c3c1b162b8aafb90e066615c51f99", 736 | "9e6533b1086900a89b734d939d325781e463e086", 737 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f0292254ae6600770c341bbacfb8190fae055e91bb4c131651282265755fe7e85f5bba2bfedf828a6e2335ea2e2ef2874fe7509086ad19418103ad2e3fb664376cb483795c638fed8e10481e4874ed3c676a8a51a42875bc1ba435c08fc92ab5027c56", 738 | ), 739 | ( 740 | "3ae686f1a11c6d54e99f450e52148a9c38209e9010165c125b0558490e0d766a", 741 | "8ee89b2f14917b680653e09272164887e6f43fe2", 742 | "c1e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922017fb95999131b293ac837fe77ec3b17eb9d57e9c901e40f13d77adbd90bfe422bfedf828a6e2335ea2e2ef2874fe7509086ad19418103ad2e3fb664376cb483795c638fed8e10481e4874ed3c676a8a51a42875bc1ba435c08fc92ab5027c56", 743 | ), 744 | ] 745 | for key, h, control_block in key_h_cb: 746 | assert key in desc_keys 747 | material = SatisfactionMaterial(signatures={bytes.fromhex(key): dummy_sig}) 748 | assert [e.hex() for e in desc.satisfy(material)] == [ 749 | dummy_sig.hex(), 750 | key, # Key push for the hash preimage 751 | "76a914" 752 | + h 753 | + "88ac", # DUP HASH160 PUSHDATA for 20 bytes, the hash, EQUALVERIFY, CHECKSIG 754 | control_block, 755 | ] 756 | 757 | # If there is a signature for any script path but also for the key path, the satisfier 758 | # will chose the key path. 759 | material = SatisfactionMaterial( 760 | signatures={ 761 | desc.output_key().format(): dummy_sig, 762 | bytes.fromhex( 763 | "45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78" 764 | ): dummy_sig, 765 | } 766 | ) 767 | assert [e.hex() for e in desc.satisfy(material)] == [ 768 | dummy_sig.hex(), 769 | ] 770 | 771 | # If there is a signature for any script path but also for the key path, the satisfier 772 | # will chose the key path. 773 | material = SatisfactionMaterial( 774 | signatures={ 775 | desc.output_key().format(): dummy_sig, 776 | bytes.fromhex( 777 | "45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78" 778 | ): dummy_sig, 779 | } 780 | ) 781 | assert [e.hex() for e in desc.satisfy(material)] == [ 782 | dummy_sig.hex(), 783 | ] 784 | 785 | # Between two leaves at the same depth the satisfier will chose the less expensive leaf 786 | # to satisfy. 787 | material = SatisfactionMaterial( 788 | signatures={ 789 | # For pkh() 790 | bytes.fromhex( 791 | "4228e97dbded4aab222af59b862cd36bc6756f21f55eadbbee8fb2a6c41a4561" 792 | ): dummy_sig, 793 | # For pk() 794 | bytes.fromhex( 795 | "45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78" 796 | ): dummy_sig, 797 | } 798 | ) 799 | sat = desc.satisfy(material) 800 | assert sat[0] == dummy_sig and sat[1] == bytes.fromhex( 801 | "2045455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78ac" 802 | ) 803 | 804 | # Between two satisfactions of the same size but at different depths the satifier will 805 | # choose the less deep one as it makes the merkle proof shorter. 806 | desc = Descriptor.from_str( 807 | "tr(e6b631547001c2ca7c6cfb0637df5dcf23540567b7130b42a5560b9fa9f02922,{{pk(6f3083e8d6e468fc5db3ec3301a259d73110b22310e0640c3b106fda8a5773cc),{pk(f2c463aeda45b31314a5fa8a98970647df2517f2b9786255d5c66dd3520e6b30),pk(fff4b58834e5ff31b2d9c30ec18b9e92de9c299484ce2a4e7ac6d8e3c0062571)}},{{pk(45455d3f915ac438c9405467bcf0fcf59d9cf4b25069fb7cef1ace3451c69e78),pk(541522cc80f357d28aa9d3883aacaa312310f915a3237ba52b8107ea33a6bbc6)},{pkh(b0e7f16f04d8fab675658197058f116eca2c3c1b162b8aafb90e066615c51f99),pkh(3ae686f1a11c6d54e99f450e52148a9c38209e9010165c125b0558490e0d766a)}}})" 808 | ) 809 | material = SatisfactionMaterial( 810 | signatures={ 811 | # pk() at depth 2 812 | bytes.fromhex( 813 | "6f3083e8d6e468fc5db3ec3301a259d73110b22310e0640c3b106fda8a5773cc" 814 | ): dummy_sig, 815 | # pk() at depth 3 816 | bytes.fromhex( 817 | "f2c463aeda45b31314a5fa8a98970647df2517f2b9786255d5c66dd3520e6b30" 818 | ): dummy_sig, 819 | } 820 | ) 821 | sat = desc.satisfy(material) 822 | assert sat[0] == dummy_sig and sat[1] == bytes.fromhex( 823 | "206f3083e8d6e468fc5db3ec3301a259d73110b22310e0640c3b106fda8a5773ccac" 824 | ) 825 | -------------------------------------------------------------------------------- /bip380/miniscript/fragments.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miniscript AST elements. 3 | 4 | Each element correspond to a Bitcoin Script fragment, and has various type properties. 5 | See the Miniscript website for the specification of the type system: https://bitcoin.sipa.be/miniscript/. 6 | """ 7 | 8 | import copy 9 | import bip380.miniscript.parsing as parsing 10 | 11 | from bip380.key import DescriptorKey 12 | from bip380.utils.hashes import hash160 13 | from bip380.utils.script import ( 14 | CScript, 15 | OP_1, 16 | OP_0, 17 | OP_ADD, 18 | OP_BOOLAND, 19 | OP_BOOLOR, 20 | OP_DUP, 21 | OP_ELSE, 22 | OP_ENDIF, 23 | OP_EQUAL, 24 | OP_EQUALVERIFY, 25 | OP_FROMALTSTACK, 26 | OP_IFDUP, 27 | OP_IF, 28 | OP_CHECKLOCKTIMEVERIFY, 29 | OP_CHECKMULTISIG, 30 | OP_CHECKMULTISIGVERIFY, 31 | OP_CHECKSEQUENCEVERIFY, 32 | OP_CHECKSIG, 33 | OP_CHECKSIGADD, 34 | OP_CHECKSIGVERIFY, 35 | OP_HASH160, 36 | OP_HASH256, 37 | OP_NOTIF, 38 | OP_NUMEQUAL, 39 | OP_RIPEMD160, 40 | OP_SHA256, 41 | OP_SIZE, 42 | OP_SWAP, 43 | OP_TOALTSTACK, 44 | OP_VERIFY, 45 | OP_0NOTEQUAL, 46 | ) 47 | 48 | from .errors import MiniscriptNodeCreationError 49 | from .property import Property 50 | from .satisfaction import ExecutionInfo, Satisfaction 51 | 52 | 53 | # Threshold for nLockTime: below this value it is interpreted as block number, 54 | # otherwise as UNIX timestamp. 55 | LOCKTIME_THRESHOLD = 500000000 # Tue Nov 5 00:53:20 1985 UTC 56 | 57 | # If CTxIn::nSequence encodes a relative lock-time and this flag 58 | # is set, the relative lock-time has units of 512 seconds, 59 | # otherwise it specifies blocks with a granularity of 1. 60 | SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22 61 | 62 | 63 | class Node: 64 | """A Miniscript fragment.""" 65 | 66 | # The fragment's type and properties 67 | p = None 68 | # List of all sub fragments 69 | subs = [] 70 | # A list of Script elements, a CScript is created all at once in the script() method. 71 | _script = [] 72 | # Whether any satisfaction for this fragment require a signature 73 | needs_sig = None 74 | # Whether any dissatisfaction for this fragment requires a signature 75 | is_forced = None 76 | # Whether this fragment has a unique unconditional satisfaction, and all conditional 77 | # ones require a signature. 78 | is_expressive = None 79 | # Whether for any possible way to satisfy this fragment (may be none), a 80 | # non-malleable satisfaction exists. 81 | is_nonmalleable = None 82 | # Whether this node or any of its subs contains an absolute heightlock 83 | abs_heightlocks = None 84 | # Whether this node or any of its subs contains a relative heightlock 85 | rel_heightlocks = None 86 | # Whether this node or any of its subs contains an absolute timelock 87 | abs_timelocks = None 88 | # Whether this node or any of its subs contains a relative timelock 89 | rel_timelocks = None 90 | # Whether this node does not contain a mix of timelock or heightlock of different types. 91 | # That is, not (abs_heightlocks and rel_heightlocks or abs_timelocks and abs_timelocks) 92 | no_timelock_mix = None 93 | # Information about this Miniscript execution (satisfaction cost, etc..) 94 | exec_info = None 95 | 96 | def __init__(self, *args, **kwargs): 97 | # Needs to be implemented by derived classes. 98 | raise NotImplementedError 99 | 100 | # TODO: make the is_taproot parameter mandatory. 101 | def from_str(ms_str, is_taproot=False): 102 | """Parse a Miniscript fragment from its string representation.""" 103 | assert isinstance(ms_str, str) 104 | assert isinstance(is_taproot, bool) 105 | 106 | return parsing.miniscript_from_str(ms_str, is_taproot) 107 | 108 | def from_script(script, is_taproot=False, pkh_preimages={}): 109 | """Decode a Miniscript fragment from its Script representation.""" 110 | assert isinstance(script, CScript) 111 | assert isinstance(is_taproot, bool) 112 | 113 | return parsing.miniscript_from_script(script, is_taproot, pkh_preimages) 114 | 115 | # TODO: have something like BuildScript from Core and get rid of the _script member. 116 | @property 117 | def script(self): 118 | return CScript(self._script) 119 | 120 | @property 121 | def keys(self): 122 | """Get the list of all keys from this Miniscript, in order of apparition.""" 123 | # Overriden by fragments that actually have keys. 124 | return [key for sub in self.subs for key in sub.keys] 125 | 126 | def satisfy(self, sat_material): 127 | """Get the witness of the smallest non-malleable satisfaction for this fragment, 128 | if one exists. 129 | 130 | :param sat_material: a SatisfactionMaterial containing available data to satisfy 131 | challenges. 132 | """ 133 | sat = self.satisfaction(sat_material) 134 | if not sat.has_sig: 135 | return None 136 | return sat.witness 137 | 138 | def satisfaction(self, sat_material): 139 | """Get the satisfaction for this fragment. 140 | 141 | :param sat_material: a SatisfactionMaterial containing available data to satisfy 142 | challenges. 143 | """ 144 | # Needs to be implemented by derived classes. 145 | raise NotImplementedError 146 | 147 | def dissatisfaction(self): 148 | """Get the dissatisfaction for this fragment.""" 149 | # Needs to be implemented by derived classes. 150 | raise NotImplementedError 151 | 152 | 153 | class Just0(Node): 154 | def __init__(self): 155 | 156 | self._script = [OP_0] 157 | 158 | self.p = Property("Bzud") 159 | self.needs_sig = False 160 | self.is_forced = False 161 | self.is_expressive = True 162 | self.is_nonmalleable = True 163 | self.abs_heightlocks = False 164 | self.rel_heightlocks = False 165 | self.abs_timelocks = False 166 | self.rel_timelocks = False 167 | self.no_timelock_mix = True 168 | self.exec_info = ExecutionInfo(0, 0, None, 0) 169 | 170 | def satisfaction(self, sat_material): 171 | return Satisfaction.unavailable() 172 | 173 | def dissatisfaction(self): 174 | return Satisfaction(witness=[]) 175 | 176 | def __repr__(self): 177 | return "0" 178 | 179 | 180 | class Just1(Node): 181 | def __init__(self): 182 | 183 | self._script = [OP_1] 184 | 185 | self.p = Property("Bzu") 186 | self.needs_sig = False 187 | self.is_forced = True # No dissat 188 | self.is_expressive = False # No dissat 189 | self.is_nonmalleable = True # FIXME: how comes? Standardness rules? 190 | self.abs_heightlocks = False 191 | self.rel_heightlocks = False 192 | self.abs_timelocks = False 193 | self.rel_timelocks = False 194 | self.no_timelock_mix = True 195 | self.exec_info = ExecutionInfo(0, 0, 0, None) 196 | 197 | def satisfaction(self, sat_material): 198 | return Satisfaction(witness=[]) 199 | 200 | def dissatisfaction(self): 201 | return Satisfaction.unavailable() 202 | 203 | def __repr__(self): 204 | return "1" 205 | 206 | 207 | class PkNode(Node): 208 | """A virtual class for nodes containing a single public key. 209 | 210 | Should not be instanced directly, use Pk() or Pkh(). 211 | """ 212 | 213 | def __init__(self, pubkey, is_taproot): 214 | 215 | if isinstance(pubkey, bytes) or isinstance(pubkey, str): 216 | self.pubkey = DescriptorKey(pubkey, x_only=is_taproot) 217 | elif isinstance(pubkey, DescriptorKey): 218 | self.pubkey = pubkey 219 | else: 220 | raise MiniscriptNodeCreationError("Invalid public key") 221 | 222 | self.needs_sig = True # FIXME: think about having it in 'c:' instead 223 | self.is_forced = False 224 | self.is_expressive = True 225 | self.is_nonmalleable = True 226 | self.abs_heightlocks = False 227 | self.rel_heightlocks = False 228 | self.abs_timelocks = False 229 | self.rel_timelocks = False 230 | self.no_timelock_mix = True 231 | 232 | @property 233 | def keys(self): 234 | return [self.pubkey] 235 | 236 | 237 | class Pk(PkNode): 238 | def __init__(self, pubkey, is_taproot): 239 | PkNode.__init__(self, pubkey, is_taproot) 240 | 241 | self.p = Property("Konud") 242 | self.exec_info = ExecutionInfo(0, 0, 0, 0) 243 | 244 | @property 245 | def _script(self): 246 | return [self.pubkey.bytes()] 247 | 248 | def satisfaction(self, sat_material): 249 | sig = sat_material.signatures.get(self.pubkey.bytes()) 250 | if sig is None: 251 | return Satisfaction.unavailable() 252 | return Satisfaction([sig], has_sig=True) 253 | 254 | def dissatisfaction(self): 255 | return Satisfaction(witness=[b""]) 256 | 257 | def __repr__(self): 258 | return f"pk_k({self.pubkey})" 259 | 260 | 261 | class Pkh(PkNode): 262 | # FIXME: should we support a hash here, like rust-bitcoin? I don't think it's safe. 263 | def __init__(self, pubkey, is_taproot): 264 | PkNode.__init__(self, pubkey, is_taproot) 265 | 266 | self.p = Property("Knud") 267 | self.exec_info = ExecutionInfo(3, 0, 1, 1) 268 | 269 | @property 270 | def _script(self): 271 | return [OP_DUP, OP_HASH160, self.pk_hash(), OP_EQUALVERIFY] 272 | 273 | def satisfaction(self, sat_material): 274 | sig = sat_material.signatures.get(self.pubkey.bytes()) 275 | if sig is None: 276 | return Satisfaction.unavailable() 277 | return Satisfaction(witness=[sig, self.pubkey.bytes()], has_sig=True) 278 | 279 | def dissatisfaction(self): 280 | return Satisfaction(witness=[b"", self.pubkey.bytes()]) 281 | 282 | def __repr__(self): 283 | return f"pk_h({self.pubkey})" 284 | 285 | def pk_hash(self): 286 | assert isinstance(self.pubkey, DescriptorKey) 287 | return hash160(self.pubkey.bytes()) 288 | 289 | 290 | class Older(Node): 291 | def __init__(self, value): 292 | assert value > 0 and value < 2 ** 31 293 | 294 | self.value = value 295 | self._script = [self.value, OP_CHECKSEQUENCEVERIFY] 296 | 297 | self.p = Property("Bz") 298 | self.needs_sig = False 299 | self.is_forced = True 300 | self.is_expressive = False # No dissat 301 | self.is_nonmalleable = True 302 | self.rel_timelocks = bool(value & SEQUENCE_LOCKTIME_TYPE_FLAG) 303 | self.rel_heightlocks = not self.rel_timelocks 304 | self.abs_heightlocks = False 305 | self.abs_timelocks = False 306 | self.no_timelock_mix = True 307 | self.exec_info = ExecutionInfo(1, 0, 0, None) 308 | 309 | def satisfaction(self, sat_material): 310 | if sat_material.max_sequence < self.value: 311 | return Satisfaction.unavailable() 312 | return Satisfaction(witness=[]) 313 | 314 | def dissatisfaction(self): 315 | return Satisfaction.unavailable() 316 | 317 | def __repr__(self): 318 | return f"older({self.value})" 319 | 320 | 321 | class After(Node): 322 | def __init__(self, value): 323 | assert value > 0 and value < 2 ** 31 324 | 325 | self.value = value 326 | self._script = [self.value, OP_CHECKLOCKTIMEVERIFY] 327 | 328 | self.p = Property("Bz") 329 | self.needs_sig = False 330 | self.is_forced = True 331 | self.is_expressive = False # No dissat 332 | self.is_nonmalleable = True 333 | self.abs_heightlocks = value < LOCKTIME_THRESHOLD 334 | self.abs_timelocks = not self.abs_heightlocks 335 | self.rel_heightlocks = False 336 | self.rel_timelocks = False 337 | self.no_timelock_mix = True 338 | self.exec_info = ExecutionInfo(1, 0, 0, None) 339 | 340 | def satisfaction(self, sat_material): 341 | if sat_material.max_lock_time < self.value: 342 | return Satisfaction.unavailable() 343 | return Satisfaction(witness=[]) 344 | 345 | def dissatisfaction(self): 346 | return Satisfaction.unavailable() 347 | 348 | def __repr__(self): 349 | return f"after({self.value})" 350 | 351 | 352 | class HashNode(Node): 353 | """A virtual class for fragments with hashlock semantics. 354 | 355 | Should not be instanced directly, use concrete fragments instead. 356 | """ 357 | 358 | def __init__(self, digest, hash_op): 359 | assert isinstance(digest, bytes) # TODO: real errors 360 | 361 | self.digest = digest 362 | self._script = [OP_SIZE, 32, OP_EQUALVERIFY, hash_op, digest, OP_EQUAL] 363 | 364 | self.p = Property("Bonud") 365 | self.needs_sig = False 366 | self.is_forced = False 367 | self.is_expressive = False 368 | self.is_nonmalleable = True 369 | self.abs_heightlocks = False 370 | self.rel_heightlocks = False 371 | self.abs_timelocks = False 372 | self.rel_timelocks = False 373 | self.no_timelock_mix = True 374 | self.exec_info = ExecutionInfo(4, 0, 1, None) 375 | 376 | def satisfaction(self, sat_material): 377 | preimage = sat_material.preimages.get(self.digest) 378 | if preimage is None: 379 | return Satisfaction.unavailable() 380 | return Satisfaction(witness=[preimage]) 381 | 382 | def dissatisfaction(self): 383 | return Satisfaction.unavailable() 384 | return Satisfaction(witness=[b""]) 385 | 386 | 387 | class Sha256(HashNode): 388 | def __init__(self, digest): 389 | assert len(digest) == 32 # TODO: real errors 390 | HashNode.__init__(self, digest, OP_SHA256) 391 | 392 | def __repr__(self): 393 | return f"sha256({self.digest.hex()})" 394 | 395 | 396 | class Hash256(HashNode): 397 | def __init__(self, digest): 398 | assert len(digest) == 32 # TODO: real errors 399 | HashNode.__init__(self, digest, OP_HASH256) 400 | 401 | def __repr__(self): 402 | return f"hash256({self.digest.hex()})" 403 | 404 | 405 | class Ripemd160(HashNode): 406 | def __init__(self, digest): 407 | assert len(digest) == 20 # TODO: real errors 408 | HashNode.__init__(self, digest, OP_RIPEMD160) 409 | 410 | def __repr__(self): 411 | return f"ripemd160({self.digest.hex()})" 412 | 413 | 414 | class Hash160(HashNode): 415 | def __init__(self, digest): 416 | assert len(digest) == 20 # TODO: real errors 417 | HashNode.__init__(self, digest, OP_HASH160) 418 | 419 | def __repr__(self): 420 | return f"hash160({self.digest.hex()})" 421 | 422 | 423 | class Multi(Node): 424 | def __init__(self, k, keys): 425 | assert 1 <= k <= len(keys) 426 | assert all(isinstance(k, DescriptorKey) and not k.ser_x_only for k in keys) 427 | 428 | self.k = k 429 | self.pubkeys = keys 430 | 431 | self.p = Property("Bndu") 432 | self.needs_sig = True 433 | self.is_forced = False 434 | self.is_expressive = True 435 | self.is_nonmalleable = True 436 | self.abs_heightlocks = False 437 | self.rel_heightlocks = False 438 | self.abs_timelocks = False 439 | self.rel_timelocks = False 440 | self.no_timelock_mix = True 441 | self.exec_info = ExecutionInfo(1, len(keys), 1 + k, 1 + k) 442 | 443 | @property 444 | def keys(self): 445 | return self.pubkeys 446 | 447 | @property 448 | def _script(self): 449 | return [ 450 | self.k, 451 | *[k.bytes() for k in self.keys], 452 | len(self.keys), 453 | OP_CHECKMULTISIG, 454 | ] 455 | 456 | def satisfaction(self, sat_material): 457 | sigs = [] 458 | for key in self.keys: 459 | sig = sat_material.signatures.get(key.bytes()) 460 | if sig is not None: 461 | assert isinstance(sig, bytes) 462 | sigs.append(sig) 463 | if len(sigs) == self.k: 464 | break 465 | if len(sigs) < self.k: 466 | return Satisfaction.unavailable() 467 | return Satisfaction(witness=[b""] + sigs, has_sig=True) 468 | 469 | def dissatisfaction(self): 470 | return Satisfaction(witness=[b""] * (self.k + 1)) 471 | 472 | def __repr__(self): 473 | return f"multi({','.join([str(self.k)] + [str(k) for k in self.keys])})" 474 | 475 | 476 | class MultiA(Node): 477 | def __init__(self, k, keys): 478 | assert 1 <= k <= len(keys) and len(keys) <= 999 479 | assert all(isinstance(k, DescriptorKey) and k.ser_x_only for k in keys) 480 | 481 | self.k = k 482 | self.pubkeys = keys 483 | 484 | self.p = Property("Bndu") 485 | self.needs_sig = True 486 | self.is_forced = False 487 | self.is_expressive = True 488 | self.is_nonmalleable = True 489 | self.abs_heightlocks = False 490 | self.rel_heightlocks = False 491 | self.abs_timelocks = False 492 | self.rel_timelocks = False 493 | self.no_timelock_mix = True 494 | self.exec_info = ExecutionInfo(len(keys) + 1, 0, len(keys), len(keys)) 495 | 496 | @property 497 | def keys(self): 498 | return self.pubkeys 499 | 500 | @property 501 | def _script(self): 502 | s = [self.pubkeys[0].bytes(), OP_CHECKSIG] 503 | for k in self.pubkeys[1:]: 504 | s += [k.bytes(), OP_CHECKSIGADD] 505 | return s + [self.k, OP_NUMEQUAL] 506 | 507 | def satisfaction(self, sat_material): 508 | sigs = [] 509 | sigs_count = 0 510 | for key in self.keys: 511 | sig = sat_material.signatures.get(key.bytes()) 512 | if sig is not None: 513 | assert isinstance(sig, bytes) 514 | sigs.append(sig) 515 | sigs_count += 1 516 | else: 517 | sigs.append(b"") 518 | if sigs_count == self.k: 519 | break 520 | if sigs_count < self.k: 521 | return Satisfaction.unavailable() 522 | if len(sigs) < len(self.keys): 523 | sigs += [b""] * (len(self.keys) - len(sigs)) 524 | return Satisfaction(witness=sigs, has_sig=True) 525 | 526 | def dissatisfaction(self): 527 | return Satisfaction(witness=[b""] * len(self.keys)) 528 | 529 | def __repr__(self): 530 | return f"multi_a({','.join([str(self.k)] + [str(k) for k in self.keys])})" 531 | 532 | 533 | class AndV(Node): 534 | def __init__(self, sub_x, sub_y): 535 | assert sub_x.p.V 536 | assert sub_y.p.has_any("BKV") 537 | 538 | self.subs = [sub_x, sub_y] 539 | 540 | self.p = Property( 541 | sub_y.p.type() 542 | + ("z" if sub_x.p.z and sub_y.p.z else "") 543 | + ("o" if sub_x.p.z and sub_y.p.o or sub_x.p.o and sub_y.p.z else "") 544 | + ("n" if sub_x.p.n or sub_x.p.z and sub_y.p.n else "") 545 | + ("u" if sub_y.p.u else "") 546 | ) 547 | self.needs_sig = any(sub.needs_sig for sub in self.subs) 548 | self.is_forced = any(sub.needs_sig for sub in self.subs) 549 | self.is_expressive = False # Not 'd' 550 | self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) 551 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 552 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 553 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 554 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 555 | self.no_timelock_mix = not ( 556 | self.abs_heightlocks 557 | and self.abs_timelocks 558 | or self.rel_heightlocks 559 | and self.rel_timelocks 560 | ) 561 | 562 | @property 563 | def _script(self): 564 | return sum((sub._script for sub in self.subs), start=[]) 565 | 566 | @property 567 | def exec_info(self): 568 | exec_info = ExecutionInfo.from_concat( 569 | self.subs[0].exec_info, self.subs[1].exec_info 570 | ) 571 | exec_info.set_undissatisfiable() # it's V. 572 | return exec_info 573 | 574 | def satisfaction(self, sat_material): 575 | return Satisfaction.from_concat(sat_material, *self.subs) 576 | 577 | def dissatisfaction(self): 578 | return Satisfaction.unavailable() # it's V. 579 | 580 | def __repr__(self): 581 | return f"and_v({','.join(map(str, self.subs))})" 582 | 583 | 584 | class AndB(Node): 585 | def __init__(self, sub_x, sub_y): 586 | assert sub_x.p.B and sub_y.p.W 587 | 588 | self.subs = [sub_x, sub_y] 589 | 590 | self.p = Property( 591 | "Bu" 592 | + ("z" if sub_x.p.z and sub_y.p.z else "") 593 | + ("o" if sub_x.p.z and sub_y.p.o or sub_x.p.o and sub_y.p.z else "") 594 | + ("n" if sub_x.p.n or sub_x.p.z and sub_y.p.n else "") 595 | + ("d" if sub_x.p.d and sub_y.p.d else "") 596 | + ("u" if sub_y.p.u else "") 597 | ) 598 | self.needs_sig = any(sub.needs_sig for sub in self.subs) 599 | self.is_forced = ( 600 | sub_x.is_forced 601 | and sub_y.is_forced 602 | or any(sub.is_forced and sub.needs_sig for sub in self.subs) 603 | ) 604 | self.is_expressive = all(sub.is_forced and sub.needs_sig for sub in self.subs) 605 | self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) 606 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 607 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 608 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 609 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 610 | self.no_timelock_mix = not ( 611 | self.abs_heightlocks 612 | and self.abs_timelocks 613 | or self.rel_heightlocks 614 | and self.rel_timelocks 615 | ) 616 | 617 | @property 618 | def _script(self): 619 | return sum((sub._script for sub in self.subs), start=[]) + [OP_BOOLAND] 620 | 621 | @property 622 | def exec_info(self): 623 | return ExecutionInfo.from_concat( 624 | self.subs[0].exec_info, self.subs[1].exec_info, ops_count=1 625 | ) 626 | 627 | def satisfaction(self, sat_material): 628 | return Satisfaction.from_concat(sat_material, self.subs[0], self.subs[1]) 629 | 630 | def dissatisfaction(self): 631 | return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() 632 | 633 | def __repr__(self): 634 | return f"and_b({','.join(map(str, self.subs))})" 635 | 636 | 637 | class OrB(Node): 638 | def __init__(self, sub_x, sub_z): 639 | assert sub_x.p.has_all("Bd") 640 | assert sub_z.p.has_all("Wd") 641 | 642 | self.subs = [sub_x, sub_z] 643 | 644 | self.p = Property( 645 | "Bdu" 646 | + ("z" if sub_x.p.z and sub_z.p.z else "") 647 | + ("o" if sub_x.p.z and sub_z.p.o or sub_x.p.o and sub_z.p.z else "") 648 | ) 649 | self.needs_sig = all(sub.needs_sig for sub in self.subs) 650 | self.is_forced = False # Both subs are 'd' 651 | self.is_expressive = all(sub.is_expressive for sub in self.subs) 652 | self.is_nonmalleable = all( 653 | sub.is_nonmalleable and sub.is_expressive for sub in self.subs 654 | ) and any(sub.needs_sig for sub in self.subs) 655 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 656 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 657 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 658 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 659 | self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) 660 | 661 | @property 662 | def _script(self): 663 | return sum((sub._script for sub in self.subs), start=[]) + [OP_BOOLOR] 664 | 665 | @property 666 | def exec_info(self): 667 | return ExecutionInfo.from_concat( 668 | self.subs[0].exec_info, 669 | self.subs[1].exec_info, 670 | ops_count=1, 671 | disjunction=True, 672 | ) 673 | 674 | def satisfaction(self, sat_material): 675 | return Satisfaction.from_concat( 676 | sat_material, self.subs[0], self.subs[1], disjunction=True 677 | ) 678 | 679 | def dissatisfaction(self): 680 | return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() 681 | 682 | def __repr__(self): 683 | return f"or_b({','.join(map(str, self.subs))})" 684 | 685 | 686 | class OrC(Node): 687 | def __init__(self, sub_x, sub_z): 688 | assert sub_x.p.has_all("Bdu") and sub_z.p.V 689 | 690 | self.subs = [sub_x, sub_z] 691 | 692 | self.p = Property( 693 | "V" 694 | + ("z" if sub_x.p.z and sub_z.p.z else "") 695 | + ("o" if sub_x.p.o and sub_z.p.z else "") 696 | ) 697 | self.needs_sig = all(sub.needs_sig for sub in self.subs) 698 | self.is_forced = True # Because sub_z is 'V' 699 | self.is_expressive = False # V 700 | self.is_nonmalleable = ( 701 | all(sub.is_nonmalleable for sub in self.subs) 702 | and any(sub.needs_sig for sub in self.subs) 703 | and sub_x.is_expressive 704 | ) 705 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 706 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 707 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 708 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 709 | self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) 710 | 711 | @property 712 | def _script(self): 713 | return self.subs[0]._script + [OP_NOTIF] + self.subs[1]._script + [OP_ENDIF] 714 | 715 | @property 716 | def exec_info(self): 717 | exec_info = ExecutionInfo.from_or_uneven( 718 | self.subs[0].exec_info, self.subs[1].exec_info, ops_count=2 719 | ) 720 | exec_info.set_undissatisfiable() # it's V. 721 | return exec_info 722 | 723 | def satisfaction(self, sat_material): 724 | return Satisfaction.from_or_uneven(sat_material, self.subs[0], self.subs[1]) 725 | 726 | def dissatisfaction(self): 727 | return Satisfaction.unavailable() # it's V. 728 | 729 | def __repr__(self): 730 | return f"or_c({','.join(map(str, self.subs))})" 731 | 732 | 733 | class OrD(Node): 734 | def __init__(self, sub_x, sub_z): 735 | assert sub_x.p.has_all("Bdu") 736 | assert sub_z.p.has_all("B") 737 | 738 | self.subs = [sub_x, sub_z] 739 | 740 | self.p = Property( 741 | "B" 742 | + ("z" if sub_x.p.z and sub_z.p.z else "") 743 | + ("o" if sub_x.p.o and sub_z.p.z else "") 744 | + ("d" if sub_z.p.d else "") 745 | + ("u" if sub_z.p.u else "") 746 | ) 747 | self.needs_sig = all(sub.needs_sig for sub in self.subs) 748 | self.is_forced = all(sub.is_forced for sub in self.subs) 749 | self.is_expressive = all(sub.is_expressive for sub in self.subs) 750 | self.is_nonmalleable = ( 751 | all(sub.is_nonmalleable for sub in self.subs) 752 | and any(sub.needs_sig for sub in self.subs) 753 | and sub_x.is_expressive 754 | ) 755 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 756 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 757 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 758 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 759 | self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) 760 | 761 | @property 762 | def _script(self): 763 | return ( 764 | self.subs[0]._script 765 | + [OP_IFDUP, OP_NOTIF] 766 | + self.subs[1]._script 767 | + [OP_ENDIF] 768 | ) 769 | 770 | @property 771 | def exec_info(self): 772 | return ExecutionInfo.from_or_uneven( 773 | self.subs[0].exec_info, self.subs[1].exec_info, ops_count=3 774 | ) 775 | 776 | def satisfaction(self, sat_material): 777 | return Satisfaction.from_or_uneven(sat_material, self.subs[0], self.subs[1]) 778 | 779 | def dissatisfaction(self): 780 | return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() 781 | 782 | def __repr__(self): 783 | return f"or_d({','.join(map(str, self.subs))})" 784 | 785 | 786 | class OrI(Node): 787 | def __init__(self, sub_x, sub_z): 788 | assert sub_x.p.type() == sub_z.p.type() and sub_x.p.has_any("BKV") 789 | 790 | self.subs = [sub_x, sub_z] 791 | 792 | self.p = Property( 793 | sub_x.p.type() 794 | + ("o" if sub_x.p.z and sub_z.p.z else "") 795 | + ("d" if sub_x.p.d or sub_z.p.d else "") 796 | + ("u" if sub_x.p.u and sub_z.p.u else "") 797 | ) 798 | self.needs_sig = all(sub.needs_sig for sub in self.subs) 799 | self.is_forced = all(sub.is_forced for sub in self.subs) 800 | self.is_expressive = ( 801 | sub_x.is_expressive 802 | and sub_z.is_forced 803 | or sub_x.is_forced 804 | and sub_z.is_expressive 805 | ) 806 | self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) and any( 807 | sub.needs_sig for sub in self.subs 808 | ) 809 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 810 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 811 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 812 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 813 | self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) 814 | 815 | @property 816 | def _script(self): 817 | return ( 818 | [OP_IF] 819 | + self.subs[0]._script 820 | + [OP_ELSE] 821 | + self.subs[1]._script 822 | + [OP_ENDIF] 823 | ) 824 | 825 | @property 826 | def exec_info(self): 827 | return ExecutionInfo.from_or_even( 828 | self.subs[0].exec_info, self.subs[1].exec_info, ops_count=3 829 | ) 830 | 831 | def satisfaction(self, sat_material): 832 | return (self.subs[0].satisfaction(sat_material) + Satisfaction([b"\x01"])) | ( 833 | self.subs[1].satisfaction(sat_material) + Satisfaction([b""]) 834 | ) 835 | 836 | def dissatisfaction(self): 837 | return (self.subs[0].dissatisfaction() + Satisfaction(witness=[b"\x01"])) | ( 838 | self.subs[1].dissatisfaction() + Satisfaction(witness=[b""]) 839 | ) 840 | 841 | def __repr__(self): 842 | return f"or_i({','.join(map(str, self.subs))})" 843 | 844 | 845 | class AndOr(Node): 846 | def __init__(self, sub_x, sub_y, sub_z): 847 | assert sub_x.p.has_all("Bdu") 848 | assert sub_y.p.type() == sub_z.p.type() and sub_y.p.has_any("BKV") 849 | 850 | self.subs = [sub_x, sub_y, sub_z] 851 | 852 | self.p = Property( 853 | sub_y.p.type() 854 | + ("z" if sub_x.p.z and sub_y.p.z and sub_z.p.z else "") 855 | + ( 856 | "o" 857 | if sub_x.p.z 858 | and sub_y.p.o 859 | and sub_z.p.o 860 | or sub_x.p.o 861 | and sub_y.p.z 862 | and sub_z.p.z 863 | else "" 864 | ) 865 | + ("d" if sub_z.p.d else "") 866 | + ("u" if sub_y.p.u and sub_z.p.u else "") 867 | ) 868 | self.needs_sig = sub_x.needs_sig and (sub_y.needs_sig or sub_z.needs_sig) 869 | self.is_forced = sub_z.is_forced and (sub_x.needs_sig or sub_y.is_forced) 870 | self.is_expressive = ( 871 | sub_x.is_expressive 872 | and sub_z.is_expressive 873 | and (sub_x.needs_sig or sub_y.is_forced) 874 | ) 875 | self.is_nonmalleable = ( 876 | all(sub.is_nonmalleable for sub in self.subs) 877 | and any(sub.needs_sig for sub in self.subs) 878 | and sub_x.is_expressive 879 | ) 880 | self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) 881 | self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) 882 | self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) 883 | self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) 884 | # X and Y, or Z. So we have a mix if any contain a timelock mix, or 885 | # there is a mix between X and Y. 886 | self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) and not ( 887 | any(sub.rel_timelocks for sub in [sub_x, sub_y]) 888 | and any(sub.rel_heightlocks for sub in [sub_x, sub_y]) 889 | or any(sub.abs_timelocks for sub in [sub_x, sub_y]) 890 | and any(sub.abs_heightlocks for sub in [sub_x, sub_y]) 891 | ) 892 | 893 | @property 894 | def _script(self): 895 | return ( 896 | self.subs[0]._script 897 | + [OP_NOTIF] 898 | + self.subs[2]._script 899 | + [OP_ELSE] 900 | + self.subs[1]._script 901 | + [OP_ENDIF] 902 | ) 903 | 904 | @property 905 | def exec_info(self): 906 | return ExecutionInfo.from_andor_uneven( 907 | self.subs[0].exec_info, 908 | self.subs[1].exec_info, 909 | self.subs[2].exec_info, 910 | ops_count=3, 911 | ) 912 | 913 | def satisfaction(self, sat_material): 914 | # (A and B) or (!A and C) 915 | return ( 916 | self.subs[1].satisfaction(sat_material) 917 | + self.subs[0].satisfaction(sat_material) 918 | ) | (self.subs[2].satisfaction(sat_material) + self.subs[0].dissatisfaction()) 919 | 920 | def dissatisfaction(self): 921 | # Dissatisfy X and Z 922 | return self.subs[2].dissatisfaction() + self.subs[0].dissatisfaction() 923 | 924 | def __repr__(self): 925 | return f"andor({','.join(map(str, self.subs))})" 926 | 927 | 928 | class AndN(AndOr): 929 | def __init__(self, sub_x, sub_y): 930 | AndOr.__init__(self, sub_x, sub_y, Just0()) 931 | 932 | def __repr__(self): 933 | return f"and_n({self.subs[0]},{self.subs[1]})" 934 | 935 | 936 | class Thresh(Node): 937 | def __init__(self, k, subs): 938 | n = len(subs) 939 | assert 1 <= k <= n 940 | 941 | self.k = k 942 | self.subs = subs 943 | 944 | all_z = True 945 | all_z_but_one_odu = False 946 | all_e = True 947 | all_m = True 948 | s_count = 0 949 | # If k == 1, just check each child for k 950 | if k > 1: 951 | self.abs_heightlocks = subs[0].abs_heightlocks 952 | self.rel_heightlocks = subs[0].rel_heightlocks 953 | self.abs_timelocks = subs[0].abs_timelocks 954 | self.rel_timelocks = subs[0].rel_timelocks 955 | else: 956 | self.no_timelock_mix = True 957 | 958 | assert subs[0].p.has_all("Bdu") 959 | for sub in subs[1:]: 960 | assert sub.p.has_all("Wdu") 961 | if not sub.p.z: 962 | if all_z_but_one_odu: 963 | # Fails "all 'z' but one" 964 | all_z_but_one_odu = False 965 | if all_z and sub.p.has_all("odu"): 966 | # They were all 'z' up to now. 967 | all_z_but_one_odu = True 968 | all_z = False 969 | all_e = all_e and sub.is_expressive 970 | all_m = all_m and sub.is_nonmalleable 971 | if sub.needs_sig: 972 | s_count += 1 973 | if k > 1: 974 | self.abs_heightlocks |= sub.abs_heightlocks 975 | self.rel_heightlocks |= sub.rel_heightlocks 976 | self.abs_timelocks |= sub.abs_timelocks 977 | self.rel_timelocks |= sub.rel_timelocks 978 | else: 979 | self.no_timelock_mix &= sub.no_timelock_mix 980 | 981 | self.p = Property( 982 | "Bdu" + ("z" if all_z else "") + ("o" if all_z_but_one_odu else "") 983 | ) 984 | self.needs_sig = s_count >= n - k 985 | self.is_forced = False # All subs need to be 'd' 986 | self.is_expressive = all_e and s_count == n 987 | self.is_nonmalleable = all_e and s_count >= n - k 988 | if k > 1: 989 | self.no_timelock_mix = not ( 990 | self.abs_heightlocks 991 | and self.abs_timelocks 992 | or self.rel_heightlocks 993 | and self.rel_timelocks 994 | ) 995 | 996 | @property 997 | def _script(self): 998 | return ( 999 | self.subs[0]._script 1000 | + sum(((sub._script + [OP_ADD]) for sub in self.subs[1:]), start=[]) 1001 | + [self.k, OP_EQUAL] 1002 | ) 1003 | 1004 | @property 1005 | def exec_info(self): 1006 | return ExecutionInfo.from_thresh(self.k, [sub.exec_info for sub in self.subs]) 1007 | 1008 | def satisfaction(self, sat_material): 1009 | return Satisfaction.from_thresh(sat_material, self.k, self.subs) 1010 | 1011 | def dissatisfaction(self): 1012 | return sum( 1013 | [sub.dissatisfaction() for sub in self.subs], start=Satisfaction(witness=[]) 1014 | ) 1015 | 1016 | def __repr__(self): 1017 | return f"thresh({self.k},{','.join(map(str, self.subs))})" 1018 | 1019 | 1020 | class WrapperNode(Node): 1021 | """A virtual base class for wrappers. 1022 | 1023 | Don't instanciate it directly, use concret wrapper fragments instead. 1024 | """ 1025 | 1026 | def __init__(self, sub): 1027 | self.subs = [sub] 1028 | 1029 | # Properties for most wrappers are directly inherited. When it's not, they 1030 | # are overriden in the fragment's __init__. 1031 | self.needs_sig = sub.needs_sig 1032 | self.is_forced = sub.is_forced 1033 | self.is_expressive = sub.is_expressive 1034 | self.is_nonmalleable = sub.is_nonmalleable 1035 | self.abs_heightlocks = sub.abs_heightlocks 1036 | self.rel_heightlocks = sub.rel_heightlocks 1037 | self.abs_timelocks = sub.abs_timelocks 1038 | self.rel_timelocks = sub.rel_timelocks 1039 | self.no_timelock_mix = not ( 1040 | self.abs_heightlocks 1041 | and self.abs_timelocks 1042 | or self.rel_heightlocks 1043 | and self.rel_timelocks 1044 | ) 1045 | 1046 | @property 1047 | def sub(self): 1048 | # Wrapper have a single sub 1049 | return self.subs[0] 1050 | 1051 | def satisfaction(self, sat_material): 1052 | # Most wrappers are satisfied this way, for special cases it's overriden. 1053 | return self.subs[0].satisfaction(sat_material) 1054 | 1055 | def dissatisfaction(self): 1056 | # Most wrappers are satisfied this way, for special cases it's overriden. 1057 | return self.subs[0].dissatisfaction() 1058 | 1059 | def skip_colon(self): 1060 | # We need to check this because of the pk() and pkh() aliases. 1061 | if isinstance(self.subs[0], WrapC) and isinstance( 1062 | self.subs[0].subs[0], (Pk, Pkh) 1063 | ): 1064 | return False 1065 | return isinstance(self.subs[0], WrapperNode) 1066 | 1067 | 1068 | class WrapA(WrapperNode): 1069 | def __init__(self, sub): 1070 | assert sub.p.B 1071 | WrapperNode.__init__(self, sub) 1072 | 1073 | self.p = Property("W" + "".join(c for c in "ud" if getattr(sub.p, c))) 1074 | 1075 | @property 1076 | def _script(self): 1077 | return [OP_TOALTSTACK] + self.sub._script + [OP_FROMALTSTACK] 1078 | 1079 | @property 1080 | def exec_info(self): 1081 | return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=2) 1082 | 1083 | def __repr__(self): 1084 | # Don't duplicate colons 1085 | if self.skip_colon(): 1086 | return f"a{self.subs[0]}" 1087 | return f"a:{self.subs[0]}" 1088 | 1089 | 1090 | class WrapS(WrapperNode): 1091 | def __init__(self, sub): 1092 | assert sub.p.has_all("Bo") 1093 | WrapperNode.__init__(self, sub) 1094 | 1095 | self.p = Property("W" + "".join(c for c in "ud" if getattr(sub.p, c))) 1096 | 1097 | @property 1098 | def _script(self): 1099 | return [OP_SWAP] + self.sub._script 1100 | 1101 | @property 1102 | def exec_info(self): 1103 | return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1) 1104 | 1105 | def __repr__(self): 1106 | # Avoid duplicating colons 1107 | if self.skip_colon(): 1108 | return f"s{self.subs[0]}" 1109 | return f"s:{self.subs[0]}" 1110 | 1111 | 1112 | class WrapC(WrapperNode): 1113 | def __init__(self, sub): 1114 | assert sub.p.K 1115 | WrapperNode.__init__(self, sub) 1116 | 1117 | # FIXME: shouldn't n and d be default props on the website? 1118 | self.p = Property("Bu" + "".join(c for c in "dno" if getattr(sub.p, c))) 1119 | 1120 | @property 1121 | def _script(self): 1122 | return self.sub._script + [OP_CHECKSIG] 1123 | 1124 | @property 1125 | def exec_info(self): 1126 | # FIXME: should need_sig be set to True here instead of in keys? 1127 | return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1, sat=1, dissat=1) 1128 | 1129 | def __repr__(self): 1130 | # Special case of aliases 1131 | if isinstance(self.subs[0], Pk): 1132 | return f"pk({self.subs[0].pubkey})" 1133 | if isinstance(self.subs[0], Pkh): 1134 | return f"pkh({self.subs[0].pubkey})" 1135 | # Avoid duplicating colons 1136 | if self.skip_colon(): 1137 | return f"c{self.subs[0]}" 1138 | return f"c:{self.subs[0]}" 1139 | 1140 | 1141 | class WrapT(AndV, WrapperNode): 1142 | def __init__(self, sub): 1143 | AndV.__init__(self, sub, Just1()) 1144 | 1145 | def is_wrapper(self): 1146 | return True 1147 | 1148 | def __repr__(self): 1149 | # Avoid duplicating colons 1150 | if self.skip_colon(): 1151 | return f"t{self.subs[0]}" 1152 | return f"t:{self.subs[0]}" 1153 | 1154 | 1155 | class WrapD(WrapperNode): 1156 | def __init__(self, sub, is_taproot): 1157 | assert sub.p.has_all("Vz") 1158 | WrapperNode.__init__(self, sub) 1159 | 1160 | self.p = Property("Bond" + ("u" if is_taproot else "")) 1161 | self.is_forced = True # sub is V 1162 | self.is_expressive = True # sub is V, and we add a single dissat 1163 | 1164 | @property 1165 | def _script(self): 1166 | return [OP_DUP, OP_IF] + self.sub._script + [OP_ENDIF] 1167 | 1168 | @property 1169 | def exec_info(self): 1170 | return ExecutionInfo.from_wrap_dissat( 1171 | self.sub.exec_info, ops_count=3, sat=1, dissat=1 1172 | ) 1173 | 1174 | def satisfaction(self, sat_material): 1175 | return Satisfaction(witness=[b"\x01"]) + self.subs[0].satisfaction(sat_material) 1176 | 1177 | def dissatisfaction(self): 1178 | return Satisfaction(witness=[b""]) 1179 | 1180 | def __repr__(self): 1181 | # Avoid duplicating colons 1182 | if self.skip_colon(): 1183 | return f"d{self.subs[0]}" 1184 | return f"d:{self.subs[0]}" 1185 | 1186 | 1187 | class WrapV(WrapperNode): 1188 | def __init__(self, sub): 1189 | assert sub.p.B 1190 | WrapperNode.__init__(self, sub) 1191 | 1192 | self.p = Property("V" + "".join(c for c in "zon" if getattr(sub.p, c))) 1193 | self.is_forced = True # V 1194 | self.is_expressive = False # V 1195 | 1196 | @property 1197 | def _script(self): 1198 | if self.sub._script[-1] == OP_CHECKSIG: 1199 | return self.sub._script[:-1] + [OP_CHECKSIGVERIFY] 1200 | elif self.sub._script[-1] == OP_CHECKMULTISIG: 1201 | return self.sub._script[:-1] + [OP_CHECKMULTISIGVERIFY] 1202 | elif self.sub._script[-1] == OP_EQUAL: 1203 | return self.sub._script[:-1] + [OP_EQUALVERIFY] 1204 | return self.sub._script + [OP_VERIFY] 1205 | 1206 | @property 1207 | def exec_info(self): 1208 | verify_cost = int(self._script[-1] == OP_VERIFY) 1209 | return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=verify_cost) 1210 | 1211 | def dissatisfaction(self): 1212 | return Satisfaction.unavailable() # It's V. 1213 | 1214 | def __repr__(self): 1215 | # Avoid duplicating colons 1216 | if self.skip_colon(): 1217 | return f"v{self.subs[0]}" 1218 | return f"v:{self.subs[0]}" 1219 | 1220 | 1221 | class WrapJ(WrapperNode): 1222 | def __init__(self, sub): 1223 | assert sub.p.has_all("Bn") 1224 | WrapperNode.__init__(self, sub) 1225 | 1226 | self.p = Property("Bnd" + "".join(c for c in "ou" if getattr(sub.p, c))) 1227 | self.is_forced = False # d 1228 | self.is_expressive = sub.is_forced 1229 | 1230 | @property 1231 | def _script(self): 1232 | return [OP_SIZE, OP_0NOTEQUAL, OP_IF, *self.sub._script, OP_ENDIF] 1233 | 1234 | @property 1235 | def exec_info(self): 1236 | return ExecutionInfo.from_wrap_dissat(self.sub.exec_info, ops_count=4, dissat=1) 1237 | 1238 | def dissatisfaction(self): 1239 | return Satisfaction(witness=[b""]) 1240 | 1241 | def __repr__(self): 1242 | # Avoid duplicating colons 1243 | if self.skip_colon(): 1244 | return f"j{self.subs[0]}" 1245 | return f"j:{self.subs[0]}" 1246 | 1247 | 1248 | class WrapN(WrapperNode): 1249 | def __init__(self, sub): 1250 | assert sub.p.B 1251 | WrapperNode.__init__(self, sub) 1252 | 1253 | self.p = Property("Bu" + "".join(c for c in "zond" if getattr(sub.p, c))) 1254 | 1255 | @property 1256 | def _script(self): 1257 | return [*self.sub._script, OP_0NOTEQUAL] 1258 | 1259 | @property 1260 | def exec_info(self): 1261 | return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1) 1262 | 1263 | def __repr__(self): 1264 | # Avoid duplicating colons 1265 | if self.skip_colon(): 1266 | return f"n{self.subs[0]}" 1267 | return f"n:{self.subs[0]}" 1268 | 1269 | 1270 | class WrapL(OrI, WrapperNode): 1271 | def __init__(self, sub): 1272 | OrI.__init__(self, Just0(), sub) 1273 | 1274 | def __repr__(self): 1275 | # Avoid duplicating colons 1276 | if self.skip_colon(): 1277 | return f"l{self.subs[1]}" 1278 | return f"l:{self.subs[1]}" 1279 | 1280 | 1281 | class WrapU(OrI, WrapperNode): 1282 | def __init__(self, sub): 1283 | OrI.__init__(self, sub, Just0()) 1284 | 1285 | def __repr__(self): 1286 | # Avoid duplicating colons 1287 | if self.skip_colon(): 1288 | return f"u{self.subs[0]}" 1289 | return f"u:{self.subs[0]}" 1290 | --------------------------------------------------------------------------------