diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f00c60..b81787a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3094,4 +3094,174 @@ misleading. - Manually tested by the owner in Electrum 4.8.1: `NO_FUTURE_DATE`, `WILLEXECUTOR_FEE` and `No Heirs` were all confirmed on screen. +## 57. Remove all `copy.deepcopy` (ad-hoc copy helpers; `WillItem` copies serialize/deserialize) + +**Date:** 2026-08-28 + +**Goal (owner request):** eliminate every `copy.deepcopy` from the codebase +and replace it with ad-hoc copy methods; `WillItem` copies must be produced by +serializing and deserializing the item rather than by deep-copying live +runtime objects (which can hold a `threading.RLock` and cannot be pickled). + +**What changed:** + +- `bal/core/util.py`: new `copy_structure(value, _path="copy")` — the single + JSON-safe, deepcopy-free recursive cloner (dict / list / tuple cloned + structurally, JSON scalars kept as-is, any accidental runtime object coerced + to `str` + logged). It replaces the old `heirs._json_safe` implementation. +- `bal/core/heirs.py`: `_json_safe` is now a thin backward-compatible alias of + `bal.core.util.copy_structure`; `Heirs.save` behaviour is unchanged. +- `bal/core/will.py`: + - `WillItem.__init__` on a `WillItem` argument no longer does + `self.__dict__ = w.__dict__.copy()` + `copy.deepcopy`; instead it + serializes (`to_dict()`) and deserializes: the tx is re-parsed into a fresh + object, `STATUS` is rebuilt from a clone, and heirs / will-executors are + cloned recursively, so the copy shares no mutable state with the source. + - New `WillItem.copy(wallet=None)` (serialize/deserialize round trip; re-adds + wallet tx info when a wallet is passed) and the static + `WillItem.copy_status_table(table)` used for the `STATUS` tables. + - `to_dict()` now also emits `Father` / `Children` so the round trip is + faithful. + - `normalize_will` routes copies through the constructor / `copy()`. +- `bal/gui/qt/window.py` and `bal/cli/controller.py`: the Build-will flow now + uses `copy_structure(...)` instead of `copy.deepcopy(...)` for heirs and + will-executors. +- Dropped now-unused `import copy` (`will.py`, `controller.py`, `qt/common.py`, + `qt/window.py`). +- Tests updated to the same helpers: STATUS tables via + `WillItem.copy_status_table`, heirs / built dicts via `copy_structure` + (`test_core_will.py`, `test_core_will_invalidate.py`, + `test_heir_relative_anchor.py`, `test_anticipate_manual_locktime.py`, + `test_no_willexecutor_karen7.py`, `test_reproduce_none_type.py`, + `test_group_e_mock_karen7.py`, `test_group_e_karen7_invalidate.py`, + `sim_update_flows.py`). + +**Verification:** +- Full offline batch `tests/test_core_*.py tests/test_gui_*.py`: 377 passed, + only the two pre-existing `test_bt_to_date_*` datetime compare failures + remain (identical to HEAD — no regression; `test_heir_relative_anchor` + isolated-file failure is pre-existing test-pollution at HEAD too). +- Ad-hoc semantics check: `copy()`/ctor copy share no mutable state with the + source (mutating source heirs/STATUS does not leak into the copy and vice + versa), `copy_status_table` returns fresh lists, `normalize_will` runs. +- `ruff` on all touched files: no new violations (4 findings, all pre-existing + at HEAD). +- `tests/smoke_test.py electrum.plugins.bal`: passed. +- `python3 build_zip.py`: 45 files, 343591 bytes, sha256 `aa8f8154…`; + `tests/external_zip_test.py bal-electrum-plugin.zip`: passed (Plugin class + loads via the zipimport shim). + **Outcome:** DONE. + +--- + +## Next. Animated-QR interop (BC-UR v1/v2, BBQR) + +**Date:** 2026-09-08 + +**Goal:** Let BAL export/import a will not only as its own BAL QR frame format +but also as BC-UR v1 (`ur:bytes`, BC32 + SHA-256), BC-UR v2 (`ur:bytes`, CBOR +bytewords-minimal fountain codes) and BBQR (Coinkite `B$…`) animated-QR +sequences, so transfers interoperate with Blockchain Commons / Coldcard-style +tools and BitKit. Codecs must be stdlib-only and the export must keep BAL QR +as the default. + +**What changed:** + +- `bal/core/animated_qr.py` (new): stdlib-only codec module. + - BC32 (bech32_bis checksum, XOR `0x3FFFFFFF`) encode/decode matching the + BCR-2020-004/005 reference vectors. + - bytewords-minimal encode/decode (BCR-2020-012) with CRC-32 rejection; + the word list was transcribed verbatim from the reference C++. + - BC-UR v2: CBOR part writer/reader, CRC-32, `choose_fragments` + (xoshiro256** + alias + ary-threshold sampler) and XOR-based fountain + mixing/solving; emits a redundant mixed wave for loss tolerance. + - BC-UR v1: multipart with SHA-256 digest and single-part digest-less + frames; `1of1` handling. + - BBQR: base32 (encoding `2`), hex (uppercase, `H`) and zlib (lowercase, + `Z`, automatic compression fallback) frames; out-of-order reconstruction. + - One `AnimatedQrSession` + `detect_format` + `parse_for_detection` for + auto-detecting the incoming format and keying the GUI debounce. + - Safety caps: `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`, + zlib-bomb guard, `TransferConflictError`/`SessionLimitError`. +- `bal/gui/qt/dialogs.py`: + - Export page (`BalQrExportWidget`) gained a **Format** selector + (BAL QR default, BC-UR v1, BC-UR v2, BBQR) reusing the QR-size presets, + with per-format intro/format-hint text. + - Import page (`BalQrImportWidget`) now routes every frame through + `parse_for_detection` + `AnimatedQrSession.add_part`, auto-detecting the + format and resetting when the transfer's session key changes; the + review/sign step resolves the session and decodes parts uniformly. + - `qr_import_accept_frame` generalised to + `(state, fmt, session_key, frame_total, index, payload, stable_reads=2)`. +- `tests/test_core_animated_qr.py` (new, 32 tests): BC32 spec vectors, + bytewords round-trip/CRC, C++ reference-frame decode+re-encode parity + (single-part 12B, seq_len=2, seq_len=7), fountain solve with missing pure + part, out-of-order/duplicate handling, single/multipart UR v1, BBQR + Z/2/H round-trips, runt last part, zlib-bomb guard, detection positive/ + negative. + +**Verification:** + +- `tests/test_core_animated_qr.py`: 32/32 pass. +- `tests/test_gui_qr_transfer.py` (now 36 tests) + `test_gui_export_dialogs.py`: pass. +- `ruff` clean on `animated_qr.py`, `dialogs.py` and both test files; + `pyright` 0 errors on the touched modules. +- `tests/smoke_test.py electrum.plugins.bal`, `python3 build_zip.py` and + `external_zip_test.py` all pass. +- Full regression: 462 passed; only pre-existing failures remain + (`test_bt_to_date_*`, will-invalidate fee, unrelated `sign_transactions` + stub test). + +**Notes / caveats:** + +- A real bug was found & fixed during this work: `_ur2_part_cost` used + `2 * body_len` but `bytewords_minimal_encode` appends a 4-byte CRC, so every + UR v2 frame was undercounted by 8 characters and could overflow the QR + budget for large transfers. +- UR v1 multipart emits the digest-carrying `1of1//` form for a + single part (both headered and headerless single parts are accepted on + import); this keeps deterministic digest verification. +- Imported payloads are UTF-8 text; the codec sessions do not decode raw + binary transfer blobs. + +**Outcome:** DONE (uncommitted). + +--- + +## Animated-QR bugfix: QVideoSink signal wiring + will-export JSON crash + +**Date:** 2026-09-08 + +**Goal:** Fix two runtime crashes found by manual testing of the QR paths. + +**What changed:** + +- `bal/gui/qt/dialogs.py`: + - `_start_scan`/`_stop_scan` used `QVideoSink.videoFrame.connect/.disconnect`, + but on PyQt6 `videoFrame` is the frame **getter method**, not a signal — + this raised ``AttributeError: 'builtin_function_or_method' object has no + attribute 'connect'`` on camera scan. Switched to the `videoFrameChanged` + signal (same wiring Electrum's `QrReaderVideoSurface` uses). + - `_stop_scan` now tolerates `AttributeError` when disconnecting the sink + and guards the `errorOccurred` disconnect too, so a mid-init failure can + never cascade into a second uncaught exception. + - `_whole_will_json` (whole-will QR export) serialized ``WillItem.to_dict()`` + with plain `json.dumps`, crashing with ``TypeError: Object of type + Transaction is not JSON serializable`` (the ``tx`` field holds a real + ``Transaction``). Now uses Electrum's `MyEncoder`, matching `write_json_file`. +- `tests/test_gui_qr_transfer.py`: new `test_import_start_stop_scan_signal_wiring` + drives the real `QVideoSink` life-cycle with a mocked camera and fails if + the signal name regresses to `videoFrame`. +- `tests/test_gui_export_dialogs.py`: new + `test_qr_whole_will_json_serializes_transaction` covers the JSON export. + +**Verification:** + +- `pytest tests/test_gui_qr_transfer.py tests/test_gui_export_dialogs.py -q`: pass. +- Full offline batch `tests/test_core_*.py tests/test_gui_*.py`: 444 passed. +- Regression test flips correctly (fails when reverted to the buggy call). +- `ruff` clean on touched files; `tests/smoke_test.py`, `build_zip.py`, + `external_zip_test.py` all pass. + +**Outcome:** DONE (uncommitted). diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 8606368..d5db6e7 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -23,6 +23,25 @@ is updated to mark them as supported. See [`README.md`](README.md) for supported Electrum versions (currently 4.7.2 and 4.8.0). +## QR wire-format compatibility + +BAL exports/imports wills as QR codes. **BAL QR** (the default) is the plugin's +own frame format and is only understood by BAL itself. The export page also +supports **BC-UR v1**, **BC-UR v2** and **BBQR**: + +| Format | Wire appearance | Interop target | +|-----------|----------------------------|------------------------------------------------------| +| BAL QR | `BALQR1\|total\|index\|…` | Past/other BAL versions (default, always exported) | +| BC-UR v1 | `ur:bytes/` | Blockchain Commons / Coldcard-style UR (BC32, SHA-256 digest, part counts per part) | +| BC-UR v2 | `ur:bytes/-/` | BC-UR 2.x fountain codes (CBOR parts, CRC-32, bytewords-minimal) | +| BBQR | `B$…` | Coinkite BitKit / Coldcard's BBQR animated-QR mode | + +Import auto-detects the format of each scanned code; out-of-order, duplicate +and (for UR v2) partially-lost fountain frames are handled. Interop is +validation-tested against the reference C++ bc-ur encoder output and the +BCR-2020-004/005 BC32 test vectors; it has not yet been cross-verified against +third-party libraries (`ur`, `bbqr`, Coldcard firmwares). + ## Reporting compatibility issues If you find a compatibility problem not listed here, please open an issue on diff --git a/HANDOFF.md b/HANDOFF.md index f639ab0..00d3612 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -395,19 +395,74 @@ See Section 5 for details. ### In progress: QR / audio will transfer (branch `feature/bal-qr-transfer`) -- P0–P4 of `PLAN_QR_TRANSFER.md` are implemented on - `feature/bal-qr-transfer` and un-committed: core scheduler - (`bal/core/qrtransfer.py`), settings row, export/import dialogs + - review/sign wizard + lists/window wiring, all with tests - (`tests/test_core_qr_transfer.py`, `tests/test_gui_qr_transfer.py`) and - docs (README, CHANGELOG entry 56, QML_PLAN, this file). -- Remaining: **P6** release hygiene — `python3 build_zip.py`, external-zip - test, full ruff check (no NEW violations; baseline is intentionally dirty), - and a manual on-device walkthrough of the QR path. The audio path is - optional and hidden until the `audio_modem` plugin + `amodem` are - installed. -- Commit policy unchanged: nothing is committed until the owner explicitly - confirms. +- The full QR-transfer feature (P0–P6) is implemented, tested and committed on + `feature/bal-qr-transfer` (commits `d288b55`, `ce3e36d`, pushed to + `origin`). PR creation URL: + `https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/pulls/new/feature/bal-qr-transfer` +- Included: core scheduler (`bal/core/qrtransfer.py`), `QR_CHUNK_SIZE` + setting (4 export presets), export/import dialogs + review/sign wizard + + lists/window wiring, export filters, auto slideshow with per-second rate + + loop option, audio send/receive buttons, and the crash fixes + (`status` default, `invalidate_will` guard). Docs: README, CHANGELOG entry + 56, QML_PLAN, `AUDIO_MODEM_DEBIAN.md`. +- Follow-up refactor (CHANGELOG entry 57): all `copy.deepcopy` removed — + `copy_structure()` in `bal/core/util.py`, `WillItem.copy()` / ctor + serialize/deserialize, `copy_status_table()`. Working tree clean after the + branch's three commits. +- Verification: batch 377 passed / 2 pre-existing `test_bt_to_date_*` + failures; ruff no new violations; smoke + `build_zip.py` + + external-zip OK; pyright clean. The isolated + `test_heir_relative_anchor.py::test_karen7_frozen_delivery_not_expired` + failure is pre-existing test pollution (fails identically on clean HEAD, + passes inside the full batch) — not caused by entry 57. +- Remaining: manual on-device walkthrough of the QR path (and, if wanted, + the audio path — buttons only appear when the `audio_modem` plugin + + `amodem` are installed; see the prerequisites below). + +### In progress: animated-QR interop (BC-UR v1/v2, BBQR) + +- `bal/core/animated_qr.py` implements stdlib-only codecs for **BC-UR v1** + (BC32 + SHA-256 digest; the bech32_bis checksum variant per + BCR-2020-004/005), **BC-UR v2** (CBOR part structure, bytewords-minimal, + CRC-32, xoshiro256-based fountain with alias-sampled mixing) and **BBQR** + (Coinkite `B$…` base32/hex/zlib frames), plus one shared + `AnimatedQrSession` with `detect_format` auto-detection and + `parse_for_detection` frame identity for the GUI debounce. +- Current status as of this session: reference parity, GUI, and tests done; + not yet committed. + - **BC32/bytewords/codec parity:** BC32 reproduces the BCR-2020-004/005 + test vectors (`Hello, world`, `Hello world`, the long seed vector); + bytewords-minimal round-trips with CRC rejection; UR v2 part encode + + decode is byte-exact against the reference C++ bc-ur encoder for a + single part, seq_len=2 (12 frames) and seq_len=7 (3 sampled mixes), + validating CBOR framing, bytewords, alias+ary-threshold sampling, + xoshiro256** and the XOR mix. + - **Sessions:** UR v2 single-part (no seq header), out-of-order frames, + duplicate drops, solve with a missing pure fragment (a second redundant + mixed wave is emitted by `ur2_frames`), UR v1 single-part + (digest-less `ur:bytes/` accepted) and multipart, BBQR full-frame + decode in any order for Z/2/H encodings. + - **Safety:** `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`, + `TransferConflictError` on a frame from a different transfer, + `SessionLimitError`, BBQR zlib-bomb guard, UTF-8 payloads only. + - **GUI:** `BalQrExportWidget` gained a Format selector (BAL QR default, + BC-UR v1, BC-UR v2, BBQR) reusing the QR-size presets; the importer now + routes every frame through `detect_format` + + `AnimatedQrSession.add_part` with the shared + `qr_import_accept_frame(state, fmt, session_key, frame_total, index, + payload, stable_reads=2)` debounce (reset on session-key change). + `_review_and_sign` resolves the session to the transfer text and decodes + parts uniformly across formats. + - **Verification:** `tests/test_core_animated_qr.py` (32 tests incl. the + C++-reference parity vectors and BC32 spec vectors) and the extended + `tests/test_gui_qr_transfer.py` pass; ruff clean on the new/changed + files; pyright 0 errors; smoke test, `build_zip.py` and + `external_zip_test.py` green. Only the pre-existing failures remain + (`test_bt_to_date_*`, fee-exceeds-balance, karen7 pollution). + - **Any remaining work:** manual on-device walkthrough of the QR path with + the new formats; optionally validate against third-party libraries + (`ur`, `bbqr`) once available; add the docstrings/branch notes already + captured in `ag1.md`/`ag2.md` context where needed. **Dev-box audio prerequisites (audio_modem channel):** See `AUDIO_MODEM_DEBIAN.md` — the full Debian setup + verification, with the diff --git a/README.md b/README.md index c9bb548..0963157 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ bal/ the installable Electrum plugin package │ ├── willexecutors.py │ ├── checkalive.py │ ├── reminders.py -│ ├── qrtransfer.py QR will-transfer wire format / chunk scheduler +│ ├── qrtransfer.py BAL QR will-transfer wire format / chunk scheduler +│ ├── animated_qr.py BC-UR v1/v2 + BBQR codecs (stdlib-only) │ └── input_rules.py ├── cli/ headless command-line layer (no Qt) │ ├── commands.py bal_* daemon commands (@plugin_command) @@ -97,7 +98,19 @@ export offers All / Valid / Valid-NC filters plus a QR size preset one at a time, then proposes exporting the signed transactions. When Electrum's `audio_modem` plugin is enabled (optional, requires `amodem` + PortAudio) Send/Receive audio buttons complement the QR channel. See -[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the full wire-format spec. +[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the BAL QR wire-format spec. + +### Animated-QR formats (interop) + +BAL QR is the default export format, but the export page's **Format** selector +also emits **BC-UR v1** (`ur:bytes`, BC32 + SHA-256), **BC-UR v2** +(`ur:bytes`, CBOR fountain codes) and **BBQR** (`B$…`, Coinkite, used by +BitKit) animated-QR sequences. The importer auto-detects the format of each +code it sees, so any of the four formats can be imported on a BAL device, and +a BAL export can be imported by any tool that understands these standards. +UR v2 imports tolerate out-of-order and duplicate frames (fountain decoding); +BBQR frames may arrive in any order. Rotation/redundancy caps and the +32 MB message limit (zlib-bomb guard) bound untrusted scanner input. ## Command-line / headless usage @@ -195,6 +208,15 @@ QT_QPA_PLATFORM=offscreen python3 tests/smoke_test.py electrum.plugins.bal QT_QPA_PLATFORM=offscreen python3 tests/external_zip_test.py bal-electrum-plugin.zip ``` +## Companion: Android reader + +The `android/` subfolder contains **BAL Reader**, a simple Android app that +reads the QR wills this plugin exports (BAL QR, BC-UR v1/v2, BBQR) straight +from the screen with your phone — view, copy, share, or save the recovered +will. It reuses the plugin's own QR codecs via Chaquopy (the very Python +modules that power the desktop import), so decoding behaviour is identical. +See [`android/README.md`](android/README.md). + ## ⚠️ Safety This plugin builds real Bitcoin inheritance transactions with time-locks. Test