Skip to content

Commit

Permalink
Add PEP484 type hints
Browse files Browse the repository at this point in the history
Type hints make reading and debugging significantly easier.
See https://peps.python.org/pep-0484/
  • Loading branch information
stickies-v committed May 5, 2022
1 parent 8aa1e2f commit f0e63b3
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 300 deletions.
44 changes: 25 additions & 19 deletions bip380/descriptors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from __future__ import annotations

import typing

from bip380.key import DescriptorKey
from bip380.miniscript import Node
from bip380.utils.hashes import sha256, hash160
Expand All @@ -8,40 +12,42 @@
OP_EQUALVERIFY,
OP_CHECKSIG,
)

from .checksum import descsum_create
from .parsing import descriptor_from_str

if typing.TYPE_CHECKING:
from bip380.miniscript import SatisfactionMaterial


class Descriptor:
"""A Bitcoin Output Script Descriptor."""

def from_str(desc_str, strict=False):
def from_str(desc_str: str, strict: bool = False) -> Descriptor:
"""Parse a Bitcoin Output Script Descriptor from its string representation.
:param strict: whether to require the presence of a checksum.
"""
return descriptor_from_str(desc_str, strict)

@property
def script_pubkey(self):
def script_pubkey(self) -> CScript:
"""Get the ScriptPubKey (output 'locking' Script) for this descriptor."""
# To be implemented by derived classes
raise NotImplementedError

@property
def script_sighash(self):
def script_sighash(self) -> CScript:
"""Get the Script to be committed to by the signature hash of a spending transaction."""
# To be implemented by derived classes
raise NotImplementedError

@property
def keys(self):
def keys(self) -> typing.List[DescriptorKey]:
"""Get the list of all keys from this descriptor, in order of apparition."""
# To be implemented by derived classes
raise NotImplementedError

def derive(self, index):
def derive(self, index: int) -> None:
"""Derive the key at the given derivation index.
A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened
Expand All @@ -65,27 +71,27 @@ def satisfy(self, *args, **kwargs):
class WshDescriptor(Descriptor):
"""A Segwit v0 P2WSH Output Script Descriptor."""

def __init__(self, witness_script):
def __init__(self, witness_script: Node):
assert isinstance(witness_script, Node)
self.witness_script = witness_script
self.witness_script: Node = witness_script

def __repr__(self):
def __repr__(self) -> str:
return descsum_create(f"wsh({self.witness_script})")

@property
def script_pubkey(self):
def script_pubkey(self) -> CScript:
witness_program = sha256(self.witness_script.script)
return CScript([0, witness_program])

@property
def script_sighash(self):
def script_sighash(self) -> CScript:
return self.witness_script.script

@property
def keys(self):
def keys(self) -> typing.List[DescriptorKey]:
return self.witness_script.keys

def satisfy(self, sat_material=None):
def satisfy(self, sat_material: SatisfactionMaterial = None) -> typing.List[Node]:
"""Get the witness stack to spend from this descriptor.
:param sat_material: a miniscript.satisfaction.SatisfactionMaterial with data
Expand All @@ -99,28 +105,28 @@ def satisfy(self, sat_material=None):
class WpkhDescriptor(Descriptor):
"""A Segwit v0 P2WPKH Output Script Descriptor."""

def __init__(self, pubkey):
def __init__(self, pubkey: DescriptorKey):
assert isinstance(pubkey, DescriptorKey)
self.pubkey = pubkey

def __repr__(self):
def __repr__(self) -> str:
return descsum_create(f"wpkh({self.pubkey})")

@property
def script_pubkey(self):
def script_pubkey(self) -> CScript:
witness_program = hash160(self.pubkey.bytes())
return CScript([0, witness_program])

@property
def script_sighash(self):
def script_sighash(self) -> CScript:
key_hash = hash160(self.pubkey.bytes())
return CScript([OP_DUP, OP_HASH160, key_hash, OP_EQUALVERIFY, OP_CHECKSIG])

@property
def keys(self):
def keys(self) -> typing.List[DescriptorKey]:
return [self.pubkey]

def satisfy(self, signature):
def satisfy(self, signature: bytes) -> typing.List[bytes]:
"""Get the witness stack to spend from this descriptor.
:param signature: a signature (in bytes) for the pubkey from the descriptor.
Expand Down
11 changes: 6 additions & 5 deletions bip380/descriptors/checksum.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"""Utility functions related to output descriptors"""

import re
import typing

INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD]


def descsum_polymod(symbols):
def descsum_polymod(symbols: typing.List[int]) -> int:
"""Internal function that computes the descriptor checksum."""
chk = 1
for value in symbols:
Expand All @@ -22,7 +23,7 @@ def descsum_polymod(symbols):
return chk


def descsum_expand(s):
def descsum_expand(s: str) -> typing.Optional[typing.List[int]]:
"""Internal function that does the character to symbol expansion"""
groups = []
symbols = []
Expand All @@ -42,7 +43,7 @@ def descsum_expand(s):
return symbols


def descsum_create(s):
def descsum_create(s: str) -> str:
"""Add a checksum to a descriptor without"""
symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
checksum = descsum_polymod(symbols) ^ 1
Expand All @@ -53,7 +54,7 @@ def descsum_create(s):
)


def descsum_check(s):
def descsum_check(s: str) -> bool:
"""Verify that the checksum is correct in a descriptor"""
if s[-9] != "#":
return False
Expand All @@ -63,7 +64,7 @@ def descsum_check(s):
return descsum_polymod(symbols) == 1


def drop_origins(s):
def drop_origins(s: str) -> str:
"""Drop the key origins from a descriptor"""
desc = re.sub(r"\[.+?\]", "", s)
if "#" in s:
Expand Down
4 changes: 2 additions & 2 deletions bip380/descriptors/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class DescriptorParsingError(ValueError):
"""Error while parsing a Bitcoin Output Descriptor from its string representation"""

def __init__(self, message):
self.message = message
def __init__(self, message: str):
self.message: str = message
6 changes: 4 additions & 2 deletions bip380/descriptors/parsing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import bip380.descriptors as descriptors

from bip380.key import DescriptorKey, DescriptorKeyError
Expand All @@ -7,7 +9,7 @@
from .errors import DescriptorParsingError


def split_checksum(desc_str, strict=False):
def split_checksum(desc_str: str, strict: bool = False) -> str:
"""Removes and check the provided checksum.
If not told otherwise, this won't fail on a missing checksum.
Expand All @@ -28,7 +30,7 @@ def split_checksum(desc_str, strict=False):
return descriptor


