Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e5beacc
tap_key_sig
pythcoiner Dec 21, 2024
638a824
taproot_key_sig serialization adjust
odudex Dec 21, 2024
d38f189
replace "set" objects with OrderedDict to ensure reproducible signatures
odudex Jan 29, 2025
c26b8a1
Add PSBT_IN_TAP_KEY_SIG parsing to the InputScope read_value function
odudex Jan 30, 2025
ce7c19b
reproducible PSBT sigs - replicate object type replacement on psbtview
odudex Jan 30, 2025
420999d
t wrapper verifies fragment type is V
odudex Mar 26, 2025
2611c97
miniscript - add literal boolean operators
odudex Mar 28, 2025
f4b42a5
fix Descriptor.num_branches for miniscript
tadeubas Apr 17, 2025
4881dc2
Merge pull request #76 from tadeubas/fix-desc-n-branch
odudex May 6, 2025
8397620
Merge branch 'develop' into validate-desc-load-miniscript
tadeubas May 6, 2025
a9b9d8b
Merge pull request #75 from tadeubas/validate-desc-load-miniscript
odudex May 6, 2025
3098f88
adds tests for invalid length of mnemonic and entropy
jdlcdl Aug 15, 2024
973e6e7
limits mnemonic to 24 words and entropy to 16-32 bytes
jdlcdl Aug 15, 2024
e1d7339
fix: differentiate descriptor types for nested cases
qlrd Jul 16, 2025
5600751
Revert "fix: differentiate descriptor types for nested cases"
odudex Jul 22, 2025
2be3999
replace assertions by conditional error raising
odudex Jun 4, 2024
84ecb0c
ValueError message adjust
odudex Jul 29, 2025
89a941b
test: add cases for `src/embit/bip85.py` (#1)
qlrd Jul 29, 2025
1f1146a
adjust test error message
odudex Jul 30, 2025
76f0d20
Merge branch 'replace_main_code_asserts' into develop
odudex Jul 30, 2025
f30fe98
refactor descriptor parsing
odudex Jun 4, 2024
8123330
fix: differentiate descriptor types
qlrd Jul 16, 2025
fc9afad
test: add test cases fore scriptpubkey_type
qlrd Jul 26, 2025
8115e45
chore: apply some black formatting
qlrd Jul 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/embit/bip39.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def mnemonic_to_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WOR
# this function is copied from Jimmy Song's HDPrivateKey.from_mnemonic() method

words = mnemonic.strip().split()
if len(words) % 3 != 0 or len(words) < 12:
if len(words) % 3 != 0 or not 12 <= len(words) <= 24:
raise ValueError("Invalid recovery phrase")

binary_seed = bytearray()
Expand Down Expand Up @@ -97,7 +97,7 @@ def _extract_index(bits, b, n):


def mnemonic_from_bytes(entropy, wordlist=WORDLIST):
if len(entropy) % 4 != 0:
if len(entropy) % 4 != 0 or not 16 <= len(entropy) <= 32:
raise ValueError("Byte array should be multiple of 4 long (16, 20, ..., 32)")
total_bits = len(entropy) * 8
checksum_bits = total_bits // 32
Expand Down
12 changes: 8 additions & 4 deletions src/embit/bip85.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def derive_entropy(root, app_index, path):
"""
Derive app-specific bip85 entropy using path m/83696968'/app_index'/...path'
"""
assert max(path) < HARDENED_INDEX
if max(path) >= HARDENED_INDEX:
raise ValueError("Path elements must be less than 2^31")
derivation = [HARDENED_INDEX + 83696968, HARDENED_INDEX + app_index] + [
p + HARDENED_INDEX for p in path
]
Expand All @@ -27,7 +28,8 @@ def derive_entropy(root, app_index, path):

def derive_mnemonic(root, num_words=12, index=0, language=LANGUAGES.ENGLISH):
"""Derive a new mnemonic with num_words using language (code, wordlist)"""
assert num_words in [12, 18, 24]
if num_words not in [12, 18, 24]:
raise ValueError("Number of words must be 12, 18 or 24")
langcode, wordlist = language
path = [langcode, num_words, index]
entropy = derive_entropy(root, 39, path)
Expand All @@ -49,7 +51,9 @@ def derive_xprv(root, index=0):

def derive_hex(root, num_bytes=32, index=0):
"""Derive raw entropy from 16 to 64 bytes long"""
assert num_bytes <= 64
assert num_bytes >= 16
if num_bytes > 64:
raise ValueError("Number of bytes must not exceed 64")
if num_bytes < 16:
raise ValueError("Number of bytes must be at least 16")
entropy = derive_entropy(root, 128169, [num_bytes, index])
return entropy[:num_bytes]
6 changes: 4 additions & 2 deletions src/embit/descriptor/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def __init__(self, fingerprint: bytes, derivation: list):
def from_string(cls, s: str):
arr = s.split("/")
mfp = unhexlify(arr[0])
assert len(mfp) == 4
if len(mfp) != 4:
raise ArgumentError("Invalid fingerprint length")
arr[0] = "m"
path = "/".join(arr)
derivation = bip32.parse_path(path)
Expand Down Expand Up @@ -315,7 +316,8 @@ def xonly(self):
return self.key.xonly()

def taproot_tweak(self, h=b""):
assert self.taproot
if not self.taproot:
raise ArgumentError("Key is not taproot")
return self.key.taproot_tweak(h)

def serialize(self):
Expand Down
68 changes: 52 additions & 16 deletions src/embit/descriptor/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def script_len(self):

@property
def num_branches(self):
if self.miniscript is not None:
return max({k.num_branches for k in self.miniscript.keys})

return max([k.num_branches for k in self.keys])

def branch(self, branch_index=None):
Expand Down Expand Up @@ -88,7 +91,7 @@ def is_wildcard(self):

@property
def is_wrapped(self):
return self.sh and self.is_segwit
return bool(self.sh and self.wpkh) or bool(self.sh and self.wsh)

@property
def is_legacy(self):
Expand All @@ -97,7 +100,9 @@ def is_legacy(self):
@property
def is_segwit(self):
return (
(self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot
bool(self.wsh and self.miniscript)
or bool(self.wpkh and self.key)
or bool(self.taproot)
)

@property
Expand All @@ -118,18 +123,28 @@ def is_basic_multisig(self) -> bool:
def is_sorted(self) -> bool:
return bool(self.is_basic_multisig and isinstance(self.miniscript, Sortedmulti))

@property
def is_miniscript(self) -> bool:
return bool(
self.miniscript and not isinstance(self.miniscript, (Multi, Sortedmulti))
)

def scriptpubkey_type(self):
if self.is_taproot:
return "p2tr"
if self.sh:
if self.wpkh:
Comment thread
qlrd marked this conversation as resolved.
return "p2sh-p2wpkh"
if self.wsh:
return "p2sh-p2wsh"
return "p2sh"
if self.wsh:
return "p2wsh"
if self.is_pkh:
if self.is_legacy:
return "p2pkh"
if self.is_segwit:
return "p2wpkh"
else:
return "p2wsh"

@property
def brief_policy(self):
Expand Down Expand Up @@ -204,7 +219,7 @@ def owns(self, psbt_scope):
if psbt_scope.script_pubkey is None:
return False
# quick check of script_pubkey type
if psbt_scope.script_pubkey.script_type() != self.scriptpubkey_type():
if psbt_scope.script_pubkey.script_type() not in self.scriptpubkey_type():
return False
for pub, der in psbt_scope.bip32_derivations.items():
# check of the fingerprints
Expand All @@ -214,9 +229,30 @@ def owns(self, psbt_scope):
res = k.check_derivation(der)
if res:
idx, branch_idx = res
sc = self.derive(idx, branch_index=branch_idx).script_pubkey()
# if derivation is found but scriptpubkey doesn't match - fail
return sc == psbt_scope.script_pubkey

# get the script from the scope,
# it can be either legacy, wrapped segwit or segwit v0
sc = self.derive(idx, branch_index=branch_idx)

# If derivation is found but scriptpubkey doesn't match - fail.
# In case of wrapped segwit we need to check redeem script too.
# In case of non-wrapped segwit we can check script_pubkey directly.
if sc.is_wrapped:
redeem_sc = sc.redeem_script()

# you can check script_type of both
# script_pubkey and redeem_script
# to make sure they match the psbt_scope
# print("%s-%s" % (sc.script_pubkey().script_type(), redeem_sc.script_type()))
return (
sc.script_pubkey() == psbt_scope.script_pubkey
and redeem_sc.script_type() == psbt_scope.redeem_script.script_type()
)
else:
# you can check script_type
# to make sure they match the psbt_scope
# print("%s" % sc.script_pubkey().script_type())
return sc.script_pubkey() == psbt_scope.script_pubkey
for pub, (leafs, der) in psbt_scope.taproot_bip32_derivations.items():
# check of the fingerprints
for k in self.keys:
Expand Down Expand Up @@ -292,7 +328,7 @@ def from_string(cls, desc):
@classmethod
def read_from(cls, s):
# starts with sh(wsh()), sh() or wsh()
start = s.read(7)
start = s.read(8)
sh = False
wsh = False
wpkh = False
Expand All @@ -301,30 +337,30 @@ def read_from(cls, s):
taptree = TapTree()
if start.startswith(b"tr("):
taproot = True
s.seek(-4, 1)
s.seek(-5, 1)
elif start.startswith(b"sh(wsh("):
sh = True
wsh = True
s.seek(-1, 1)
elif start.startswith(b"wsh("):
sh = False
wsh = True
s.seek(-3, 1)
elif start.startswith(b"sh(wpkh"):
s.seek(-4, 1)
elif start.startswith(b"sh(wpkh("):
is_miniscript = False
sh = True
wpkh = True
assert s.read(1) == b"("
elif start.startswith(b"wpkh("):
is_miniscript = False
wpkh = True
s.seek(-2, 1)
s.seek(-3, 1)
elif start.startswith(b"pkh("):
is_miniscript = False
s.seek(-3, 1)
s.seek(-4, 1)
elif start.startswith(b"sh("):
sh = True
wsh = False
s.seek(-4, 1)
s.seek(-5, 1)
else:
raise ValueError("Invalid descriptor (starts with '%s')" % start.decode())
# taproot always has a key, and may have taptree miniscript
Expand Down
50 changes: 43 additions & 7 deletions src/embit/descriptor/miniscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,25 @@ def type(self):

@classmethod
def read_from(cls, s, taproot=False):
op, char = read_until(s, b"(")
def wrapped(m_script):
for w in reversed(wrappers):
if w not in WRAPPER_NAMES:
raise MiniscriptError("Unknown wrapper")
WrapperCls = WRAPPERS[WRAPPER_NAMES.index(w)]
m_script = WrapperCls(m_script, taproot=taproot)
return m_script

op, char = read_until(s, b"(,)")
if char in (b",", b")"):
s.seek(-1, 1)
op = op.decode()
wrappers = ""
if ":" in op:
wrappers, op = op.split(":")
# handle boolean literals: 0 or 1
if op in ("0", "1"):
miniscript = JustOne() if op == "1" else JustZero()
return wrapped(miniscript)
if char != b"(":
raise MiniscriptError("Missing operator")
if op not in OPERATOR_NAMES:
Expand All @@ -67,12 +81,7 @@ def read_from(cls, s, taproot=False):
MiniscriptCls = OPERATORS[OPERATOR_NAMES.index(op)]
args = MiniscriptCls.read_arguments(s, taproot=taproot)
miniscript = MiniscriptCls(*args, taproot=taproot)
for w in reversed(wrappers):
if w not in WRAPPER_NAMES:
raise MiniscriptError("Unknown wrapper")
WrapperCls = WRAPPERS[WRAPPER_NAMES.index(w)]
miniscript = WrapperCls(miniscript, taproot=taproot)
return miniscript
return wrapped(miniscript)

@classmethod
def read_arguments(cls, s, taproot=False):
Expand Down Expand Up @@ -119,6 +128,28 @@ def len_args(self):
########### Known fragments (miniscript operators) ##############


class JustZero(Miniscript):
TYPE = "B"
PROPS = "zud"

def inner_compile(self):
return Number(0).compile()

def __str__(self):
return "0"


class JustOne(Miniscript):
TYPE = "B"
PROPS = "zu"

def inner_compile(self):
return Number(1).compile()

def __str__(self):
return "1"


class OneArg(Miniscript):
NARGS = 1

Expand Down Expand Up @@ -870,6 +901,11 @@ def inner_compile(self):

def __len__(self):
return len(self.arg) + 1

def verify(self):
super().verify()
if self.arg.type != "V":
raise MiniscriptError("t: X must be of type V")

@property
def properties(self):
Expand Down
6 changes: 4 additions & 2 deletions src/embit/ec.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def read_from(cls, stream):

class SchnorrSig(EmbitBase):
def __init__(self, sig):
assert len(sig) == 64
if len(sig) != 64:
raise ECError("Invalid schnorr signature")
self._sig = sig

def write_to(self, stream) -> int:
Expand Down Expand Up @@ -93,7 +94,8 @@ def _xonly(self):

@classmethod
def from_xonly(cls, data: bytes):
assert len(data) == 32
if len(data) != 32:
raise ECError("Invalid xonly pubkey")
return cls.parse(b"\x02" + data)

def schnorr_verify(self, sig, msg_hash) -> bool:
Expand Down
9 changes: 6 additions & 3 deletions src/embit/liquid/pset.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ def unblind(self, blinding_key):
return
# verify
gen = secp256k1.generator_generate_blinded(asset, in_abf)
assert gen == secp256k1.generator_parse(self.utxo.asset)
if gen != secp256k1.generator_parse(self.utxo.asset):
raise PSBTError("Invalid asset commitment")
cmt = secp256k1.pedersen_commit(vbf, value, gen)
assert cmt == secp256k1.pedersen_commitment_parse(self.utxo.value)
if cmt != secp256k1.pedersen_commitment_parse(self.utxo.value):
raise PSBTError("Invalid value commitment")

self.asset = asset
self.value = value
Expand Down Expand Up @@ -506,7 +508,8 @@ def unblind(self, blinding_key):
inp.unblind(blinding_key)

def txseed(self, seed: bytes):
assert len(seed) == 32
if len(seed) != 32:
raise PSBTError("Seed should be 32 bytes")
# get unique seed for this tx:
# we use seed + txid:vout + scriptpubkey as unique data for tagged hash
data = b"".join(
Expand Down
3 changes: 2 additions & 1 deletion src/embit/liquid/psetview.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

def skip_commitment(stream):
c = stream.read(1)
assert len(c) == 1
if len(c) != 1:
raise PSBTError("Unexpected end of stream")
if c == b"\x00": # None
return 1
if c == b"\x01": # unconfidential
Expand Down
Loading