Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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
444fec6
Add testnet4 network support (Bitcoin Core 28.0+)
al-munazzim Feb 8, 2026
ea240d2
Add GitHub Actions workflow for tests
al-munazzim Feb 8, 2026
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
58 changes: 58 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Tests

on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]

jobs:
test-python:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package
run: pip install -e .

- name: Run tests
run: |
cd tests
python run_tests.py

test-micropython:
name: MicroPython (Unix port)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install MicroPython Unix port
run: |
sudo apt-get update
sudo apt-get install -y build-essential libffi-dev pkg-config
git clone --depth 1 https://github.com/micropython/micropython.git /tmp/micropython
cd /tmp/micropython/ports/unix
make submodules
make

- name: Run tests on MicroPython
run: |
cd tests
/tmp/micropython/ports/unix/build-standard/micropython run_tests.py
continue-on-error: true # MicroPython may lack some deps
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
19 changes: 11 additions & 8 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 @@ -292,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 @@ -301,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
Loading