Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,21 @@ jobs:
- run: python tools/differential_correction.py --verify
- run: python -m build --no-isolation
- run: python tools/verify_wheel_environment.py

compatibility:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.14", "3.15"]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: ${{ matrix.python-version == '3.15' }}
- run: python -m pip install --upgrade pip
- run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt
- run: python -m pip install --no-build-isolation -e '.[dev]'
- run: python -m pip check
- run: python -m pytest -q
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ contains offline verification utilities.

## Development and verification

Use the existing virtual environment when available. Python 3.12 is the minimum;
CI also covers 3.13. Install development dependencies only when needed:
Use the existing virtual environment when available. Python 3.10 is the minimum;
the supported range is Python 3.10 through 3.15. Install development
dependencies only when needed:
`python -m pip install -e '.[dev]'`. Run the CLI with `codex32 --help`.

Choose checks according to the changed behavior:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ obtain an independent review before relying on it with funds. See

## Install

Python 3.12 or 3.13 is required. The installed package has no third-party
Python 3.10 through 3.15 is supported. The installed package has no third-party
runtime dependencies. To install it with the pinned build backend, run these
commands from the project folder:

Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [
]
description = "Python reference implementation for codex32 (BIP93) and codex32-encoded master seeds."
readme = "README.md"
requires-python = ">=3.12,<3.14"
requires-python = ">=3.10,<3.16"
license = "MIT"
license-files = ["LICENSE*"]
maintainers = [
Expand All @@ -22,8 +22,12 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Operating System :: OS Independent",
"Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries",
Expand Down Expand Up @@ -52,7 +56,7 @@ where = ["src"]
testpaths = ["tests"]

[tool.mypy]
python_version = "3.12"
python_version = "3.10"
strict = true

[tool.ruff]
Expand Down
4 changes: 2 additions & 2 deletions src/codex32/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Callable, Sequence
from collections.abc import Set as AbstractSet
from dataclasses import dataclass
from typing import Never, SupportsIndex, cast
from typing import NoReturn, SupportsIndex, cast

from codex32._bip32 import _valid_root
from codex32.bech32 import CHARSET, _u5_to_chars, convertbits
Expand Down Expand Up @@ -198,7 +198,7 @@ class CreationCeremony:
def __init__(self) -> None:
raise TypeError("use a CreationCeremony class constructor")

def __reduce_ex__(self, _protocol: SupportsIndex) -> Never:
def __reduce_ex__(self, _protocol: SupportsIndex) -> NoReturn:
raise TypeError("creation ceremonies cannot be copied or serialized")

@classmethod
Expand Down
10 changes: 6 additions & 4 deletions src/codex32/profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@

from __future__ import annotations

from enum import StrEnum
from typing import TYPE_CHECKING
from enum import Enum
from typing import TYPE_CHECKING, TypeAlias, Union

from codex32.errors import UnknownProfile

if TYPE_CHECKING:
from codex32.profiles.bip39 import _Bip39Rules
from codex32.profiles.cl32 import _Cl32Rules
from codex32.profiles.ms32 import _Ms32Rules
type _ProfileRules = _Ms32Rules | _Cl32Rules | _Bip39Rules
_ProfileRules: TypeAlias = Union["_Ms32Rules", "_Cl32Rules", "_Bip39Rules"]


class Profile(StrEnum):
class Profile(str, Enum):
__str__ = str.__str__

MS = "ms"
CL = "cl"
BIP39_12W = "bip39_12w"
Expand Down
2 changes: 1 addition & 1 deletion src/codex32/profiles/bip39.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def validate_payload(self, symbols: tuple[int, ...], index: str) -> None:
semantic = value >> self.outer_padding
embedded = semantic & ((1 << self.checksum_bits) - 1)
entropy = semantic >> self.checksum_bits
entropy_bytes = entropy.to_bytes(self.entropy_bits // 8)
entropy_bytes = entropy.to_bytes(self.entropy_bits // 8, "big")
expected = hashlib.sha256(entropy_bytes).digest()[0] >> (8 - self.checksum_bits)
if embedded != expected:
raise InvalidBip39Checksum("embedded BIP39 entropy checksum is invalid")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
from codex32.errors import CodexError, InvalidChecksum


def test_profile_string_matches_its_value() -> None:
assert str(Profile.MS) == "ms"


def test_vector_1_parts_and_seed() -> None:
secret = parse_codex32(VECTOR_1["secret_s"])
assert isinstance(secret, MasterSeed)
Expand Down
18 changes: 10 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,12 @@ def isatty(self) -> bool:


class _CreationOutput(io.StringIO):
def __init__(self, *, pretty: bool = False) -> None:
def __init__(self) -> None:
super().__init__()
self.pretty = pretty
self.checks = 0
self.interactive = True

def isatty(self) -> bool:
self.checks += 1
return self.pretty or self.checks == 1
return self.interactive


@dataclass
Expand Down Expand Up @@ -151,12 +149,16 @@ def _invoke_terminal(args: list[str], *lines: str) -> _Result:
def _invoke_confirmed_create(
args: list[str],
*lines: str,
terminal_output: bool = False,
core: _FakeBitcoinCore | None = None,
) -> _Result:
stdin = _TTYInput("\n".join(lines) + "\n")
stdout = _CreationOutput(pretty=terminal_output)
stdout = _CreationOutput()
stderr = io.StringIO()
selected_core = core or _FakeBitcoinCore()

def connect(*_args: object, **_kwargs: object) -> _FakeBitcoinCore:
stdout.interactive = False
return selected_core

def confirm_card(
artifact: Share | Secret,
Expand All @@ -169,7 +171,7 @@ def confirm_card(
with (
patch.object(sys, "stdin", stdin),
patch("codex32.cli._confirm_card", confirm_card),
patch("codex32.cli.BitcoinCore.connect", return_value=core or _FakeBitcoinCore()),
patch("codex32.cli.BitcoinCore.connect", side_effect=connect),
contextlib.redirect_stdout(stdout),
contextlib.redirect_stderr(stderr),
):
Expand Down
Loading