Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ad4b245
Initial BIP-352 support
kdmukai Aug 26, 2024
575e80b
Add labeled SP addrs
kdmukai Dec 31, 2024
d91a969
bip352: restrict labels to 32-bit ints, fix formatting
odudex May 29, 2026
78ce0cb
chore: make bech32 flexible for large payloads(sp)
notTanveer Jul 11, 2025
1fa298f
chore: replace None w error msg
notTanveer Jul 11, 2025
c1f3608
sp checks
notTanveer Jul 15, 2025
8356d27
bech32: make Bech32DecodeError an EmbitError and tighten validation
odudex May 29, 2026
77cf14a
feat: silent payment send support
notTanveer May 21, 2026
735eef5
test(sp): outputs and BIP-352 test vectors
notTanveer May 21, 2026
a7ebf3f
sp: make silent payment code MicroPython-safe
odudex May 29, 2026
de23863
feat(BIP-392): sp descriptors
notTanveer May 25, 2026
b191771
sp descriptors: use flexible bech32, robust parsing, clearer errors
odudex May 29, 2026
c8cb4e4
secp256k1: implement ec_pubkey_tweak_mul and ec_pubkey_combine python…
kdmukai May 13, 2026
6f1be83
secp256k1: fix mutable key in tweak functions and add ec_privkey_twea…
miketlk May 15, 2026
585c52d
bip352: pass bytearray to in-place pubkey tweaks in create_outputs
odudex May 29, 2026
874c727
psbt: add PSBTv2 per-input locktime fields and version-aware scope pa…
notTanveer May 11, 2026
11eab7a
psbt: implement PSBTv2 global parsing and serialization
notTanveer May 11, 2026
e8a272a
psbt: add GLOBAL_TX_MODIFIABLE API, add_input/add_output, update sign…
notTanveer May 11, 2026
8699ecf
tests: add BIP-370 PSBTv2 test vectors
notTanveer May 11, 2026
c870c8a
psbtview: add PSBTv2 support
notTanveer May 13, 2026
b4da2cd
fix: tighten PSBTv2 review follow-ups
odudex May 14, 2026
2bc8147
fix: restore old MicroPython-compatible PSBT errors
odudex May 14, 2026
4aca48b
fix(psbt): avoid signed byte conversion kwargs
odudex May 14, 2026
658d06f
fix(psbtview): read global tx version as signed int32
odudex Jun 1, 2026
2c68073
fix(psbt): don't reject valid appends under SIGHASH_SINGLE
odudex Jun 1, 2026
6e42654
fix(psbtview): pass version when parsing extra scope streams
odudex Jun 1, 2026
b09c135
fix(psbt): preserve an explicit nSequence of 0 in vin
odudex Jun 1, 2026
3bbb2d2
fix(psbt): don't clobber taproot_internal_key on combine
odudex Jun 1, 2026
8baf31f
feat(BIP-374): dleq proofs
notTanveer May 15, 2026
b6b5a01
test: dleq proofs
notTanveer May 15, 2026
d914f2f
fix(dleq): wrap parsed points in bytearray for in-place tweaks
odudex Jun 1, 2026
1f74ac0
test(dleq): run BIP-374 vectors on the pure-Python backend
odudex Jun 1, 2026
0a1b31e
fix(dleq): match BIP-374 reference on zero-scalar verify terms
odudex Jun 1, 2026
25b49c2
feat: add SP PSBT support (375 & 376)
notTanveer May 30, 2026
78cb7a7
fix(sp): correct SP input eligibility (taproot, NUMS, segwit v>1)
odudex Jun 1, 2026
101bba4
fix(sp): correct SP output-script derivation and ordering
odudex Jun 1, 2026
83ed75c
fix(sp): compute input_hash over all inputs, not just eligible ones
odudex Jun 1, 2026
25317d9
fix(sp): require PSBT_IN_BIP32_DERIVATION for non-taproot DLEQ inputs
odudex Jun 1, 2026
9eb4211
test(sp): drive all official BIP-375 vectors through the validator
odudex Jun 1, 2026
95a053f
feat(sp): sign taproot inputs for silent payments (BIP-352 negation)
odudex Jun 1, 2026
01065d8
feat(sp): enforce SIGHASH_ALL and verify existing ECDH shares when si…
odudex Jun 1, 2026
6f1e430
refactor(sp): use embit.misc.urandom instead of os.urandom
odudex Jun 1, 2026
9b2f4da
chore(sp): drop now-unused validator imports
odudex Jun 1, 2026
3aa557d
fix: MicroPython friendly syntax
odudex Jun 2, 2026
3fbcd45
fix: MicroPython friendly COutPoint.serialize (no stepped slice)
odudex Jun 2, 2026
36e60e4
feat: P2TR input support for silent payments send
odudex Jun 2, 2026
72a591b
refactor(sp): audit fixes — version guard, error propagation, memory
notTanveer Jun 3, 2026
aff9a67
refactor(sp): replace local NUMS_H with ec.NUMS_PUBKEY.xonly()
notTanveer Jun 3, 2026
84879bc
test(sp): fix lint nits — single import style, drop unused var
odudex Jun 3, 2026
b7b1941
fix(bech32): re-enforce 90-char segwit limit in decode()
odudex Jun 3, 2026
6cddba0
fix(psbt): reject negative PSBT_OUT_AMOUNT at parse time
odudex Jun 3, 2026
f77b590
fix(sp): propagate SPValidationError from _sign_with_sp
odudex Jun 3, 2026
b0984fb
fix(sp): case-insensitive HRP detection, drop stale TODO
odudex Jun 3, 2026
114230e
test(sp): assert per-label and narrow exception types for invalid labels
odudex Jun 3, 2026
bb540d7
fix(sp): reject global DLEQ proof without its ECDH share
notTanveer Jun 5, 2026
d432a71
fix(sp): raise clear errors for empty outpoints and empty key expression
odudex Jun 8, 2026
f6c7de7
fix(sp): correct k assignment for interleaved recipients, add with_sp…
notTanveer Jun 12, 2026
7866ba9
fix(sp): add kmax limit
notTanveer Jun 12, 2026
6ad890b
test(sp): cover interleaved k assignment; document create_outputs vou…
odudex Jun 13, 2026
f49bc62
fix(sp): parse origin-prefixed key in-place to preserve two-arg stream
odudex Jun 15, 2026
cf085f8
fix(psbtview): reject duplicated global unsigned transaction
odudex Jun 15, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ settings.json
src/embit/util/prebuilt/*.so
src/embit/util/prebuilt/*.dylib
src/embit/util/prebuilt/*.dll

.claude/
CLAUDE.md
77 changes: 56 additions & 21 deletions src/embit/bech32.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
# THE SOFTWARE.

"""Reference implementation for Bech32 and segwit addresses."""

from .misc import const
from .base import EmbitError

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32_CONST = const(1)
Expand All @@ -33,6 +35,10 @@ class Encoding:
BECH32M = 2


class Bech32DecodeError(EmbitError):
pass


def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
Expand Down Expand Up @@ -77,21 +83,32 @@ def bech32_encode(encoding, hrp, data):

def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return (None, None, None)
if any(ord(x) < 33 or ord(x) > 126 for x in bech):
raise Bech32DecodeError("Invalid character in input")
if bech.lower() != bech and bech.upper() != bech:
raise Bech32DecodeError("Mixed case strings not allowed")
bech = bech.lower()
pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None, None)
if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None, None)
if pos < 1:
raise Bech32DecodeError("Separator '1' not found or misplaced")
# BIP-173: the HRP must contain 1 to 83 characters.
if pos > 83:
raise Bech32DecodeError("HRP too long (max 83 characters)")
if pos + 7 > len(bech):
raise Bech32DecodeError("Data part too short")
# BIP-173 caps Bech32 strings at 90 chars, but BIP-352 silent payment
# addresses are longer (>=117) and recommend a 1023-char limit to leave
# room for future versions.
if len(bech) > 1023:
raise Bech32DecodeError("String too long (max 1023 characters)")
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos + 1 :]]
data_part = bech[pos + 1 :]
if not all(x in CHARSET for x in data_part):
raise Bech32DecodeError("Data part contains invalid characters")
data = [CHARSET.find(x) for x in data_part]
encoding = bech32_verify_checksum(hrp, data)
if encoding is None:
return (None, None, None)
raise Bech32DecodeError("Checksum verification failed")
return (encoding, hrp, data[:-6])


Expand All @@ -104,7 +121,7 @@ def convertbits(data, frombits, tobits, pad=True):
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
raise Bech32DecodeError("Invalid input value for bit conversion")
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
Expand All @@ -114,33 +131,51 @@ def convertbits(data, frombits, tobits, pad=True):
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
raise Bech32DecodeError("Invalid padding in bit conversion")
return ret


def decode(hrp, addr):
"""Decode a segwit address."""
"""Decode a segwit address.

Silent payment (sp/tsp) addresses are not witness programs and must not be
decoded here; use bech32_decode + convertbits for those.
"""
# BIP-173/BIP-350: segwit addresses are capped at 90 characters.
if len(addr) > 90:
raise Bech32DecodeError("Segwit address too long (max 90 characters)")
encoding, hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
raise Bech32DecodeError("HRP mismatch: expected {}, got {}".format(hrp, hrpgot))
decoded = convertbits(data[1:], 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return (None, None)
# BIP-141: a witness program is 2 to 40 bytes.
if len(decoded) < 2 or len(decoded) > 40:
raise Bech32DecodeError("Invalid witness program length")
if data[0] > 16:
return (None, None)
raise Bech32DecodeError("Invalid witness version")
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None)
raise Bech32DecodeError("Invalid witness program length for version 0")
if (data[0] == 0 and encoding != Encoding.BECH32) or (
data[0] != 0 and encoding != Encoding.BECH32M
):
return (None, None)
raise Bech32DecodeError("Invalid encoding for witness version")
return (data[0], decoded)


def encode(hrp, witver, witprog):
"""Encode a segwit address."""
if witver < 0 or witver > 16:
raise Bech32DecodeError("Invalid witness version")
if len(witprog) < 2 or len(witprog) > 40:
raise Bech32DecodeError("Invalid witness program length")
if witver == 0 and len(witprog) != 20 and len(witprog) != 32:
raise Bech32DecodeError("Invalid witness program length for version 0")

encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5))
if decode(hrp, ret) == (None, None):
return None

# Sanity check: the result must round-trip. Any failure propagates with its
# original, descriptive Bech32DecodeError.
decode(hrp, ret)

return ret
1 change: 1 addition & 0 deletions src/embit/descriptor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import miniscript
from .descriptor import Descriptor
from .arguments import Key
from .sp import SilentPaymentDescriptor
10 changes: 10 additions & 0 deletions src/embit/descriptor/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .miniscript import Miniscript, Multi, Sortedmulti
from .arguments import Key
from .taptree import TapTree
from .sp import SilentPaymentDescriptor


class Descriptor(DescriptorBase):
Expand Down Expand Up @@ -302,6 +303,15 @@ def read_from(cls, s):
is_miniscript = True
taproot = False
taptree = TapTree()
if start.startswith(b"sp("):
# rewind to right after "sp(" with a relative seek (robust to a
# short read(8)). MicroPython's BytesIO has no tell(), so avoid it.
s.seek(3 - len(start), 1)
sp_desc = SilentPaymentDescriptor.read_from(s)
end = s.read(1)
if end != b")":
raise DescriptorError("Expected closing ) for sp()")
return sp_desc
if start.startswith(b"tr("):
taproot = True
s.seek(-5, 1)
Expand Down
Loading
Loading