Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
f5df860
Descriptor minisscript fix check branches
tadeubas Jun 19, 2024
219cd62
more real test cases
tadeubas Apr 27, 2025
e911c7b
Changed tests to work as before
tadeubas Apr 29, 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
ed2aa4e
Update pyproject.toml to include poetry (#103)
tadeubas Sep 3, 2025
8731be0
mnemonics with double spaces, \n as separators, trailing and leading …
Nov 6, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

#IntelliJ
.idea/

#VSCode
settings.json
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
[tool.poetry]
name = "embit"
version = "0.8.0"
description="A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems."
license="MIT"
authors= ["Stepan Snigirev <snigirev.stepan@gmail.com>"]

[tool.poetry.urls]
repository = "https://github.com/diybitcoinhardware/embit"

[tool.poetry.dependencies]
python = "^3.0"

[build-system]
requires = ["setuptools>=42.0", "wheel"]
build-backend = "setuptools.build_meta"
Expand Down
8 changes: 4 additions & 4 deletions src/embit/bip39.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
def mnemonic_to_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WORDLIST):
# 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:
words = mnemonic.split(" ")
if len(words) % 3 != 0 or not 12 <= len(words) <= 24:
raise ValueError("Invalid recovery phrase")

binary_seed = bytearray()
Expand Down Expand Up @@ -68,7 +68,7 @@ def mnemonic_is_valid(mnemonic: str, wordlist=WORDLIST):
try:
mnemonic_to_bytes(mnemonic, wordlist=wordlist)
return True
except Exception as e:
except Exception:
return False


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
23 changes: 12 additions & 11 deletions src/embit/descriptor/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ def __init__(
if miniscript.type != "B":
raise DescriptorError("Top level miniscript should be 'B'")
# check all branches have the same length
branches = {
len(k.branches) for k in miniscript.keys if k.branches is not None
}
branches = {k.num_branches for k in miniscript.keys}
if len(branches) > 1:
raise DescriptorError("All branches should have the same length")
self.sh = sh
Expand All @@ -61,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 @@ -294,7 +295,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 @@ -303,30 +304,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
27 changes: 18 additions & 9 deletions src/embit/liquid/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ class LTransactionError(TransactionError):

def read_commitment(stream):
c = stream.read(1)
assert len(c) == 1
if len(c) != 1:
raise TransactionError("Invalid commitment")
if c == b"\x00": # None
return None
if c == b"\x01": # unconfidential
r = stream.read(8)
assert len(r) == 8
if len(r) != 8:
raise TransactionError("Invalid commitment")
return int.from_bytes(r, "big")
# confidential
r = stream.read(32)
assert len(r) == 32
if len(r) != 32:
raise TransactionError("Invalid commitment")
return c + r


Expand All @@ -71,10 +74,14 @@ def unblind(
message_length=64,
) -> tuple:
"""Unblinds a range proof and returns value, asset, value blinding factor, asset blinding factor, extra data, min and max values"""
assert len(pubkey) in [33, 65]
assert len(blinding_key) == 32
assert len(value_commitment) == 33
assert len(asset_commitment) == 33
if len(pubkey) not in [33, 65]:
raise TransactionError("Invalid pubkey length")
if len(blinding_key) != 32:
raise TransactionError("Invalid blinding key length")
if len(value_commitment) != 33:
raise TransactionError("Invalid value commitment length")
if len(asset_commitment) != 33:
raise TransactionError("Invalid asset commitment length")
pub = secp256k1.ec_pubkey_parse(pubkey)
secp256k1.ec_pubkey_tweak_mul(pub, blinding_key)
sec = secp256k1.ec_pubkey_serialize(pub)
Expand Down Expand Up @@ -397,9 +404,11 @@ def __init__(self, nonce, entropy, amount_commitment, token_commitment=None):
@classmethod
def read_from(cls, stream):
nonce = stream.read(32)
assert len(nonce) == 32
if len(nonce) != 32:
raise TransactionError("Invalid nonce")
entropy = stream.read(32)
assert len(entropy) == 32
if len(entropy) != 32:
raise TransactionError("Invalid entropy")
amount_commitment = read_commitment(stream)
token_commitment = read_commitment(stream)
return cls(nonce, entropy, amount_commitment, token_commitment)
Expand Down
3 changes: 2 additions & 1 deletion src/embit/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def secure_randint(vmin: int, vmax: int) -> int:
"""
import math

assert vmax > vmin
if vmax <= vmin:
raise ValueError("vmax must be greater than vmin")
delta = vmax - vmin
nbits = math.ceil(math.log2(delta + 1))
randn = getrandbits(nbits)
Expand Down
Loading
Loading