def descriptor_from_str(desc_str, strict=False):
def descriptor_from_str(desc_str: str, strict: bool = False) -> descriptors.Descriptor:
"""Parse a Bitcoin Output Script Descriptor from its string representation.
:param strict: whether to require the presence of a checksum.
Expand Down
43 changes: 26 additions & 17 deletions bip380/key.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from __future__ import annotations

import typing
from enum import Enum, auto

from bip32 import BIP32
from bip32.utils import coincurve, _deriv_path_str_to_list

from bip380.utils.hashes import hash160
from enum import Enum, auto


class DescriptorKeyError(Exception):
def __init__(self, message):
self.message = message
def __init__(self, message: str):
self.message: str = message


class DescriporKeyOrigin:
Expand All @@ -15,13 +20,13 @@ class DescriporKeyOrigin:
See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions.
"""

def __init__(self, fingerprint, path):
def __init__(self, fingerprint: bytes, path: typing.List[int]):
assert isinstance(fingerprint, bytes) and isinstance(path, list)

self.fingerprint = fingerprint
self.path = path
self.fingerprint: bytes = fingerprint
self.path: typing.List[int] = path

def from_str(origin_str):
def from_str(origin_str: str) -> DescriporKeyOrigin:
# Origing starts and ends with brackets
if not origin_str.startswith("[") or not origin_str.endswith("]"):
raise DescriptorKeyError(f"Insane origin: '{origin_str}'")
Expand Down Expand Up @@ -54,7 +59,7 @@ class KeyPathKind(Enum):
WILDCARD_UNHARDENED = auto()
WILDCARD_HARDENED = auto()

def is_wildcard(self):
def is_wildcard(self) -> bool:
return self in [KeyPathKind.WILDCARD_HARDENED, KeyPathKind.WILDCARD_UNHARDENED]


Expand All @@ -64,13 +69,13 @@ class DescriptorKeyPath:
See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions.
"""

def __init__(self, path, kind):
def __init__(self, path: typing.List[int], kind: KeyPathKind):
assert isinstance(path, list) and isinstance(kind, KeyPathKind)

self.path = path
self.kind = kind
self.path: typing.List[int] = path
self.kind: KeyPathKind = kind

def from_str(path_str):
def from_str(path_str: str) -> DescriptorKeyPath:
if len(path_str) < 1:
raise DescriptorKeyError(f"Insane key path: '{path_str}'")
if path_str[0] == "/":
Expand Down Expand Up @@ -108,7 +113,11 @@ class DescriptorKey:
May be an extended or raw public key.
"""

def __init__(self, key):
origin: typing.Optional[DescriporKeyOrigin]
path: typing.Optional[DescriptorKeyPath]
key: typing.Union[coincurve.PublicKey, BIP32]

def __init__(self, key: typing.Union[bytes, BIP32, str]):
# Information about the origin of this key.
self.origin = None
# If it is an xpub, a path toward a child key of that xpub.
Expand Down Expand Up @@ -156,10 +165,10 @@ def __init__(self, key):
"Invalid parameter type: expecting bytes, hex str or BIP32 instance."
)

def __repr__(self):
def __repr__(self) -> str:
key = ""

def ser_path(key, path):
def ser_path(key: str, path: typing.List[int]) -> str:
for i in path:
if i < 2**31:
key += f"/{i}"
Expand All @@ -185,7 +194,7 @@ def ser_path(key, path):

return key

def bytes(self):
def bytes(self) -> bytes:
if isinstance(self.key, coincurve.PublicKey):
return self.key.format()
else:
Expand All @@ -195,7 +204,7 @@ def bytes(self):
assert not self.path.kind.is_wildcard() # TODO: real errors
return self.key.get_pubkey_from_path(self.path.path)

def derive(self, index):
def derive(self, index: int) -> None:
"""Derive the key at the given index.
A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened
Expand Down
8 changes: 4 additions & 4 deletions bip380/miniscript/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@


class MiniscriptNodeCreationError(ValueError):
def __init__(self, message):
self.message = message
def __init__(self, message: str):
self.message: str = message


class MiniscriptPropertyError(ValueError):
def __init__(self, message):
self.message = message
def __init__(self, message: str):
self.message: str = message

# TODO: errors for type errors, parsing errors, etc..
Loading

0 comments on commit f0e63b3

Please sign in to comment.